Skip to main content

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;