air_sys_types/fs.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//! Types de la famille `fs` — filesystem, ouverture, métadonnées, etc.
6//!
7//! Cf. `docs/specs/layer-0/family-fs.md`.
8
9use alloc::vec::Vec;
10
11use bitflags::bitflags;
12
13use crate::fd::BorrowedFd;
14
15// ─────────────────────────────────────────────────────────────────────────
16// Type de base : alias Mode
17// ─────────────────────────────────────────────────────────────────────────
18
19/// Permissions POSIX d'un fichier (bits `mode_t`).
20///
21/// Valeurs typiques : `0o644` (rw-r--r--), `0o755` (rwxr-xr-x),
22/// `0o600` (rw-------). Seuls les 12 bits inférieurs sont significatifs
23/// (9 bits de permission + 3 bits setuid/setgid/sticky).
24pub type Mode = u32;
25
26// ─────────────────────────────────────────────────────────────────────────
27// DirFd — base pour les opérations *at
28// ─────────────────────────────────────────────────────────────────────────
29
30/// Base d'un appel `*at` (cf. ADR-021 convention 1).
31///
32/// Remplace la sentinelle kernel `AT_FDCWD` (`-100`) par une variante
33/// `Cwd` typée. Air exige une base explicite sur toutes les opérations
34/// filesystem — pas de dépendance implicite au `cwd` global du processus.
35///
36/// # Examples
37///
38/// ```no_run
39/// use air_sys_types::fs::DirFd;
40/// // Ouvrir un fichier par rapport au répertoire courant
41/// let _dirfd = DirFd::Cwd;
42/// ```
43#[derive(Debug, Clone, Copy)]
44pub enum DirFd<'fd> {
45 /// Répertoire courant du processus (équivalent à `AT_FDCWD`).
46 Cwd,
47 /// FD d'un répertoire ouvert (accès relatif à ce répertoire).
48 Fd(BorrowedFd<'fd>),
49}
50
51// ─────────────────────────────────────────────────────────────────────────
52// openat2 / openat
53// ─────────────────────────────────────────────────────────────────────────
54
55bitflags! {
56 /// Flags d'ouverture de fichier pour `openat2(2)` / `openat(2)`.
57 ///
58 /// Correspond aux constantes `O_*` de `<fcntl.h>`.
59 /// `CLOEXEC` est ajouté **automatiquement** par le wrapper — l'appelant
60 /// ne doit pas l'insérer manuellement.
61 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
62 pub struct OpenFlags: u64 {
63 /// Lecture seule.
64 const RDONLY = 0;
65 /// Écriture seule.
66 const WRONLY = 1;
67 /// Lecture et écriture.
68 const RDWR = 2;
69 /// Crée le fichier s'il n'existe pas.
70 const CREAT = 0o0000_0100;
71 /// Échec si le fichier existe déjà (avec `CREAT`).
72 const EXCL = 0o0000_0200;
73 /// Ne prend pas le terminal comme terminal de contrôle.
74 const NOCTTY = 0o0000_0400;
75 /// Tronque le fichier existant à zéro.
76 const TRUNC = 0o0000_1000;
77 /// Écriture en mode ajout.
78 const APPEND = 0o0000_2000;
79 /// Non-bloquant.
80 const NONBLOCK = 0o0000_4000;
81 /// Sync à chaque écriture sur les données et les métadonnées.
82 const DSYNC = 0o0001_0000;
83 /// Mode asynchrone POSIX (SIGIO).
84 const ASYNC = 0o0002_0000;
85 /// Accès direct (pas de cache kernel).
86 ///
87 /// **Valeur dépendante de l'architecture** (ABI `asm-generic` sur
88 /// aarch64 vs `x86`) : `0o40000` sur x86_64, `0o200000` sur aarch64.
89 const DIRECT = if cfg!(target_arch = "x86_64") { 0o0004_0000 } else { 0o0020_0000 };
90 /// Fichier potentiellement large (obsolète en 64 bits).
91 ///
92 /// **Valeur dépendante de l'architecture** : `0o100000` sur x86_64,
93 /// `0o400000` sur aarch64.
94 const LARGEFILE = if cfg!(target_arch = "x86_64") { 0o0010_0000 } else { 0o0040_0000 };
95 /// Doit être un répertoire.
96 ///
97 /// **Valeur dépendante de l'architecture** : `0o200000` sur x86_64,
98 /// `0o40000` sur aarch64. (Une valeur figée x86 cassait `openat` avec
99 /// `EINVAL` sur aarch64 — elle y désignait en réalité `O_DIRECT`.)
100 const DIRECTORY = if cfg!(target_arch = "x86_64") { 0o0020_0000 } else { 0o0004_0000 };
101 /// Ne suit pas les liens symboliques.
102 ///
103 /// **Valeur dépendante de l'architecture** : `0o400000` sur x86_64,
104 /// `0o100000` sur aarch64.
105 const NOFOLLOW = if cfg!(target_arch = "x86_64") { 0o0040_0000 } else { 0o0010_0000 };
106 /// Pas de mise à jour de `atime`.
107 const NOATIME = 0o0100_0000;
108 /// Ferme le FD à l'`exec` (ajouté automatiquement par le wrapper).
109 const CLOEXEC = 0o0200_0000;
110 /// Ouvre sans accéder à l'objet (FD de chemin pur).
111 const PATH = 0o1000_0000;
112 /// Crée un fichier temporaire sans nom dans le répertoire.
113 ///
114 /// `__O_TMPFILE | O_DIRECTORY` : hérite donc de la divergence d'arch
115 /// d'`O_DIRECTORY` — `0o20200000` sur x86_64, `0o20040000` sur aarch64.
116 const TMPFILE = if cfg!(target_arch = "x86_64") { 0o2020_0000 } else { 0o2004_0000 };
117 }
118}
119
120bitflags! {
121 /// Flags de `close_range(2)` (Linux 5.9+) — fermeture en masse d'un intervalle
122 /// de descripteurs. Utilisé par le confinement (ADR-122) : un enfant forké ferme
123 /// tous les fd hérités hormis un keep-set avant de poser sa cage.
124 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
125 pub struct CloseRangeFlags: u32 {
126 /// `CLOSE_RANGE_UNSHARE` — détache la table de fd partagée avant la
127 /// fermeture (n'affecte que ce processus, pas les partageurs de table).
128 const UNSHARE = 0x0000_0002;
129 /// `CLOSE_RANGE_CLOEXEC` — positionne `O_CLOEXEC` sur l'intervalle **au lieu**
130 /// de le fermer (fermeture différée à l'`execve`).
131 const CLOEXEC = 0x0000_0004;
132 }
133}
134
135bitflags! {
136 /// Flags de résolution de chemin pour `openat2(2)` (Linux 5.6+).
137 ///
138 /// Ces flags permettent une sandbox de résolution de chemin stricte.
139 /// `BENEATH` et `IN_ROOT` sont particulièrement utiles pour les
140 /// contextes sandboxés (launcher Air, couche 5).
141 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
142 pub struct ResolveFlags: u64 {
143 /// Refuse de traverser les frontières de système de fichiers.
144 const NO_XDEV = 0x01;
145 /// Refuse les liens magiques (ex. `/proc/self/fd/X`).
146 const NO_MAGICLINKS = 0x02;
147 /// Refuse tous les liens symboliques.
148 const NO_SYMLINKS = 0x04;
149 /// La résolution ne peut pas remonter au-delà de `dirfd`
150 /// (protection contre `../../`).
151 const BENEATH = 0x08;
152 /// Traite `dirfd` comme la racine de la résolution.
153 const IN_ROOT = 0x10;
154 /// Utilise uniquement le cache VFS (pas d'E/S si absent).
155 const CACHED = 0x20;
156 }
157}
158
159/// Structure d'ouverture pour `openat2(2)`.
160///
161/// Combine flags, mode et flags de résolution en un seul argument.
162/// `how.flags` inclut automatiquement `CLOEXEC` via le wrapper.
163#[derive(Debug, Clone, Copy, Default)]
164pub struct OpenHow {
165 /// Flags d'accès et de comportement.
166 pub flags: OpenFlags,
167 /// Permissions de création (ignoré si `CREAT` absent).
168 pub mode: Mode,
169 /// Politique de résolution de chemin (Linux 5.6+).
170 pub resolve: ResolveFlags,
171}
172
173// ─────────────────────────────────────────────────────────────────────────
174// statx
175// ─────────────────────────────────────────────────────────────────────────
176
177bitflags! {
178 /// Flags pour `statx(2)` (comportement de résolution de chemin).
179 ///
180 /// Cf. `linux/stat.h` : constantes `AT_*`.
181 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
182 pub struct StatxFlags: u32 {
183 /// Opère sur le FD lui-même sans résolution de chemin
184 /// (équivalent à `AT_EMPTY_PATH`).
185 const EMPTY_PATH = 0x1000;
186 /// Ne déclenche pas de montage automount.
187 const NO_AUTOMOUNT = 0x0800;
188 /// Ne suit pas les liens symboliques.
189 const SYMLINK_NOFOLLOW = 0x0100;
190 }
191}
192
193bitflags! {
194 /// Masque des champs demandés / disponibles dans [`StatxResult`].
195 ///
196 /// Passé en entrée pour sélectionner les champs souhaités ;
197 /// retourné dans `StatxResult::mask` pour indiquer quels champs ont
198 /// été effectivement remplis par le kernel.
199 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
200 pub struct StatxMask: u32 {
201 /// Type de fichier (regular, dir, symlink…).
202 const TYPE = 0x0001;
203 /// Bits de permissions (`mode & 0o7777`).
204 const MODE = 0x0002;
205 /// Nombre de liens physiques.
206 const NLINK = 0x0004;
207 /// UID du propriétaire.
208 const UID = 0x0008;
209 /// GID du propriétaire.
210 const GID = 0x0010;
211 /// Horodatage d'accès (`atime`).
212 const ATIME = 0x0020;
213 /// Horodatage de modification de données (`mtime`).
214 const MTIME = 0x0040;
215 /// Horodatage de modification de métadonnées (`ctime`).
216 const CTIME = 0x0080;
217 /// Numéro d'inode.
218 const INO = 0x0100;
219 /// Taille en octets.
220 const SIZE = 0x0200;
221 /// Nombre de blocs de 512 octets alloués.
222 const BLOCKS = 0x0400;
223 /// Horodatage de création (`btime`, si supporté par le FS).
224 const BTIME = 0x0800;
225 /// Identifiant de point de montage (Linux 5.8+).
226 const MNT_ID = 0x1000;
227 /// Tous les champs courants.
228 const ALL = 0x0fff;
229 }
230}
231
232/// Horodatage retourné par `statx(2)`.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct StatxTimestamp {
235 /// Secondes depuis l'epoch Unix.
236 pub seconds: i64,
237 /// Nanosecondes (complément, toujours dans `[0, 999_999_999]`).
238 pub nanoseconds: u32,
239}
240
241/// Résultat de `statx(2)`.
242///
243/// Les champs non demandés (absent de `mask` en entrée) ou non disponibles
244/// (absent de `mask` en sortie) ont des valeurs indéfinies. Vérifiez
245/// toujours `mask.contains(StatxMask::XYZ)` avant de lire un champ.
246#[derive(Debug, Clone)]
247pub struct StatxResult {
248 /// Champs effectivement remplis par le kernel.
249 pub mask: StatxMask,
250 /// Granularité des E/S préférée (en octets).
251 pub blksize: u32,
252 /// Attributs supplémentaires (compressed, immutable, append-only…).
253 pub attributes: u64,
254 /// Nombre de liens physiques.
255 pub nlink: u32,
256 /// UID du propriétaire.
257 pub uid: u32,
258 /// GID du propriétaire.
259 pub gid: u32,
260 /// Type et permissions (`type << 12 | perm`).
261 pub mode: u16,
262 /// Numéro d'inode.
263 pub ino: u64,
264 /// Taille en octets.
265 pub size: u64,
266 /// Nombre de blocs de 512 octets alloués.
267 pub blocks: u64,
268 /// Horodatage d'accès.
269 pub atime: StatxTimestamp,
270 /// Horodatage de création.
271 pub btime: StatxTimestamp,
272 /// Horodatage de modification de métadonnées.
273 pub ctime: StatxTimestamp,
274 /// Horodatage de modification de données.
275 pub mtime: StatxTimestamp,
276 /// Numéro majeur du device (si fichier device).
277 pub rdev_major: u32,
278 /// Numéro mineur du device (si fichier device).
279 pub rdev_minor: u32,
280 /// Numéro majeur du device contenant le fichier.
281 pub dev_major: u32,
282 /// Numéro mineur du device contenant le fichier.
283 pub dev_minor: u32,
284 /// Identifiant du point de montage (Linux 5.8+, 0 si non disponible).
285 pub mount_id: u64,
286}
287
288// ─────────────────────────────────────────────────────────────────────────
289// faccessat
290// ─────────────────────────────────────────────────────────────────────────
291
292bitflags! {
293 /// Mode d'accès pour `faccessat(2)`.
294 ///
295 /// Cf. `linux/unistd.h` : constantes `F_OK`, `R_OK`, `W_OK`, `X_OK`.
296 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
297 pub struct AccessMode: u32 {
298 /// Vérifie l'existence du fichier.
299 const F_OK = 0;
300 /// Vérifie les droits de lecture.
301 const R_OK = 4;
302 /// Vérifie les droits d'écriture.
303 const W_OK = 2;
304 /// Vérifie les droits d'exécution.
305 const X_OK = 1;
306 }
307}
308
309bitflags! {
310 /// Flags comportementaux pour `faccessat(2)`.
311 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
312 pub struct AccessFlags: i32 {
313 /// Utilise l'UID/GID effectif (plutôt que réel).
314 const EACCESS = 0x200;
315 /// Ne suit pas les liens symboliques.
316 const SYMLINK_NOFOLLOW = 0x100;
317 }
318}
319
320// ─────────────────────────────────────────────────────────────────────────
321// Répertoires
322// ─────────────────────────────────────────────────────────────────────────
323
324/// Type d'entrée de répertoire retourné par `getdents64(2)`.
325///
326/// Correspond au champ `d_type` de la struct `linux_dirent64`.
327/// La valeur `Unknown` (0) est produite sur les filesystems qui ne
328/// stockent pas le type (ex. certains systèmes de fichiers en réseau).
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330#[repr(u8)]
331pub enum DirEntryType {
332 /// Type inconnu (filesystem ne stocke pas le type).
333 Unknown = 0,
334 /// FIFO (pipe nommé).
335 Fifo = 1,
336 /// Fichier caractère.
337 Character = 2,
338 /// Répertoire.
339 Directory = 4,
340 /// Fichier bloc.
341 Block = 6,
342 /// Fichier régulier.
343 Regular = 8,
344 /// Lien symbolique.
345 Symlink = 10,
346 /// Socket Unix.
347 Socket = 12,
348}
349
350/// Entrée de répertoire parsée depuis le buffer de `getdents64(2)`.
351///
352/// La durée de vie `'buffer` est liée au buffer de lecture — les données
353/// ne sont pas copiées.
354#[derive(Debug, Clone)]
355pub struct DirEntry {
356 /// Numéro d'inode.
357 pub inode: u64,
358 /// Offset de la prochaine entrée dans le flux de répertoire.
359 pub offset: i64,
360 /// Type de l'entrée.
361 pub file_type: DirEntryType,
362 /// Nom de l'entrée (sans le chemin parent), **octets bruts** propriétaires.
363 ///
364 /// Sur Unix un nom de fichier est une suite d'octets opaque (qui peut ne
365 /// pas être de l'UTF-8 valide), terminée par `NUL` dans le buffer kernel
366 /// mais stockée ici **sans** le `NUL`. Le type est volontairement
367 /// [`alloc::vec::Vec<u8>`] (octets) et non `OsString`/`String` : la couche 0
368 /// ne présume aucun encodage (Principe 3, doctrine « chemins = octets »).
369 pub name: alloc::vec::Vec<u8>,
370}
371
372bitflags! {
373 /// Flags pour `renameat2(2)` (cf. `linux/fs.h`).
374 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
375 pub struct RenameFlags: u32 {
376 /// Ne remplace pas la destination si elle existe.
377 const NOREPLACE = 1;
378 /// Échange atomiquement source et destination.
379 const EXCHANGE = 2;
380 /// Pose un whiteout sur la source (union mounts).
381 const WHITEOUT = 4;
382 }
383}
384
385// ─────────────────────────────────────────────────────────────────────────
386// lseek
387// ─────────────────────────────────────────────────────────────────────────
388
389/// Origine pour `lseek(2)`.
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391#[repr(i32)]
392pub enum SeekWhence {
393 /// Depuis le début du fichier.
394 Set = 0,
395 /// Depuis la position courante.
396 Current = 1,
397 /// Depuis la fin du fichier.
398 End = 2,
399 /// Depuis la prochaine zone de données après l'offset (Linux).
400 Data = 3,
401 /// Depuis le prochain trou après l'offset (Linux).
402 Hole = 4,
403}
404
405// ─────────────────────────────────────────────────────────────────────────
406// utimensat
407// ─────────────────────────────────────────────────────────────────────────
408
409/// Valeur d'horodatage pour `utimensat(2)`.
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411pub enum UtimeValue {
412 /// Met l'horodatage à l'heure courante (`UTIME_NOW`).
413 Now,
414 /// Ne modifie pas cet horodatage (`UTIME_OMIT`).
415 Omit,
416 /// Met l'horodatage à la valeur fournie.
417 Time(StatxTimestamp),
418}
419
420// ─────────────────────────────────────────────────────────────────────────
421// fallocate
422// ─────────────────────────────────────────────────────────────────────────
423
424bitflags! {
425 /// Mode pour `fallocate(2)` (cf. `linux/falloc.h`).
426 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
427 pub struct FallocateMode: i32 {
428 /// Garde la taille du fichier inchangée (alloue de l'espace sans étendre).
429 const KEEP_SIZE = 0x01;
430 /// Perfore un trou dans le fichier (libère les blocs, laisse un trou).
431 const PUNCH_HOLE = 0x02;
432 /// Collapse une plage du fichier en supprimant les blocs.
433 const COLLAPSE_RANGE = 0x08;
434 /// Remplace une plage par des zéros sans désallouer les blocs.
435 const ZERO_RANGE = 0x10;
436 /// Insère de l'espace dans le fichier (décale le contenu).
437 const INSERT_RANGE = 0x20;
438 /// Dé-partage les blocs copy-on-write.
439 const UNSHARE_RANGE = 0x40;
440 }
441}
442
443// ─────────────────────────────────────────────────────────────────────────
444// fcntl individuel
445// ─────────────────────────────────────────────────────────────────────────
446
447bitflags! {
448 /// Flags de descripteur de fichier (`F_GETFD`/`F_SETFD`).
449 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
450 pub struct FdFlags: i32 {
451 /// Ferme le FD à l'`exec`.
452 const CLOEXEC = 1;
453 }
454}
455
456bitflags! {
457 /// Flags de statut de fichier (`F_GETFL`/`F_SETFL`).
458 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
459 pub struct StatusFlags: i32 {
460 /// Mode non-bloquant.
461 const NONBLOCK = 0x0000_0800;
462 /// Mode append.
463 const APPEND = 0x0000_0400;
464 /// Sync asynchrone.
465 const ASYNC = 0x0002_0000;
466 /// Sync synchrone sur les données.
467 const DSYNC = 0x0000_1000;
468 /// Accès direct (no cache).
469 ///
470 /// **Valeur dépendante de l'architecture** (`O_DIRECT`) : `0x4000` sur
471 /// x86_64, `0x10000` sur aarch64.
472 const DIRECT = if cfg!(target_arch = "x86_64") { 0x0000_4000 } else { 0x0001_0000 };
473 }
474}
475
476/// Type de verrou pour `flock` noyau (opérations `F_SETLK`/`F_GETLK`).
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
478#[repr(i16)]
479pub enum LockType {
480 /// Verrou de lecture (partagé).
481 Read = 0,
482 /// Verrou d'écriture (exclusif).
483 Write = 1,
484 /// Libère le verrou.
485 Unlock = 2,
486}
487
488/// Verrou de fichier POSIX (pour `fcntl F_SETLK`/`F_SETLKW`/`F_GETLK`).
489///
490/// Correspond à `struct flock` de `<fcntl.h>`.
491#[derive(Debug, Clone, Copy)]
492pub struct FileLock {
493 /// Type du verrou.
494 pub lock_type: LockType,
495 /// Origine pour `start` (comme `SeekWhence`, `SeekWhence::Set` = 0).
496 pub whence: SeekWhence,
497 /// Offset de début (relatif à `whence`).
498 pub start: i64,
499 /// Longueur verrouillée (0 = jusqu'à la fin du fichier).
500 pub length: i64,
501 /// PID du processus détenant le verrou (rempli par `F_GETLK`).
502 pub pid: i32,
503}
504
505bitflags! {
506 /// Scellements d'un `memfd` (cf. `linux/fcntl.h` : `F_SEAL_*`).
507 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
508 pub struct Seals: u32 {
509 /// Interdit d'ajouter de nouveaux scellements.
510 const SEAL = 1;
511 /// Interdit de réduire la taille du fichier.
512 const SHRINK = 2;
513 /// Interdit d'augmenter la taille du fichier.
514 const GROW = 4;
515 /// Interdit toute écriture dans le fichier.
516 const WRITE = 8;
517 /// Interdit les écritures futures même via des mappings existants.
518 const FUTURE_WRITE = 16;
519 /// Interdit la création de nouveaux mappings exécutables.
520 const EXEC = 32;
521 }
522}
523
524// ─────────────────────────────────────────────────────────────────────────
525// name_to_handle_at / open_by_handle_at
526// ─────────────────────────────────────────────────────────────────────────
527
528bitflags! {
529 /// Flags pour `name_to_handle_at(2)`.
530 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
531 pub struct NameToHandleFlags: i32 {
532 /// Ne suit pas les liens symboliques.
533 const SYMLINK_NOFOLLOW = 0x100;
534 /// Opère sur le FD lui-même (équivalent à `AT_EMPTY_PATH`).
535 const EMPTY_PATH = 0x1000;
536 }
537}
538
539/// Handle de fichier persistant retourné par `name_to_handle_at(2)`.
540///
541/// Permet de référencer un fichier même après un changement de chemin.
542/// Opaque : la taille et le contenu de `handle_bytes` dépendent du
543/// filesystem.
544///
545/// **Allocation heap :** la taille du handle est dynamique et déterminée
546/// par le kernel au premier appel. Nécessité documentée (ADR-021
547/// convention 4).
548#[derive(Debug, Clone)]
549pub struct FileHandle {
550 /// Identifiant de type de handle (opaque, défini par le filesystem).
551 pub handle_type: i32,
552 /// Octets du handle (taille et contenu définis par le filesystem).
553 pub handle_bytes: Vec<u8>,
554 /// Identifiant de point de montage (retourné par `name_to_handle_at`).
555 pub mount_id: i32,
556}
557
558// ─────────────────────────────────────────────────────────────────────────
559// statfs / fstatfs
560// ─────────────────────────────────────────────────────────────────────────
561
562/// Type de filesystem (champ `f_type` de `statfs(2)`).
563///
564/// Enveloppe la valeur magique retournée par le kernel identifiant le type de FS.
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
566pub struct FsType(pub i64);
567
568impl FsType {
569 /// ext4 (0xEF53)
570 pub const EXT4: Self = Self(0xEF53);
571 /// tmpfs (0x0102_1994)
572 pub const TMPFS: Self = Self(0x0102_1994);
573 /// proc (0x9FA0)
574 pub const PROC: Self = Self(0x9FA0);
575 /// sysfs (0x6265_6572)
576 pub const SYSFS: Self = Self(0x6265_6572);
577 /// btrfs (0x9123_683E)
578 pub const BTRFS: Self = Self(0x9123_683E);
579}
580
581/// Résultat de `statfs(2)` / `fstatfs(2)` — informations sur le système de fichiers.
582///
583/// Mappé depuis la struct kernel `statfs64` de manière indépendante de l'architecture.
584/// Le wrapper Air n'expose jamais le layout kernel brut.
585#[derive(Debug, Clone, Copy)]
586pub struct StatFsResult {
587 /// Type du système de fichiers (voir [`FsType`]).
588 pub f_type: FsType,
589 /// Taille optimale des blocs de transfert (octets).
590 pub f_bsize: i64,
591 /// Taille de fragment (blocs FS fondamentaux, octets).
592 pub f_frsize: i64,
593 /// Nombre total de blocs de données.
594 pub f_blocks: u64,
595 /// Nombre de blocs libres.
596 pub f_bfree: u64,
597 /// Nombre de blocs disponibles pour les non-privilégiés.
598 pub f_bavail: u64,
599 /// Nombre total d'inodes.
600 pub f_files: u64,
601 /// Nombre d'inodes libres.
602 pub f_ffree: u64,
603 /// Identifiant du système de fichiers (opaque).
604 pub f_fsid: [u32; 2],
605 /// Longueur maximale d'un nom de fichier.
606 pub f_namelen: i64,
607 /// Drapeaux du FS (ex. `ST_RDONLY`, `ST_NOEXEC`).
608 pub f_flags: i64,
609}
610
611// ─────────────────────────────────────────────────────────────────────────
612// Types partagés ajoutés pour io_uring Temps 2a (cf.
613// docs/specs/layer-0/io-uring-2a-filesystem.md). Ces types vivent dans la
614// famille `fs` car ils décrivent la sémantique des opérations filesystem,
615// qu'elles soient exposées de façon synchrone (`air-sys-syscall::fs`) ou
616// asynchrone (`air-sys-syscall::io_uring`). Valeurs : uapi Linux 6.12.
617// ─────────────────────────────────────────────────────────────────────────
618
619bitflags! {
620 /// Drapeaux de `fsync` via io_uring (`IORING_FSYNC_*`).
621 ///
622 /// `DATASYNC` sélectionne la sémantique `fdatasync` (ne synchronise pas les
623 /// métadonnées non essentielles à la relecture des données).
624 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
625 pub struct FsyncFlags: u32 {
626 /// `IORING_FSYNC_DATASYNC` : sémantique `fdatasync`.
627 const DATASYNC = 1 << 0;
628 }
629}
630
631bitflags! {
632 /// Drapeaux de `sync_file_range(2)` (cf. `linux/fs.h` : `SYNC_FILE_RANGE_*`).
633 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
634 pub struct SyncFileRangeFlags: u32 {
635 /// Attend l'écriture des pages déjà en cours avant de lancer la plage.
636 const WAIT_BEFORE = 1;
637 /// Lance l'écriture des pages sales de la plage.
638 const WRITE = 2;
639 /// Attend l'écriture des pages de la plage après les avoir lancées.
640 const WAIT_AFTER = 4;
641 }
642}
643
644bitflags! {
645 /// Drapeaux de changement de permissions pour `fchmodat2(2)` (`AT_*`).
646 ///
647 /// N'a de sens **qu'avec** `fchmodat2` (4 arguments, Linux ≥ 6.6) : le syscall
648 /// historique `fchmodat` n'en prend que 3 et ne peut donc porter aucun drapeau.
649 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
650 pub struct ChmodFlags: i32 {
651 /// `AT_SYMLINK_NOFOLLOW` : opère sur le **lien** lui-même plutôt que sur sa
652 /// cible. Un lien symbolique n'ayant pas de permissions modifiables sous
653 /// Linux, l'appel échoue alors en `EOPNOTSUPP` — ce qui en fait une garantie
654 /// utilisable : « ce chemin n'était pas un lien » (anti-TOCTOU).
655 const SYMLINK_NOFOLLOW = 0x100;
656 }
657}
658
659bitflags! {
660 /// Drapeaux de suppression pour `unlinkat(2)` (`AT_*`).
661 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
662 pub struct UnlinkFlags: i32 {
663 /// `AT_REMOVEDIR` : supprime un répertoire (sémantique `rmdir`).
664 const REMOVEDIR = 0x200;
665 }
666}
667
668bitflags! {
669 /// Drapeaux de lien pour `linkat(2)` (`AT_*`).
670 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
671 pub struct LinkFlags: i32 {
672 /// `AT_SYMLINK_FOLLOW` : suit la cible si l'ancien chemin est un lien.
673 const SYMLINK_FOLLOW = 0x400;
674 /// `AT_EMPTY_PATH` : lie le FD lui-même (chemin vide).
675 const EMPTY_PATH = 0x1000;
676 }
677}
678
679bitflags! {
680 /// Drapeaux de création des attributs étendus (`setxattr(2)` : `XATTR_*`).
681 ///
682 /// Mutuellement exclusifs ; sans drapeau, l'attribut est créé ou remplacé.
683 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
684 pub struct XattrFlags: i32 {
685 /// `XATTR_CREATE` : échoue (`EEXIST`) si l'attribut existe déjà.
686 const CREATE = 1;
687 /// `XATTR_REPLACE` : échoue (`ENODATA`) si l'attribut n'existe pas.
688 const REPLACE = 2;
689 }
690}
691
692/// Conseil d'accès pour `posix_fadvise(2)` (`POSIX_FADV_*`).
693///
694/// Distinct de [`crate::mem::MadviseAdvice`] : l'espace de valeurs de
695/// `posix_fadvise` (fichier) n'est pas celui de `madvise` (mémoire), même si
696/// la spec io_uring 2a les nomme tous deux « advice ». Sur les cibles tier-1
697/// d'Air (x86_64/aarch64), les constantes valent 0..=5.
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
699#[repr(i32)]
700pub enum FadviseAdvice {
701 /// `POSIX_FADV_NORMAL` : aucun conseil particulier.
702 Normal = 0,
703 /// `POSIX_FADV_RANDOM` : accès aléatoire (désactive le préfetch).
704 Random = 1,
705 /// `POSIX_FADV_SEQUENTIAL` : accès séquentiel (préfetch agressif).
706 Sequential = 2,
707 /// `POSIX_FADV_WILLNEED` : données bientôt nécessaires (préfetch).
708 WillNeed = 3,
709 /// `POSIX_FADV_DONTNEED` : données plus nécessaires (libère le cache).
710 DontNeed = 4,
711 /// `POSIX_FADV_NOREUSE` : données accédées une seule fois.
712 NoReuse = 5,
713}
714
715impl FadviseAdvice {
716 /// Valeur kernel (`POSIX_FADV_*`) du conseil.
717 #[must_use]
718 pub const fn as_raw(self) -> i32 {
719 self as i32
720 }
721}
722
723/// Horodatage brut du kernel (`struct statx_timestamp`, 16 octets).
724///
725/// Miroir `#[repr(C)]` écrit par le kernel ; cf. [`Statx`].
726#[repr(C)]
727#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
728pub struct StatxTimestampRaw {
729 /// Secondes depuis l'epoch Unix (`tv_sec`).
730 pub tv_sec: i64,
731 /// Nanosecondes (`tv_nsec`, dans `[0, 999_999_999]`).
732 pub tv_nsec: u32,
733 /// Réservé kernel (toujours 0).
734 reserved: i32,
735}
736
737/// Miroir brut `#[repr(C)]` de `struct statx` (256 octets, cf. `linux/stat.h`).
738///
739/// Contrairement à [`StatxResult`] (forme décodée, indépendante de l'ABI), ce
740/// type est le **tampon de sortie écrit directement par le kernel** :
741/// `io_uring`'s `IORING_OP_STATX` y dépose les métadonnées de façon asynchrone.
742/// Les champs portent les noms kernel (zone d'interface, ADR-029). Construire
743/// via [`Statx::default`] (zéro-initialisé) avant de le confier au ring.
744#[repr(C)]
745#[derive(Debug, Clone, Default)]
746pub struct Statx {
747 /// Champs effectivement remplis (`stx_mask`).
748 pub mask: u32,
749 /// Granularité d'E/S préférée (`stx_blksize`).
750 pub blksize: u32,
751 /// Attributs supplémentaires (`stx_attributes`).
752 pub attributes: u64,
753 /// Nombre de liens physiques (`stx_nlink`).
754 pub nlink: u32,
755 /// UID du propriétaire (`stx_uid`).
756 pub uid: u32,
757 /// GID du propriétaire (`stx_gid`).
758 pub gid: u32,
759 /// Type et permissions (`stx_mode`).
760 pub mode: u16,
761 spare0: [u16; 1],
762 /// Numéro d'inode (`stx_ino`).
763 pub ino: u64,
764 /// Taille en octets (`stx_size`).
765 pub size: u64,
766 /// Blocs de 512 octets alloués (`stx_blocks`).
767 pub blocks: u64,
768 /// Masque d'attributs supportés (`stx_attributes_mask`).
769 pub attributes_mask: u64,
770 /// Horodatage d'accès (`stx_atime`).
771 pub atime: StatxTimestampRaw,
772 /// Horodatage de création (`stx_btime`).
773 pub btime: StatxTimestampRaw,
774 /// Horodatage de modification de métadonnées (`stx_ctime`).
775 pub ctime: StatxTimestampRaw,
776 /// Horodatage de modification de données (`stx_mtime`).
777 pub mtime: StatxTimestampRaw,
778 /// Numéro majeur du device représenté (`stx_rdev_major`).
779 pub rdev_major: u32,
780 /// Numéro mineur du device représenté (`stx_rdev_minor`).
781 pub rdev_minor: u32,
782 /// Numéro majeur du device contenant (`stx_dev_major`).
783 pub dev_major: u32,
784 /// Numéro mineur du device contenant (`stx_dev_minor`).
785 pub dev_minor: u32,
786 /// Identifiant de point de montage (`stx_mnt_id`).
787 pub mnt_id: u64,
788 spare2: u64,
789 spare3: [u64; 12],
790}
791
792impl Statx {
793 /// Vrai si le bit de [`StatxMask`] `field` est présent dans `mask`.
794 #[must_use]
795 pub fn has(&self, field: StatxMask) -> bool {
796 self.mask & field.bits() == field.bits()
797 }
798}
799
800// Vérifications de layout ABI (figées par le kernel) : une dérive ferait
801// échouer la compilation avant tout appel kernel.
802const _: () = {
803 assert!(core::mem::size_of::<StatxTimestampRaw>() == 16);
804 assert!(core::mem::size_of::<Statx>() == 256);
805 assert!(core::mem::align_of::<Statx>() == 8);
806};
807
808// ─────────────────────────────────────────────────────────────────────────
809// inotify — surveillance de fichiers (sous-module `fs::inotify`)
810// Cf. `docs/specs/layer-0/family-fs-inotify.md`.
811// ─────────────────────────────────────────────────────────────────────────
812
813/// Descripteur de surveillance (retour d'`inotify_add_watch`). Newtype typé
814/// (jamais un `i32` brut) — ADR-029. Aussi le champ `wd` des événements lus.
815#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
816pub struct WatchDescriptor(i32);
817
818impl WatchDescriptor {
819 /// Construit depuis le `wd` brut rendu par `inotify_add_watch` (≥ 0) ou lu
820 /// dans un événement. Usage interne aux wrappers/au décodeur.
821 #[must_use]
822 pub const fn from_raw(wd: i32) -> Self {
823 Self(wd)
824 }
825
826 /// Valeur brute du descripteur (à passer à `inotify_rm_watch`).
827 #[must_use]
828 pub const fn as_raw(self) -> i32 {
829 self.0
830 }
831}
832
833bitflags! {
834 /// Masque d'événements / d'options inotify (`IN_*`). Noms kernel conservés
835 /// (ADR-029). Sert à la fois en **entrée** (`add_watch`) et en **sortie**
836 /// (bits positionnés par le kernel dans l'événement lu).
837 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
838 pub struct InotifyEventMask: u32 {
839 /// Fichier accédé (lecture).
840 const ACCESS = 0x0000_0001;
841 /// Fichier modifié (écriture).
842 const MODIFY = 0x0000_0002;
843 /// Métadonnées changées (permissions, horodatage…).
844 const ATTRIB = 0x0000_0004;
845 /// Fichier ouvert en écriture fermé.
846 const CLOSE_WRITE = 0x0000_0008;
847 /// Fichier ouvert en lecture seule fermé.
848 const CLOSE_NOWRITE = 0x0000_0010;
849 /// Fichier ouvert.
850 const OPEN = 0x0000_0020;
851 /// Fichier déplacé **hors** du répertoire surveillé (corrèle par `cookie`).
852 const MOVED_FROM = 0x0000_0040;
853 /// Fichier déplacé **dans** le répertoire surveillé (corrèle par `cookie`).
854 const MOVED_TO = 0x0000_0080;
855 /// Fichier/répertoire créé.
856 const CREATE = 0x0000_0100;
857 /// Fichier/répertoire supprimé.
858 const DELETE = 0x0000_0200;
859 /// Le répertoire/fichier surveillé lui-même a été supprimé.
860 const DELETE_SELF = 0x0000_0400;
861 /// Le répertoire/fichier surveillé lui-même a été déplacé.
862 const MOVE_SELF = 0x0000_0800;
863 /// Le système de fichiers contenant l'objet surveillé a été démonté
864 /// (positionné par le kernel).
865 const UNMOUNT = 0x0000_2000;
866 /// La file d'événements a débordé (positionné par le kernel ; `wd = -1`).
867 const Q_OVERFLOW = 0x0000_4000;
868 /// Le watch a été retiré (explicitement, ou objet supprimé/démonté).
869 const IGNORED = 0x0000_8000;
870 /// L'objet de l'événement est un répertoire (positionné par le kernel).
871 const ISDIR = 0x4000_0000;
872 /// Option : ne surveiller que si `path` est un répertoire.
873 const ONLYDIR = 0x0100_0000;
874 /// Option : ne pas déréférencer `path` s'il est un lien symbolique.
875 const DONT_FOLLOW = 0x0200_0000;
876 /// Option : exclure les événements des fichiers déjà unlinkés.
877 const EXCL_UNLINK = 0x0400_0000;
878 /// Option : ajouter à (au lieu de remplacer) le masque d'un watch existant.
879 const MASK_ADD = 0x2000_0000;
880 /// Option : surveiller une seule fois puis retirer le watch.
881 const ONESHOT = 0x8000_0000;
882 }
883}
884
885bitflags! {
886 /// Flags de création de l'instance inotify (`inotify_init1`). `CLOEXEC` est
887 /// posé **par défaut** par le wrapper ([`inotify_init`]).
888 ///
889 /// [`inotify_init`]: ../../../air_sys_syscall/fs/inotify/fn.inotify_init.html
890 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
891 pub struct InotifyFlags: i32 {
892 /// `IN_NONBLOCK` (= `O_NONBLOCK`) : FD non bloquant (`read` → `EAGAIN`).
893 const NONBLOCK = 0x800;
894 /// `IN_CLOEXEC` (= `O_CLOEXEC`) : posé par défaut par le wrapper.
895 const CLOEXEC = 0x8_0000;
896 }
897}
898
899/// Un événement inotify décodé, **empruntant** le buffer de lecture (zéro copie).
900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901pub struct InotifyEvent<'b> {
902 /// Watch d'où provient l'événement (`-1` typé pour `Q_OVERFLOW`).
903 pub wd: WatchDescriptor,
904 /// Masque des événements survenus (`IN_*`).
905 pub mask: InotifyEventMask,
906 /// Corrèle un `MOVED_FROM` avec son `MOVED_TO` (0 si non applicable).
907 pub cookie: u32,
908 /// Nom (octets, **pas** d'UTF-8 présumé) relatif au watch, sans le NUL de
909 /// padding ; `None` si l'événement ne porte pas de nom.
910 pub name: Option<&'b [u8]>,
911}
912
913/// Entête fixe d'un `struct inotify_event` : `wd:i32 + mask:u32 + cookie:u32 +
914/// len:u32` = 16 octets, suivi de `len` octets de `name` (padded NUL).
915const INOTIFY_EVENT_HEADER_LEN: usize = 16;
916
917/// Vue empruntée sur un buffer rempli par `read(2)` : suite d'événements de
918/// **taille variable**, décodée par un itérateur **zéro allocation** (mêmes
919/// principes que le parseur uevent : `get()` partout, jamais d'indexation
920/// paniquante, `name` = `&[u8]`). **Zéro perte (ADR-032)** : tous les événements
921/// complets sont rendus ; un enregistrement final **tronqué** est *signalé* via
922/// [`InotifyEvents::truncated`] (jamais avalé silencieusement).
923#[derive(Debug, Clone)]
924pub struct InotifyEvents<'b> {
925 data: &'b [u8],
926 offset: usize,
927 truncated: bool,
928}
929
930impl<'b> InotifyEvents<'b> {
931 /// Décode un buffer (rempli par `read` du FD inotify). Donnée **externe**
932 /// (kernel) au sens du Principe 3 : aucune entrée ne panique (cf. fuzz).
933 #[must_use]
934 pub const fn parse(data: &'b [u8]) -> Self {
935 Self {
936 data,
937 offset: 0,
938 truncated: false,
939 }
940 }
941
942 /// `true` si, après itération, le buffer se terminait par un enregistrement
943 /// **incomplet** (entête ou `name` coupé). Signale à l'appelant qu'un
944 /// événement n'a pas pu être rendu (agrandir le buffer / relire) — ADR-032.
945 #[must_use]
946 pub const fn truncated(&self) -> bool {
947 self.truncated
948 }
949
950 /// Lit un `u32` natif à `off` dans `data` (borné par `get`).
951 fn read_u32(&self, off: usize) -> Option<u32> {
952 let end = off.checked_add(4)?;
953 let bytes: [u8; 4] = self.data.get(off..end)?.try_into().ok()?;
954 Some(u32::from_ne_bytes(bytes))
955 }
956}
957
958impl<'b> Iterator for InotifyEvents<'b> {
959 type Item = InotifyEvent<'b>;
960
961 fn next(&mut self) -> Option<Self::Item> {
962 let start = self.offset;
963 // Fin propre : exactement à une frontière d'enregistrement.
964 if start == self.data.len() {
965 return None;
966 }
967 // Entête (16 o) : s'il ne tient pas, c'est une queue tronquée.
968 let Some(name_len_u32) = self.read_u32(start.checked_add(12)?) else {
969 self.truncated = true;
970 return None;
971 };
972 let wd = self.read_u32(start)?.cast_signed();
973 let mask = self.read_u32(start.checked_add(4)?)?;
974 let cookie = self.read_u32(start.checked_add(8)?)?;
975 let name_len = usize::try_from(name_len_u32).ok()?;
976 let name_start = start.checked_add(INOTIFY_EVENT_HEADER_LEN)?;
977 let name_end = name_start.checked_add(name_len)?;
978 // `name` annoncé mais coupé par la fin du buffer → queue tronquée.
979 let Some(name_bytes) = self.data.get(name_start..name_end) else {
980 self.truncated = true;
981 return None;
982 };
983 self.offset = name_end;
984 // Retire le padding NUL ; `None` si aucun nom (len == 0 ou tout NUL).
985 let trimmed = match name_bytes.iter().position(|&b| b == 0) {
986 Some(nul) => name_bytes.get(..nul).unwrap_or(&[]),
987 None => name_bytes,
988 };
989 let name = if trimmed.is_empty() {
990 None
991 } else {
992 Some(trimmed)
993 };
994 Some(InotifyEvent {
995 wd: WatchDescriptor::from_raw(wd),
996 mask: InotifyEventMask::from_bits_truncate(mask),
997 cookie,
998 name,
999 })
1000 }
1001}
1002
1003#[cfg(test)]
1004mod io_uring_2a_types_tests;
1005
1006#[cfg(test)]
1007mod inotify_decoder_tests;