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 // SAFETY: `inotify_init1` a retourné un FD frais et possédé (CLOEXEC posé).
55 Ok(Inotify {
56 fd: unsafe { OwnedFd::from_raw_fd(fd) },
57 })
58}
59
60impl Inotify {
61 /// FD emprunté (intégration dans une boucle d'événements — `poll`/io_uring).
62 #[must_use]
63 pub fn as_fd(&self) -> BorrowedFd<'_> {
64 self.fd.as_fd()
65 }
66
67 /// Consomme l'instance et rend l'[`OwnedFd`] (transfert d'ownership).
68 #[must_use]
69 pub fn into_fd(self) -> OwnedFd {
70 self.fd
71 }
72
73 /// Ajoute (ou met à jour, avec [`InotifyEventMask::MASK_ADD`]) un watch sur
74 /// `path` pour les événements `mask`. `path` : `&CStr` (octets terminés NUL,
75 /// convention couche 0).
76 ///
77 /// # Errors
78 ///
79 /// - [`Errno::ENOSPC`] : limite `max_user_watches` atteinte.
80 /// - [`Errno::ENOENT`] : `path` n'existe pas.
81 /// - [`Errno::EACCES`] : accès en lecture refusé sur `path`.
82 /// - [`Errno::ENOTDIR`] : [`InotifyEventMask::ONLYDIR`] posé et `path` n'est
83 /// pas un répertoire.
84 pub fn add_watch(&self, path: &CStr, mask: InotifyEventMask) -> Result<WatchDescriptor, Errno> {
85 // SAFETY: `path` est un `CStr` terminé NUL, valide en lecture pour la
86 // durée de l'appel ; `fd` est le FD inotify possédé ; `inotify_add_watch`
87 // lit `path` (ne l'écrit pas) et `mask` est un scalaire.
88 let ret = unsafe {
89 syscall3(
90 nr::INOTIFY_ADD_WATCH,
91 fd_to_u64(self.fd.as_raw_fd()),
92 path.as_ptr() as u64,
93 u64::from(mask.bits()),
94 )
95 };
96 if ret < 0 {
97 return Err(errno_from_negative_syscall_ret(ret));
98 }
99 let wd = i32::try_from(ret).map_err(|_| Errno::EINVAL)?;
100 Ok(WatchDescriptor::from_raw(wd))
101 }
102
103 /// Retire le watch `wd`.
104 ///
105 /// # Errors
106 ///
107 /// - [`Errno::EINVAL`] : `wd` n'est pas un descripteur de watch valide.
108 pub fn remove_watch(&self, wd: WatchDescriptor) -> Result<(), Errno> {
109 // SAFETY: `fd` est le FD inotify possédé ; `inotify_rm_watch` ne touche
110 // aucune mémoire utilisateur (`wd` est un scalaire).
111 let ret = unsafe {
112 syscall2(
113 nr::INOTIFY_RM_WATCH,
114 fd_to_u64(self.fd.as_raw_fd()),
115 u64::from(wd.as_raw().cast_unsigned()),
116 )
117 };
118 if ret < 0 {
119 return Err(errno_from_negative_syscall_ret(ret));
120 }
121 Ok(())
122 }
123
124 /// Lit un lot d'événements **dans `buffer`** (fourni par l'appelant, zéro
125 /// allocation) et le décode. Le [`InotifyEvents`] retourné **emprunte**
126 /// `buffer` ; l'appelant l'itère puis peut consulter
127 /// [`InotifyEvents::truncated`] (ADR-032 : un événement coupé est signalé,
128 /// jamais avalé).
129 ///
130 /// # Errors
131 ///
132 /// - [`Errno::EAGAIN`] : FD [`InotifyFlags::NONBLOCK`] et aucun événement prêt.
133 /// - [`Errno::EINVAL`] : `buffer` trop petit pour le **prochain** événement
134 /// (l'appelant agrandit — recommandé : [`INOTIFY_RECOMMENDED_BUFFER_SIZE`]).
135 pub fn read_events<'b>(&self, buffer: &'b mut [u8]) -> Result<InotifyEvents<'b>, Errno> {
136 let count = read(self.as_fd(), buffer)?;
137 // Vue immutable de durée `'b` sur les `count` octets remplis par le kernel.
138 let filled: &'b [u8] = buffer;
139 Ok(InotifyEvents::parse(filled.get(..count).unwrap_or(filled)))
140 }
141}
142
143// ─────────────────────────────────────────────────────────────────────────
144// Tests — intégration kernel réel (inotify non privilégié) ⇒ exécutés sur les
145// exécuteurs ; `#[cfg_attr(miri, ignore)]` (syscalls non modélisés par Miri).
146// Le décodeur pur, le property-based et le fuzz vivent dans `air-sys-types::fs`.
147// ─────────────────────────────────────────────────────────────────────────
148#[cfg(test)]
149mod tests;