air_sys_syscall/fs/inotify.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! **inotify** — surveillance de changements de fichiers (couche 0, non
6//! privilégié, basé sur les chemins). Sous-module dédié `fs::inotify`, dans
7//! l'esprit FD-événementiel de `signalfd`/`timerfd`/`eventfd`.
8//!
9//! Référence normative : `docs/specs/layer-0/family-fs-inotify.md`.
10//!
11//! - **`CLOEXEC` par défaut** sur le FD ([`inotify_init`]).
12//! - **Décodage zéro-allocation emprunté** des `inotify_event` de taille variable
13//! ([`InotifyEvents`], `air-sys-types::fs`) ; **zéro perte** (ADR-032) : tous les
14//! événements complets sont rendus, une queue tronquée est *signalée*.
15//! - **Récursivité = couche 1** (`AirFileSystemWatcher`), pas ici. **`fanotify`
16//! hors périmètre** (primitif privilégié distinct).
17
18use super::{errno_from_negative_syscall_ret, fd_to_u64, nr, read, syscall1, syscall2, syscall3};
19use air_sys_types::Errno;
20use air_sys_types::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
21use air_sys_types::fs::{InotifyEventMask, InotifyEvents, InotifyFlags, WatchDescriptor};
22use core::ffi::CStr;
23
24/// Taille de buffer **recommandée** pour [`Inotify::read_events`] : un événement
25/// fait 16 octets d'entête + `name` (jusqu'à `NAME_MAX + 1`). 4096 octets
26/// accueillent confortablement un lot d'événements.
27pub const INOTIFY_RECOMMENDED_BUFFER_SIZE: usize = 4096;
28
29/// Instance inotify : possède un [`OwnedFd`], fermé au `Drop`.
30#[derive(Debug)]
31pub struct Inotify {
32 fd: OwnedFd,
33}
34
35/// Crée une instance inotify. **`IN_CLOEXEC` est posé par défaut** (tous les FD
36/// ouverts par Air ont `CLOEXEC`) ; `flags` ajoute par exemple
37/// [`InotifyFlags::NONBLOCK`].
38///
39/// # Errors
40///
41/// - [`Errno::EMFILE`]/[`Errno::ENFILE`] : limite de FD (processus / système) ou
42/// d'instances inotify (`max_user_instances`) atteinte.
43/// - [`Errno::ENOMEM`] : mémoire kernel insuffisante.
44pub fn inotify_init(flags: InotifyFlags) -> Result<Inotify, Errno> {
45 // `CLOEXEC` par défaut (jamais à la charge de l'appelant).
46 let raw_flags = (flags | InotifyFlags::CLOEXEC).bits();
47 // SAFETY: `inotify_init1` ne lit aucune mémoire utilisateur ; `raw_flags` est
48 // un jeu de drapeaux `IN_*` valides (NONBLOCK/CLOEXEC).
49 let ret = unsafe { syscall1(nr::INOTIFY_INIT1, u64::from(raw_flags.cast_unsigned())) };
50 if ret < 0 {
51 return Err(errno_from_negative_syscall_ret(ret));
52 }
53 let fd = i32::try_from(ret).map_err(|_| Errno::EINVAL)?;
54 Ok(Inotify {
55 // SAFETY: `inotify_init1` a retourné un FD frais, que personne d'autre ne possède
56 // (CLOEXEC posé) : il est pris en propriété une seule fois, ici.
57 fd: unsafe { OwnedFd::from_raw_fd(fd) },
58 })
59}
60
61impl Inotify {
62 /// FD emprunté (intégration dans une boucle d'événements — `poll`/io_uring).
63 #[must_use]
64 pub fn as_fd(&self) -> BorrowedFd<'_> {
65 self.fd.as_fd()
66 }
67
68 /// Consomme l'instance et rend l'[`OwnedFd`] (transfert d'ownership).
69 #[must_use]
70 pub fn into_fd(self) -> OwnedFd {
71 self.fd
72 }
73
74 /// Ajoute (ou met à jour, avec [`InotifyEventMask::MASK_ADD`]) un watch sur
75 /// `path` pour les événements `mask`. `path` : `&CStr` (octets terminés NUL,
76 /// convention couche 0).
77 ///
78 /// # Errors
79 ///
80 /// - [`Errno::ENOSPC`] : limite `max_user_watches` atteinte.
81 /// - [`Errno::ENOENT`] : `path` n'existe pas.
82 /// - [`Errno::EACCES`] : accès en lecture refusé sur `path`.
83 /// - [`Errno::ENOTDIR`] : [`InotifyEventMask::ONLYDIR`] posé et `path` n'est
84 /// pas un répertoire.
85 pub fn add_watch(&self, path: &CStr, mask: InotifyEventMask) -> Result<WatchDescriptor, Errno> {
86 // SAFETY: `path` est un `CStr` terminé NUL, valide en lecture pour la
87 // durée de l'appel ; `fd` est le FD inotify possédé ; `inotify_add_watch`
88 // lit `path` (ne l'écrit pas) et `mask` est un scalaire.
89 let ret = unsafe {
90 syscall3(
91 nr::INOTIFY_ADD_WATCH,
92 fd_to_u64(self.fd.as_raw_fd()),
93 path.as_ptr() as u64,
94 u64::from(mask.bits()),
95 )
96 };
97 if ret < 0 {
98 return Err(errno_from_negative_syscall_ret(ret));
99 }
100 let wd = i32::try_from(ret).map_err(|_| Errno::EINVAL)?;
101 Ok(WatchDescriptor::from_raw(wd))
102 }
103
104 /// Retire le watch `wd`.
105 ///
106 /// # Errors
107 ///
108 /// - [`Errno::EINVAL`] : `wd` n'est pas un descripteur de watch valide.
109 pub fn remove_watch(&self, wd: WatchDescriptor) -> Result<(), Errno> {
110 // SAFETY: `fd` est le FD inotify possédé ; `inotify_rm_watch` ne touche
111 // aucune mémoire utilisateur (`wd` est un scalaire).
112 let ret = unsafe {
113 syscall2(
114 nr::INOTIFY_RM_WATCH,
115 fd_to_u64(self.fd.as_raw_fd()),
116 u64::from(wd.as_raw().cast_unsigned()),
117 )
118 };
119 if ret < 0 {
120 return Err(errno_from_negative_syscall_ret(ret));
121 }
122 Ok(())
123 }
124
125 /// Lit un lot d'événements **dans `buffer`** (fourni par l'appelant, zéro
126 /// allocation) et le décode. Le [`InotifyEvents`] retourné **emprunte**
127 /// `buffer` ; l'appelant l'itère puis peut consulter
128 /// [`InotifyEvents::truncated`] (ADR-032 : un événement coupé est signalé,
129 /// jamais avalé).
130 ///
131 /// # Errors
132 ///
133 /// - [`Errno::EAGAIN`] : FD [`InotifyFlags::NONBLOCK`] et aucun événement prêt.
134 /// - [`Errno::EINVAL`] : `buffer` trop petit pour le **prochain** événement
135 /// (l'appelant agrandit — recommandé : [`INOTIFY_RECOMMENDED_BUFFER_SIZE`]).
136 pub fn read_events<'b>(&self, buffer: &'b mut [u8]) -> Result<InotifyEvents<'b>, Errno> {
137 let count = read(self.as_fd(), buffer)?;
138 // Vue immutable de durée `'b` sur les `count` octets remplis par le kernel.
139 let filled: &'b [u8] = buffer;
140 Ok(InotifyEvents::parse(filled.get(..count).unwrap_or(filled)))
141 }
142}
143
144// ─────────────────────────────────────────────────────────────────────────
145// Tests — intégration kernel réel (inotify non privilégié) ⇒ exécutés sur les
146// exécuteurs ; `#[cfg_attr(miri, ignore)]` (syscalls non modélisés par Miri).
147// Le décodeur pur, le property-based et le fuzz vivent dans `air-sys-types::fs`.
148// ─────────────────────────────────────────────────────────────────────────
149#[cfg(test)]
150mod tests;