Skip to main content

air_sys_syscall/
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//! Wrappers de la famille `fs` — filesystem, I/O synchrone, métadonnées.
6//!
7//! Cf. `docs/specs/layer-0/family-fs.md`.
8
9#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
10compile_error!("air-sys-syscall::fs supporte uniquement x86_64 et aarch64 (ADR-014).");
11
12use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
13use alloc::vec::Vec;
14use core::ffi::CStr;
15use core::num::NonZeroI32;
16
17use air_sys_types::Errno;
18use air_sys_types::fs::{
19    AccessFlags, AccessMode, ChmodFlags, CloseRangeFlags, DirEntry, DirEntryType, DirFd,
20    FallocateMode, FdFlags, FileHandle, FileLock, FsType, Mode, NameToHandleFlags, OpenFlags,
21    OpenHow, RenameFlags, Seals, SeekWhence, StatFsResult, StatusFlags, StatxFlags, StatxMask,
22    StatxResult, StatxTimestamp, UtimeValue,
23};
24use air_sys_types::net::{IoSlice, IoSliceMut};
25
26// ─────────────────────────────────────────────────────────────────────────
27// Constantes kernel
28// ─────────────────────────────────────────────────────────────────────────
29
30const AT_FDCWD: i32 = -100;
31const O_CLOEXEC: u64 = 0o2_000_000;
32
33/// Surveillance de fichiers (inotify). Sous-module dédié `fs::inotify`
34/// (cf. `docs/specs/layer-0/family-fs-inotify.md`).
35pub mod inotify;
36
37// fcntl commands
38const F_DUPFD_CLOEXEC: i32 = 1030;
39const F_GETFD: i32 = 1;
40const F_SETFD: i32 = 2;
41const F_GETFL: i32 = 3;
42const F_SETFL: i32 = 4;
43const F_GETPIPE_SZ: i32 = 1032;
44const F_SETPIPE_SZ: i32 = 1031;
45const F_SETLK: i32 = 6;
46const F_SETLKW: i32 = 7;
47const F_ADD_SEALS: i32 = 1033;
48const F_GET_SEALS: i32 = 1034;
49
50// utimensat special values
51const UTIME_NOW: i64 = 0x3FFF_FFFF;
52const UTIME_OMIT: i64 = 0x3FFF_FFFE;
53
54// ─────────────────────────────────────────────────────────────────────────
55// Helpers internes
56// ─────────────────────────────────────────────────────────────────────────
57
58fn dirfd_to_raw(dirfd: DirFd<'_>) -> i32 {
59    match dirfd {
60        DirFd::Cwd => AT_FDCWD,
61        DirFd::Fd(fd) => fd.as_raw_fd(),
62    }
63}
64
65/// Convertit un fd (i32, potentiellement AT_FDCWD = -100) en u64 pour le syscall ABI.
66///
67/// Un fd négatif (AT_FDCWD = -100) est une sentinelle valide du kernel ; la
68/// représentation u64 en complément à deux correspond exactement à ce que le kernel attend.
69#[inline]
70fn fd_to_u64(fd: i32) -> u64 {
71    // SAFETY: les syscalls sur x86_64/aarch64 prennent les arguments dans des
72    // registres 64 bits. Un fd négatif (ex. AT_FDCWD = -100) est transmis en
73    // extension de signe vers 64 bits, ce que le cast i32 → i64 → u64 produit.
74    #[allow(clippy::cast_sign_loss)]
75    {
76        fd as u64
77    }
78}
79
80/// Convertit un i32 en u64 (extension de signe) pour les arguments syscall.
81#[inline]
82fn i32_to_u64(v: i32) -> u64 {
83    // Les valeurs i32 négatives (ex. O_* flags, AT_REMOVEDIR, whence) sont des
84    // arguments syscall valides ; leur représentation u64 en complément à deux
85    // est correcte pour l'ABI kernel.
86    #[allow(clippy::cast_sign_loss)]
87    {
88        v as u64
89    }
90}
91
92/// Convertit un i64 retourné par un syscall en usize.
93///
94/// Précondition : `ret >= 0` (déjà vérifié avant l'appel).
95#[inline]
96fn ret_to_usize(ret: i64) -> usize {
97    // ret >= 0 et <= SSIZE_MAX sur toutes les cibles 64 bits Air (ADR-014) ;
98    // la troncature en usize est exacte sur les cibles 64 bits.
99    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
100    {
101        ret as usize
102    }
103}
104
105/// Convertit un i64 en u64 (interprétation bit-à-bit) pour les arguments syscall.
106///
107/// Utilisé pour les offsets signés passés au kernel (ex. lseek, ftruncate, fallocate).
108/// L'ABI syscall Linux attend les valeurs dans des registres 64 bits ; un offset
109/// négatif est valide et représenté en complément à deux.
110#[inline]
111fn i64_to_u64(v: i64) -> u64 {
112    #[allow(clippy::cast_sign_loss)]
113    {
114        v as u64
115    }
116}
117
118/// Wraps `ret_to_usize` dans un `Ok()` — utilisé comme dernière expression des fonctions
119/// de type I/O retournant un compte d'octets.
120#[inline]
121fn ret_to_usize_ok(ret: i64) -> Result<usize, Errno> {
122    Ok(ret_to_usize(ret))
123}
124fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
125    debug_assert!(ret < 0 && ret > -4096);
126    // `ret.wrapping_neg()` est donc dans `[1, 4095]` — toujours fit in i32.
127    #[allow(clippy::cast_possible_truncation)]
128    let raw = ret.wrapping_neg() as i32;
129    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
130    Errno::from_nonzero(nz)
131}
132
133// ─────────────────────────────────────────────────────────────────────────
134// Structs kernel privées
135// ─────────────────────────────────────────────────────────────────────────
136
137#[repr(C)]
138struct KernelOpenHow {
139    flags: u64,
140    mode: u64,
141    resolve: u64,
142}
143
144const _: () = assert!(core::mem::size_of::<KernelOpenHow>() == 24);
145
146#[repr(C)]
147struct KernelStatxTimestamp {
148    tv_sec: i64,
149    tv_nsec: u32,
150    __reserved: i32,
151}
152
153#[repr(C)]
154struct KernelStatx {
155    stx_mask: u32,
156    stx_blksize: u32,
157    stx_attributes: u64,
158    stx_nlink: u32,
159    stx_uid: u32,
160    stx_gid: u32,
161    stx_mode: u16,
162    __spare0: [u16; 1],
163    stx_ino: u64,
164    stx_size: u64,
165    stx_blocks: u64,
166    stx_attributes_mask: u64,
167    stx_atime: KernelStatxTimestamp,
168    stx_btime: KernelStatxTimestamp,
169    stx_ctime: KernelStatxTimestamp,
170    stx_mtime: KernelStatxTimestamp,
171    stx_rdev_major: u32,
172    stx_rdev_minor: u32,
173    stx_dev_major: u32,
174    stx_dev_minor: u32,
175    stx_mnt_id: u64,
176    __spare2: u64,
177    __spare3: [u64; 12],
178}
179
180const _: () = assert!(core::mem::size_of::<KernelStatx>() == 256);
181
182#[repr(C)]
183struct KernelStatfs {
184    f_type: i64,
185    f_bsize: i64,
186    f_blocks: u64,
187    f_bfree: u64,
188    f_bavail: u64,
189    f_files: u64,
190    f_ffree: u64,
191    f_fsid: [u32; 2],
192    f_namelen: i64,
193    f_frsize: i64,
194    f_flags: i64,
195    f_spare: [i64; 4],
196}
197
198const _: () = assert!(core::mem::size_of::<KernelStatfs>() == 120);
199
200#[repr(C)]
201struct KernelFlock {
202    l_type: i16,
203    l_whence: i16,
204    _pad: i32,
205    l_start: i64,
206    l_len: i64,
207    l_pid: i32,
208    _pad2: i32,
209}
210
211#[repr(C)]
212struct KernelFileHandle {
213    handle_bytes: u32,
214    handle_type: i32,
215    f_handle: [u8; 128],
216}
217
218// ─────────────────────────────────────────────────────────────────────────
219// Syscall raw helpers — x86_64
220// ─────────────────────────────────────────────────────────────────────────
221
222#[cfg(target_arch = "x86_64")]
223#[inline]
224unsafe fn syscall1(nr: i64, a0: u64) -> i64 {
225    let ret: i64;
226    // SAFETY: appelant garantit la validité des arguments.
227    unsafe {
228        core::arch::asm!(
229            "syscall",
230            in("rax") nr,
231            in("rdi") a0,
232            lateout("rax") ret,
233            lateout("rcx") _,
234            lateout("r11") _,
235            options(nostack, preserves_flags),
236        );
237    }
238    ret
239}
240
241#[cfg(target_arch = "x86_64")]
242#[inline]
243unsafe fn syscall2(nr: i64, a0: u64, a1: u64) -> i64 {
244    let ret: i64;
245    // SAFETY: appelant garantit la validité des arguments.
246    unsafe {
247        core::arch::asm!(
248            "syscall",
249            in("rax") nr,
250            in("rdi") a0,
251            in("rsi") a1,
252            lateout("rax") ret,
253            lateout("rcx") _,
254            lateout("r11") _,
255            options(nostack, preserves_flags),
256        );
257    }
258    ret
259}
260
261#[cfg(target_arch = "x86_64")]
262#[inline]
263unsafe fn syscall3(nr: i64, a0: u64, a1: u64, a2: u64) -> i64 {
264    let ret: i64;
265    // SAFETY: appelant garantit la validité des arguments.
266    unsafe {
267        core::arch::asm!(
268            "syscall",
269            in("rax") nr,
270            in("rdi") a0,
271            in("rsi") a1,
272            in("rdx") a2,
273            lateout("rax") ret,
274            lateout("rcx") _,
275            lateout("r11") _,
276            options(nostack, preserves_flags),
277        );
278    }
279    ret
280}
281
282#[cfg(target_arch = "x86_64")]
283#[inline]
284unsafe fn syscall4(nr: i64, a0: u64, a1: u64, a2: u64, a3: u64) -> i64 {
285    let ret: i64;
286    // SAFETY: appelant garantit la validité des arguments.
287    unsafe {
288        core::arch::asm!(
289            "syscall",
290            in("rax") nr,
291            in("rdi") a0,
292            in("rsi") a1,
293            in("rdx") a2,
294            in("r10") a3,
295            lateout("rax") ret,
296            lateout("rcx") _,
297            lateout("r11") _,
298            options(nostack, preserves_flags),
299        );
300    }
301    ret
302}
303
304#[cfg(target_arch = "x86_64")]
305#[inline]
306unsafe fn syscall5(nr: i64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> i64 {
307    let ret: i64;
308    // SAFETY: appelant garantit la validité des arguments.
309    unsafe {
310        core::arch::asm!(
311            "syscall",
312            in("rax") nr,
313            in("rdi") a0,
314            in("rsi") a1,
315            in("rdx") a2,
316            in("r10") a3,
317            in("r8") a4,
318            lateout("rax") ret,
319            lateout("rcx") _,
320            lateout("r11") _,
321            options(nostack, preserves_flags),
322        );
323    }
324    ret
325}
326
327#[cfg(target_arch = "x86_64")]
328#[inline]
329unsafe fn syscall6(nr: i64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {
330    let ret: i64;
331    // SAFETY: appelant garantit la validité des arguments.
332    unsafe {
333        core::arch::asm!(
334            "syscall",
335            in("rax") nr,
336            in("rdi") a0,
337            in("rsi") a1,
338            in("rdx") a2,
339            in("r10") a3,
340            in("r8") a4,
341            in("r9") a5,
342            lateout("rax") ret,
343            lateout("rcx") _,
344            lateout("r11") _,
345            options(nostack, preserves_flags),
346        );
347    }
348    ret
349}
350
351// ─────────────────────────────────────────────────────────────────────────
352// Syscall raw helpers — aarch64
353// ─────────────────────────────────────────────────────────────────────────
354
355#[cfg(target_arch = "aarch64")]
356#[inline]
357unsafe fn syscall1(nr: i64, a0: u64) -> i64 {
358    let ret: i64;
359    // SAFETY: appelant garantit la validité des arguments.
360    unsafe {
361        core::arch::asm!(
362            "svc 0",
363            in("x8") nr,
364            inout("x0") a0 => ret,
365            options(nostack, preserves_flags),
366        );
367    }
368    ret
369}
370
371#[cfg(target_arch = "aarch64")]
372#[inline]
373unsafe fn syscall2(nr: i64, a0: u64, a1: u64) -> i64 {
374    let ret: i64;
375    // SAFETY: appelant garantit la validité des arguments.
376    unsafe {
377        core::arch::asm!(
378            "svc 0",
379            in("x8") nr,
380            inout("x0") a0 => ret,
381            in("x1") a1,
382            options(nostack, preserves_flags),
383        );
384    }
385    ret
386}
387
388#[cfg(target_arch = "aarch64")]
389#[inline]
390unsafe fn syscall3(nr: i64, a0: u64, a1: u64, a2: u64) -> i64 {
391    let ret: i64;
392    // SAFETY: appelant garantit la validité des arguments.
393    unsafe {
394        core::arch::asm!(
395            "svc 0",
396            in("x8") nr,
397            inout("x0") a0 => ret,
398            in("x1") a1,
399            in("x2") a2,
400            options(nostack, preserves_flags),
401        );
402    }
403    ret
404}
405
406#[cfg(target_arch = "aarch64")]
407#[inline]
408unsafe fn syscall4(nr: i64, a0: u64, a1: u64, a2: u64, a3: u64) -> i64 {
409    let ret: i64;
410    // SAFETY: appelant garantit la validité des arguments.
411    unsafe {
412        core::arch::asm!(
413            "svc 0",
414            in("x8") nr,
415            inout("x0") a0 => ret,
416            in("x1") a1,
417            in("x2") a2,
418            in("x3") a3,
419            options(nostack, preserves_flags),
420        );
421    }
422    ret
423}
424
425#[cfg(target_arch = "aarch64")]
426#[inline]
427unsafe fn syscall5(nr: i64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> i64 {
428    let ret: i64;
429    // SAFETY: appelant garantit la validité des arguments.
430    unsafe {
431        core::arch::asm!(
432            "svc 0",
433            in("x8") nr,
434            inout("x0") a0 => ret,
435            in("x1") a1,
436            in("x2") a2,
437            in("x3") a3,
438            in("x4") a4,
439            options(nostack, preserves_flags),
440        );
441    }
442    ret
443}
444
445#[cfg(target_arch = "aarch64")]
446#[inline]
447unsafe fn syscall6(nr: i64, a0: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {
448    let ret: i64;
449    // SAFETY: appelant garantit la validité des arguments.
450    unsafe {
451        core::arch::asm!(
452            "svc 0",
453            in("x8") nr,
454            inout("x0") a0 => ret,
455            in("x1") a1,
456            in("x2") a2,
457            in("x3") a3,
458            in("x4") a4,
459            in("x5") a5,
460            options(nostack, preserves_flags),
461        );
462    }
463    ret
464}
465
466// ─────────────────────────────────────────────────────────────────────────
467// Numéros de syscall par architecture
468// ─────────────────────────────────────────────────────────────────────────
469
470#[cfg(target_arch = "x86_64")]
471mod nr {
472    pub const READ: i64 = 0;
473    pub const WRITE: i64 = 1;
474    pub const CLOSE: i64 = 3;
475    pub const CLOSE_RANGE: i64 = 436;
476    pub const LSEEK: i64 = 8;
477    pub const PREAD64: i64 = 17;
478    pub const PWRITE64: i64 = 18;
479    pub const READV: i64 = 19;
480    pub const WRITEV: i64 = 20;
481    pub const PREADV: i64 = 295;
482    pub const PWRITEV: i64 = 296;
483    pub const FCNTL: i64 = 72;
484    pub const FLOCK: i64 = 73;
485    pub const FSYNC: i64 = 74;
486    pub const FDATASYNC: i64 = 75;
487    pub const FCHMOD: i64 = 91;
488    pub const FCHOWN: i64 = 93;
489    pub const SENDFILE: i64 = 40;
490    pub const FTRUNCATE: i64 = 77;
491    pub const GETDENTS64: i64 = 217;
492    pub const STATFS: i64 = 137;
493    pub const FSTATFS: i64 = 138;
494    pub const FALLOCATE: i64 = 285;
495    pub const OPENAT: i64 = 257;
496    pub const MKDIRAT: i64 = 258;
497    pub const MKNODAT: i64 = 259;
498    pub const FCHOWNAT: i64 = 260;
499    pub const UNLINKAT: i64 = 263;
500    pub const LINKAT: i64 = 265;
501    pub const SYMLINKAT: i64 = 266;
502    pub const READLINKAT: i64 = 267;
503    pub const FCHMODAT: i64 = 268;
504    pub const FCHMODAT2: i64 = 452;
505    pub const FACCESSAT: i64 = 269;
506    pub const FACCESSAT2: i64 = 439;
507    pub const UTIMENSAT: i64 = 280;
508    pub const COPY_FILE_RANGE: i64 = 326;
509    pub const STATX: i64 = 332;
510    pub const NAME_TO_HANDLE_AT: i64 = 303;
511    pub const OPEN_BY_HANDLE_AT: i64 = 304;
512    pub const SYNC_FILE_RANGE: i64 = 277;
513    pub const RENAMEAT2: i64 = 316;
514    pub const OPENAT2: i64 = 437;
515    pub const INOTIFY_INIT1: i64 = 294;
516    pub const INOTIFY_ADD_WATCH: i64 = 254;
517    pub const INOTIFY_RM_WATCH: i64 = 255;
518}
519
520#[cfg(target_arch = "aarch64")]
521mod nr {
522    pub const READ: i64 = 63;
523    pub const WRITE: i64 = 64;
524    pub const CLOSE: i64 = 57;
525    pub const CLOSE_RANGE: i64 = 436;
526    pub const LSEEK: i64 = 62;
527    pub const PREAD64: i64 = 67;
528    pub const PWRITE64: i64 = 68;
529    pub const READV: i64 = 65;
530    pub const WRITEV: i64 = 66;
531    pub const PREADV: i64 = 69;
532    pub const PWRITEV: i64 = 70;
533    pub const FCNTL: i64 = 25;
534    pub const FLOCK: i64 = 32;
535    pub const FSYNC: i64 = 82;
536    pub const FDATASYNC: i64 = 83;
537    pub const FCHMOD: i64 = 52;
538    pub const FCHOWN: i64 = 55;
539    pub const SENDFILE: i64 = 71;
540    pub const FTRUNCATE: i64 = 46;
541    pub const GETDENTS64: i64 = 61;
542    pub const STATFS: i64 = 43;
543    pub const FSTATFS: i64 = 44;
544    pub const FALLOCATE: i64 = 47;
545    pub const OPENAT: i64 = 56;
546    pub const MKDIRAT: i64 = 34;
547    pub const MKNODAT: i64 = 33;
548    pub const FCHOWNAT: i64 = 54;
549    pub const UNLINKAT: i64 = 35;
550    pub const LINKAT: i64 = 37;
551    pub const SYMLINKAT: i64 = 36;
552    pub const READLINKAT: i64 = 78;
553    pub const FCHMODAT: i64 = 53;
554    pub const FCHMODAT2: i64 = 452;
555    pub const FACCESSAT: i64 = 48;
556    pub const FACCESSAT2: i64 = 439;
557    pub const UTIMENSAT: i64 = 88;
558    pub const COPY_FILE_RANGE: i64 = 285;
559    pub const STATX: i64 = 291;
560    pub const NAME_TO_HANDLE_AT: i64 = 264;
561    pub const OPEN_BY_HANDLE_AT: i64 = 265;
562    pub const SYNC_FILE_RANGE: i64 = 84;
563    pub const RENAMEAT2: i64 = 276;
564    pub const OPENAT2: i64 = 437;
565    pub const INOTIFY_INIT1: i64 = 26;
566    pub const INOTIFY_ADD_WATCH: i64 = 27;
567    pub const INOTIFY_RM_WATCH: i64 = 28;
568}
569
570// ─────────────────────────────────────────────────────────────────────────
571// Ouverture et fermeture
572// ─────────────────────────────────────────────────────────────────────────
573
574/// Ouvre un fichier avec contrôle de résolution de chemin avancé.
575///
576/// Wrappeur de `openat2(2)` (Linux 5.6+, numéro 437). Préféré à
577/// [`openat`] car il permet de contraindre la résolution de chemin via
578/// [`OpenHow::resolve`]. Retombe automatiquement sur [`openat`] si le
579/// kernel retourne `ENOSYS` (< Linux 5.6).
580///
581/// `O_CLOEXEC` est **toujours** ajouté aux flags par le wrapper —
582/// tous les FDs ouverts par Air ont `CLOEXEC` par défaut.
583///
584/// # Parameters
585///
586/// - `dirfd` : répertoire de base pour la résolution ([`DirFd::Cwd`] ou
587///   un FD de répertoire).
588/// - `path` : chemin relatif à `dirfd` (ou absolu si `dirfd` est ignoré).
589/// - `how` : flags, mode et politique de résolution.
590///
591/// # Errors
592///
593/// - `EACCES` : permissions insuffisantes.
594/// - `ENOENT` : chemin inexistant (sans `CREAT`).
595/// - `EEXIST` : fichier existant avec `CREAT | EXCL`.
596/// - `EISDIR` : ouverture en écriture d'un répertoire.
597/// - `ENOTDIR` : un composant du chemin n'est pas un répertoire.
598/// - `EXDEV` : violation de `RESOLVE_NO_XDEV`.
599/// - `EAGAIN` : violation de `RESOLVE_BENEATH` ou `RESOLVE_NO_SYMLINKS`.
600/// - `ENFILE` / `EMFILE` : quotas FD atteints.
601///
602/// # Examples
603///
604/// ```no_run
605/// use air_sys_syscall::fs::openat2;
606/// use air_sys_types::fs::{DirFd, OpenHow, OpenFlags, ResolveFlags};
607///
608/// let how = OpenHow {
609///     flags: OpenFlags::RDONLY,
610///     mode: 0,
611///     resolve: ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS,
612/// };
613/// let fd = openat2(DirFd::Cwd, c"./config.toml", how).expect("openat2");
614/// ```
615pub fn openat2(dirfd: DirFd<'_>, path: &CStr, how: OpenHow) -> Result<OwnedFd, Errno> {
616    let raw_dirfd = dirfd_to_raw(dirfd);
617    let kernel_how = KernelOpenHow {
618        flags: how.flags.bits() | O_CLOEXEC,
619        mode: u64::from(how.mode),
620        resolve: how.resolve.bits(),
621    };
622    let path_ptr = path.as_ptr() as u64;
623    let how_ptr = &kernel_how as *const KernelOpenHow as u64;
624    let how_size = core::mem::size_of::<KernelOpenHow>();
625
626    // SAFETY:
627    // - path_ptr pointe sur un CStr nul-terminé valide pour la durée du syscall.
628    // - how_ptr pointe sur KernelOpenHow local valide, size_of correct.
629    // - raw_dirfd est AT_FDCWD ou un fd valide fourni par l'appelant.
630    let ret = unsafe {
631        syscall4(
632            nr::OPENAT2,
633            fd_to_u64(raw_dirfd),
634            path_ptr,
635            how_ptr,
636            how_size as u64,
637        )
638    };
639
640    if ret < 0 {
641        let err = errno_from_negative_syscall_ret(ret);
642        // Fallback sur openat si ENOSYS (kernel < 5.6)
643        if err == Errno::ENOSYS {
644            return openat(dirfd, path, how.flags, how.mode);
645        }
646        return Err(err);
647    }
648
649    // SAFETY: ret est un fd valide retourné par le kernel après succès.
650    #[allow(clippy::cast_possible_truncation)]
651    Ok(unsafe { OwnedFd::from_raw_fd(ret as i32) })
652}
653
654/// Ouvre un fichier (version sans contrôle de résolution avancé).
655///
656/// Wrappeur de `openat(2)`. Utilisé en fallback quand `openat2` n'est
657/// pas disponible (kernel < 5.6). `O_CLOEXEC` est toujours ajouté.
658///
659/// # Parameters
660///
661/// - `dirfd` : répertoire de base.
662/// - `path` : chemin relatif ou absolu.
663/// - `flags` : flags d'ouverture.
664/// - `mode` : permissions de création (significatif seulement avec `CREAT`).
665///
666/// # Errors
667///
668/// Voir [`openat2`] (sans les codes liés à `RESOLVE_*`).
669///
670/// # Examples
671///
672/// ```no_run
673/// use air_sys_syscall::fs::openat;
674/// use air_sys_types::fs::{DirFd, OpenFlags};
675///
676/// let fd = openat(DirFd::Cwd, c"./data.bin", OpenFlags::RDONLY, 0)
677///     .expect("openat");
678/// ```
679pub fn openat(
680    dirfd: DirFd<'_>,
681    path: &CStr,
682    flags: OpenFlags,
683    mode: Mode,
684) -> Result<OwnedFd, Errno> {
685    let raw_dirfd = dirfd_to_raw(dirfd);
686    let raw_flags = flags.bits() | O_CLOEXEC;
687    // raw_flags fits in i32 (O_* constants are all < 2^31).
688    #[allow(clippy::cast_possible_truncation)]
689    let raw_flags_i32 = raw_flags as i32;
690    let path_ptr = path.as_ptr() as u64;
691
692    // SAFETY:
693    // - path_ptr est un CStr nul-terminé valide pour la durée du syscall.
694    // - raw_dirfd est AT_FDCWD ou un fd valide.
695    // - raw_flags_i32 contient des bits O_* valides.
696    // - mode est une valeur Mode (u32) ; les 12 bits inférieurs sont utilisés.
697    let ret = unsafe {
698        syscall4(
699            nr::OPENAT,
700            fd_to_u64(raw_dirfd),
701            path_ptr,
702            i32_to_u64(raw_flags_i32),
703            u64::from(mode),
704        )
705    };
706
707    if ret < 0 {
708        return Err(errno_from_negative_syscall_ret(ret));
709    }
710
711    // SAFETY: ret est un fd valide retourné par le kernel après succès.
712    #[allow(clippy::cast_possible_truncation)]
713    Ok(unsafe { OwnedFd::from_raw_fd(ret as i32) })
714}
715
716/// Ferme un FD explicitement et récupère l'erreur éventuelle.
717///
718/// Consomme l'[`OwnedFd`]. Le `Drop` automatique ferme aussi le FD mais
719/// ignore les erreurs (ex. `EIO` sur NFS en cas de panne). Cette
720/// fonction permet de les détecter.
721///
722/// # Errors
723///
724/// - `EIO` : erreur d'E/S lors de la fermeture (ex. NFS).
725/// - `EINTR` : interrompu par un signal. Sur Linux, le FD est quand même
726///   fermé après `EINTR` (ne pas réessayer).
727///
728/// # Examples
729///
730/// ```no_run
731/// use air_sys_syscall::fs::{openat, close};
732/// use air_sys_types::fs::{DirFd, OpenFlags};
733///
734/// let fd = openat(DirFd::Cwd, c"./out.log", OpenFlags::WRONLY, 0)
735///     .expect("openat");
736/// close(fd).expect("close");
737/// ```
738pub fn close(fd: OwnedFd) -> Result<(), Errno> {
739    let raw = fd.as_raw_fd();
740    // Consomme le OwnedFd sans appeler son Drop (qui fermerait le fd une 2e fois).
741    core::mem::forget(fd);
742
743    // SAFETY:
744    // - raw est le fd de l'OwnedFd consommé, valide au moment de l'appel.
745    // - close(2) ne lit ni n'écrit de mémoire utilisateur.
746    let ret = unsafe { syscall1(nr::CLOSE, fd_to_u64(raw)) };
747
748    if ret < 0 {
749        return Err(errno_from_negative_syscall_ret(ret));
750    }
751    Ok(())
752}
753
754/// `close_range(first, last, flags)` (Linux 5.9+) — ferme **tous** les descripteurs
755/// de l'intervalle **inclusif** `[first, last]`. `last == u32::MAX` ferme tout à
756/// partir de `first`.
757///
758/// Confinement (ADR-122) : un enfant forké ferme en un appel tous les fd hérités
759/// hormis un keep-set (ring io_uring, sockets d'écoute, autres connexions), **avant**
760/// de poser sa cage — évite un balayage `/proc/self/fd` (openat/getdents) qui, lui,
761/// serait interdit par le profil confiné.
762///
763/// Contrairement à [`close`], cette fonction **ne consomme pas** d'`OwnedFd` : elle
764/// opère sur des **numéros bruts** (l'appelant garantit qu'aucun `OwnedFd` vivant ne
765/// couvre l'intervalle, sous peine de double-fermeture au `Drop`).
766///
767/// # Errors
768///
769/// - [`Errno::EINVAL`] : `first > last`, ou `flags` non reconnus.
770/// - [`Errno::EMFILE`] / [`Errno::ENOMEM`] : avec [`CloseRangeFlags::UNSHARE`], échec
771///   du dédoublement de la table de descripteurs.
772///
773/// # Examples
774///
775/// ```no_run
776/// use air_sys_syscall::fs::close_range;
777/// use air_sys_types::fs::CloseRangeFlags;
778///
779/// // Ferme tout à partir du fd 3 (garde stdin/stdout/stderr).
780/// close_range(3, u32::MAX, CloseRangeFlags::empty()).expect("close_range");
781/// ```
782pub fn close_range(first: u32, last: u32, flags: CloseRangeFlags) -> Result<(), Errno> {
783    // SAFETY:
784    // - close_range(2) ne lit ni n'écrit de mémoire utilisateur.
785    // - Ferme des descripteurs par numéro ; l'invariant « aucun OwnedFd vivant dans
786    //   l'intervalle » est une précondition documentée de l'appelant.
787    let ret = unsafe {
788        syscall3(
789            nr::CLOSE_RANGE,
790            u64::from(first),
791            u64::from(last),
792            u64::from(flags.bits()),
793        )
794    };
795
796    if ret < 0 {
797        return Err(errno_from_negative_syscall_ret(ret));
798    }
799    Ok(())
800}
801
802// ─────────────────────────────────────────────────────────────────────────
803// I/O synchrone
804// ─────────────────────────────────────────────────────────────────────────
805
806/// Lit depuis un FD dans `buffer`.
807///
808/// Wrappeur de `read(2)`. Peut retourner moins d'octets que `buffer.len()`
809/// (short read). L'appelant doit boucler si un read complet est requis.
810///
811/// # Errors
812///
813/// - `EAGAIN` : FD non-bloquant sans données disponibles.
814/// - `EINTR` : interrompu par un signal (retry à la charge de l'appelant).
815/// - `EIO` : erreur I/O bas niveau.
816/// - `EBADF` : FD invalide.
817///
818/// # Examples
819///
820/// ```no_run
821/// use air_sys_syscall::fs::read;
822/// use air_sys_types::fd::BorrowedFd;
823///
824/// # fn example(fd: BorrowedFd<'_>) {
825/// let mut buffer = [0u8; 1024];
826/// let n = read(fd, &mut buffer).expect("read");
827/// # }
828/// ```
829pub fn read(fd: BorrowedFd<'_>, buffer: &mut [u8]) -> Result<usize, Errno> {
830    // SAFETY:
831    // - buffer est un slice mutable valide pour la durée du syscall.
832    // - fd est un BorrowedFd valide.
833    // - read(2) écrit au plus buffer.len() octets dans buffer.
834    let ret = unsafe {
835        syscall3(
836            nr::READ,
837            fd_to_u64(fd.as_raw_fd()),
838            buffer.as_mut_ptr() as u64,
839            buffer.len() as u64,
840        )
841    };
842
843    if ret < 0 {
844        return Err(errno_from_negative_syscall_ret(ret));
845    }
846
847    // ret >= 0, au plus buffer.len() <= usize::MAX : cast sûr.
848    #[allow(clippy::cast_sign_loss)]
849    ret_to_usize_ok(ret)
850}
851
852/// Écrit `buffer` sur un FD.
853///
854/// Wrappeur de `write(2)`. Peut retourner moins d'octets écrits que
855/// `buffer.len()` (short write). L'appelant doit boucler si nécessaire.
856///
857/// # Errors
858///
859/// - `EAGAIN` : FD non-bloquant et buffer kernel plein.
860/// - `EINTR` : interrompu par un signal.
861/// - `EIO` : erreur I/O bas niveau.
862/// - `ENOSPC` : plus d'espace sur le device.
863/// - `EPIPE` : extrémité de lecture du pipe fermée.
864///
865/// # Examples
866///
867/// ```no_run
868/// use air_sys_syscall::fs::write;
869/// use air_sys_types::fd::BorrowedFd;
870///
871/// # fn example(fd: BorrowedFd<'_>) {
872/// let n = write(fd, b"hello\n").expect("write");
873/// # }
874/// ```
875pub fn write(fd: BorrowedFd<'_>, buffer: &[u8]) -> Result<usize, Errno> {
876    // SAFETY:
877    // - buffer est un slice immuable valide pour la durée du syscall.
878    // - write(2) lit au plus buffer.len() octets depuis buffer.
879    let ret = unsafe {
880        syscall3(
881            nr::WRITE,
882            fd_to_u64(fd.as_raw_fd()),
883            buffer.as_ptr() as u64,
884            buffer.len() as u64,
885        )
886    };
887
888    if ret < 0 {
889        return Err(errno_from_negative_syscall_ret(ret));
890    }
891
892    #[allow(clippy::cast_sign_loss)]
893    ret_to_usize_ok(ret)
894}
895
896/// Lit depuis une position absolue sans modifier la position courante du FD.
897///
898/// # Errors
899///
900/// Voir [`read`]. De plus : `ESPIPE` si le FD n'est pas seekable (pipe, socket).
901///
902/// # Examples
903///
904/// ```no_run
905/// use air_sys_syscall::fs::pread;
906/// use air_sys_types::fd::BorrowedFd;
907///
908/// # fn example(fd: BorrowedFd<'_>) {
909/// let mut buffer = [0u8; 512];
910/// let n = pread(fd, &mut buffer, 1024).expect("pread");
911/// # }
912/// ```
913pub fn pread(fd: BorrowedFd<'_>, buffer: &mut [u8], offset: u64) -> Result<usize, Errno> {
914    // SAFETY: buffer est un slice mutable valide ; pread64 écrit au plus buffer.len() octets.
915    let ret = unsafe {
916        syscall4(
917            nr::PREAD64,
918            fd_to_u64(fd.as_raw_fd()),
919            buffer.as_mut_ptr() as u64,
920            buffer.len() as u64,
921            offset,
922        )
923    };
924
925    if ret < 0 {
926        return Err(errno_from_negative_syscall_ret(ret));
927    }
928
929    #[allow(clippy::cast_sign_loss)]
930    ret_to_usize_ok(ret)
931}
932
933/// Écrit à une position absolue sans modifier la position courante du FD.
934///
935/// # Errors
936///
937/// Voir [`write()`]. De plus : `ESPIPE` si FD non seekable.
938///
939/// # Examples
940///
941/// ```no_run
942/// use air_sys_syscall::fs::pwrite;
943/// use air_sys_types::fd::BorrowedFd;
944///
945/// # fn example(fd: BorrowedFd<'_>) {
946/// let n = pwrite(fd, b"data", 0).expect("pwrite");
947/// # }
948/// ```
949pub fn pwrite(fd: BorrowedFd<'_>, buffer: &[u8], offset: u64) -> Result<usize, Errno> {
950    // SAFETY: buffer est un slice immuable valide ; pwrite64 lit au plus buffer.len() octets.
951    let ret = unsafe {
952        syscall4(
953            nr::PWRITE64,
954            fd_to_u64(fd.as_raw_fd()),
955            buffer.as_ptr() as u64,
956            buffer.len() as u64,
957            offset,
958        )
959    };
960
961    if ret < 0 {
962        return Err(errno_from_negative_syscall_ret(ret));
963    }
964
965    #[allow(clippy::cast_sign_loss)]
966    ret_to_usize_ok(ret)
967}
968
969/// Lecture scatter vers plusieurs buffers (`readv(2)`).
970///
971/// # Errors
972///
973/// Voir [`read`].
974///
975/// # Examples
976///
977/// ```no_run
978/// use air_sys_syscall::fs::readv;
979/// use air_sys_types::net::IoSliceMut;
980/// use air_sys_types::fd::BorrowedFd;
981///
982/// # fn example(fd: BorrowedFd<'_>) {
983/// let mut a = [0u8; 64];
984/// let mut b = [0u8; 64];
985/// let mut iov = [IoSliceMut::new(&mut a), IoSliceMut::new(&mut b)];
986/// let n = readv(fd, &mut iov).expect("readv");
987/// # }
988/// ```
989pub fn readv(fd: BorrowedFd<'_>, iov: &mut [IoSliceMut<'_>]) -> Result<usize, Errno> {
990    // SAFETY:
991    // - iov est un slice de IoSliceMut (repr(C), layout iovec) valide.
992    // - readv(2) écrit dans les buffers pointés par chaque iovec.
993    let ret = unsafe {
994        syscall3(
995            nr::READV,
996            fd_to_u64(fd.as_raw_fd()),
997            iov.as_ptr() as u64,
998            iov.len() as u64,
999        )
1000    };
1001
1002    if ret < 0 {
1003        return Err(errno_from_negative_syscall_ret(ret));
1004    }
1005
1006    #[allow(clippy::cast_sign_loss)]
1007    ret_to_usize_ok(ret)
1008}
1009
1010/// Écriture gather depuis plusieurs buffers (`writev(2)`).
1011///
1012/// # Errors
1013///
1014/// Voir [`write()`].
1015///
1016/// # Examples
1017///
1018/// ```no_run
1019/// use air_sys_syscall::fs::writev;
1020/// use air_sys_types::net::IoSlice;
1021/// use air_sys_types::fd::BorrowedFd;
1022///
1023/// # fn example(fd: BorrowedFd<'_>) {
1024/// let a = b"header";
1025/// let b = b"payload";
1026/// let iov = [IoSlice::new(a), IoSlice::new(b)];
1027/// let n = writev(fd, &iov).expect("writev");
1028/// # }
1029/// ```
1030pub fn writev(fd: BorrowedFd<'_>, iov: &[IoSlice<'_>]) -> Result<usize, Errno> {
1031    // SAFETY:
1032    // - iov est un slice de IoSlice (repr(C), layout iovec) valide.
1033    // - writev(2) lit dans les buffers pointés par chaque iovec.
1034    let ret = unsafe {
1035        syscall3(
1036            nr::WRITEV,
1037            fd_to_u64(fd.as_raw_fd()),
1038            iov.as_ptr() as u64,
1039            iov.len() as u64,
1040        )
1041    };
1042
1043    if ret < 0 {
1044        return Err(errno_from_negative_syscall_ret(ret));
1045    }
1046
1047    #[allow(clippy::cast_sign_loss)]
1048    ret_to_usize_ok(ret)
1049}
1050
1051/// Lecture scatter positionnée (`preadv(2)`).
1052pub fn preadv(fd: BorrowedFd<'_>, iov: &mut [IoSliceMut<'_>], offset: u64) -> Result<usize, Errno> {
1053    // preadv ABI: (fd, iov, iovcnt, offset_lo, offset_hi) on x86_64
1054    // On aarch64 : (fd, iov, iovcnt, offset) — offset is a single 64-bit register.
1055    // We pass offset as a5 (r8/x4) with a4=0 on x86_64 for simplicity using 5-arg form.
1056    // Actually the Linux preadv ABI on x86_64 takes offset split into two 32-bit args
1057    // in r8 (low) and r9 (high). We use syscall6 to pass both halves.
1058    let off_lo = offset & 0xFFFF_FFFF;
1059    let off_hi = offset >> 32;
1060
1061    // SAFETY:
1062    // - iov est un slice de IoSliceMut valide.
1063    // - preadv écrit dans les buffers référencés par iov.
1064    // - offset est une position valide (non vérifiée par le wrapper, ESPIPE si non seekable).
1065    let ret = unsafe {
1066        syscall6(
1067            nr::PREADV,
1068            fd_to_u64(fd.as_raw_fd()),
1069            iov.as_ptr() as u64,
1070            iov.len() as u64,
1071            off_lo,
1072            off_hi,
1073            0,
1074        )
1075    };
1076
1077    if ret < 0 {
1078        return Err(errno_from_negative_syscall_ret(ret));
1079    }
1080
1081    #[allow(clippy::cast_sign_loss)]
1082    ret_to_usize_ok(ret)
1083}
1084
1085/// Écriture gather positionnée (`pwritev(2)`).
1086pub fn pwritev(fd: BorrowedFd<'_>, iov: &[IoSlice<'_>], offset: u64) -> Result<usize, Errno> {
1087    let off_lo = offset & 0xFFFF_FFFF;
1088    let off_hi = offset >> 32;
1089
1090    // SAFETY:
1091    // - iov est un slice de IoSlice valide.
1092    // - pwritev lit dans les buffers référencés par iov.
1093    let ret = unsafe {
1094        syscall6(
1095            nr::PWRITEV,
1096            fd_to_u64(fd.as_raw_fd()),
1097            iov.as_ptr() as u64,
1098            iov.len() as u64,
1099            off_lo,
1100            off_hi,
1101            0,
1102        )
1103    };
1104
1105    if ret < 0 {
1106        return Err(errno_from_negative_syscall_ret(ret));
1107    }
1108
1109    #[allow(clippy::cast_sign_loss)]
1110    ret_to_usize_ok(ret)
1111}
1112
1113// ─────────────────────────────────────────────────────────────────────────
1114// Positionnement
1115// ─────────────────────────────────────────────────────────────────────────
1116
1117/// Modifie la position courante d'un FD seekable.
1118///
1119/// Wrappeur de `lseek(2)`. Retourne la nouvelle position absolue.
1120///
1121/// # Parameters
1122///
1123/// - `fd` : FD seekable.
1124/// - `offset` : déplacement.
1125/// - `whence` : origine ([`SeekWhence::Set`], `Current`, `End`, `Data`, `Hole`).
1126///
1127/// # Errors
1128///
1129/// - `ESPIPE` : FD non seekable (pipe, socket).
1130/// - `EINVAL` : position résultante négative.
1131///
1132/// # Examples
1133///
1134/// ```no_run
1135/// use air_sys_syscall::fs::lseek;
1136/// use air_sys_types::fs::SeekWhence;
1137/// use air_sys_types::fd::BorrowedFd;
1138///
1139/// # fn example(fd: BorrowedFd<'_>) {
1140/// let pos = lseek(fd, 0, SeekWhence::End).expect("lseek");
1141/// # }
1142/// ```
1143pub fn lseek(fd: BorrowedFd<'_>, offset: i64, whence: SeekWhence) -> Result<u64, Errno> {
1144    // SAFETY: lseek(2) ne touche aucune mémoire utilisateur.
1145    let ret = unsafe {
1146        syscall3(
1147            nr::LSEEK,
1148            fd_to_u64(fd.as_raw_fd()),
1149            i64_to_u64(offset),
1150            i32_to_u64(whence as i32),
1151        )
1152    };
1153
1154    if ret < 0 {
1155        return Err(errno_from_negative_syscall_ret(ret));
1156    }
1157
1158    // La nouvelle position est non-négative.
1159    #[allow(clippy::cast_sign_loss)]
1160    #[allow(clippy::cast_sign_loss)]
1161    Ok(ret as u64)
1162}
1163
1164// ─────────────────────────────────────────────────────────────────────────
1165// Métadonnées
1166// ─────────────────────────────────────────────────────────────────────────
1167
1168/// Lit les métadonnées d'un fichier (API unifiée et moderne).
1169///
1170/// Wrappeur de `statx(2)` (Linux 4.11+, numéro x86_64: 332, aarch64: 291).
1171/// Remplace tous les anciens `stat`/`fstat`/`lstat`/`fstatat` — seul
1172/// wrapper de métadonnées exposé par Air.
1173///
1174/// Le paramètre `mask` sélectionne les champs souhaités ;
1175/// `StatxResult::mask` en sortie indique les champs effectivement
1176/// disponibles (sous-ensemble possible si le FS ne les supporte pas).
1177///
1178/// # Parameters
1179///
1180/// - `dirfd` : base de résolution.
1181/// - `path` : chemin (peut être vide avec `StatxFlags::EMPTY_PATH` pour
1182///   opérer directement sur le FD).
1183/// - `flags` : comportement de résolution.
1184/// - `mask` : champs à récupérer.
1185///
1186/// # Errors
1187///
1188/// - `ENOENT` : chemin inexistant.
1189/// - `EACCES` : permissions insuffisantes.
1190/// - `ENOSYS` : kernel < 4.11.
1191///
1192/// # Examples
1193///
1194/// ```no_run
1195/// use air_sys_syscall::fs::statx;
1196/// use air_sys_types::fs::{DirFd, StatxFlags, StatxMask};
1197///
1198/// let result = statx(
1199///     DirFd::Cwd,
1200///     c"./config.toml",
1201///     StatxFlags::empty(),
1202///     StatxMask::SIZE | StatxMask::MTIME,
1203/// ).expect("statx");
1204/// if result.mask.contains(StatxMask::SIZE) {
1205///     println!("size: {}", result.size);
1206/// }
1207/// ```
1208pub fn statx(
1209    dirfd: DirFd<'_>,
1210    path: &CStr,
1211    flags: StatxFlags,
1212    mask: StatxMask,
1213) -> Result<StatxResult, Errno> {
1214    let mut kstatx = KernelStatx {
1215        stx_mask: 0,
1216        stx_blksize: 0,
1217        stx_attributes: 0,
1218        stx_nlink: 0,
1219        stx_uid: 0,
1220        stx_gid: 0,
1221        stx_mode: 0,
1222        __spare0: [0],
1223        stx_ino: 0,
1224        stx_size: 0,
1225        stx_blocks: 0,
1226        stx_attributes_mask: 0,
1227        stx_atime: KernelStatxTimestamp {
1228            tv_sec: 0,
1229            tv_nsec: 0,
1230            __reserved: 0,
1231        },
1232        stx_btime: KernelStatxTimestamp {
1233            tv_sec: 0,
1234            tv_nsec: 0,
1235            __reserved: 0,
1236        },
1237        stx_ctime: KernelStatxTimestamp {
1238            tv_sec: 0,
1239            tv_nsec: 0,
1240            __reserved: 0,
1241        },
1242        stx_mtime: KernelStatxTimestamp {
1243            tv_sec: 0,
1244            tv_nsec: 0,
1245            __reserved: 0,
1246        },
1247        stx_rdev_major: 0,
1248        stx_rdev_minor: 0,
1249        stx_dev_major: 0,
1250        stx_dev_minor: 0,
1251        stx_mnt_id: 0,
1252        __spare2: 0,
1253        __spare3: [0u64; 12],
1254    };
1255
1256    let raw_dirfd = dirfd_to_raw(dirfd);
1257    let path_ptr = path.as_ptr() as u64;
1258    let buf_ptr = &mut kstatx as *mut KernelStatx as u64;
1259
1260    // SAFETY:
1261    // - path_ptr est un CStr nul-terminé valide.
1262    // - buf_ptr pointe sur KernelStatx local correctement dimensionné (256 octets).
1263    // - Le kernel remplit la structure si le syscall réussit.
1264    let ret = unsafe {
1265        syscall5(
1266            nr::STATX,
1267            fd_to_u64(raw_dirfd),
1268            path_ptr,
1269            u64::from(flags.bits()),
1270            u64::from(mask.bits()),
1271            buf_ptr,
1272        )
1273    };
1274
1275    if ret < 0 {
1276        return Err(errno_from_negative_syscall_ret(ret));
1277    }
1278
1279    let ts_from = |ts: &KernelStatxTimestamp| StatxTimestamp {
1280        seconds: ts.tv_sec,
1281        nanoseconds: ts.tv_nsec,
1282    };
1283
1284    Ok(StatxResult {
1285        mask: StatxMask::from_bits_truncate(kstatx.stx_mask),
1286        blksize: kstatx.stx_blksize,
1287        attributes: kstatx.stx_attributes,
1288        nlink: kstatx.stx_nlink,
1289        uid: kstatx.stx_uid,
1290        gid: kstatx.stx_gid,
1291        mode: kstatx.stx_mode,
1292        ino: kstatx.stx_ino,
1293        size: kstatx.stx_size,
1294        blocks: kstatx.stx_blocks,
1295        atime: ts_from(&kstatx.stx_atime),
1296        btime: ts_from(&kstatx.stx_btime),
1297        ctime: ts_from(&kstatx.stx_ctime),
1298        mtime: ts_from(&kstatx.stx_mtime),
1299        rdev_major: kstatx.stx_rdev_major,
1300        rdev_minor: kstatx.stx_rdev_minor,
1301        dev_major: kstatx.stx_dev_major,
1302        dev_minor: kstatx.stx_dev_minor,
1303        mount_id: kstatx.stx_mnt_id,
1304    })
1305}
1306
1307/// Teste les permissions d'accès à un fichier sans l'ouvrir.
1308///
1309/// Wrappeur de `faccessat(2)`. **Attention :** sujet à des races
1310/// TOCTOU — préférer ouvrir directement et gérer l'erreur.
1311///
1312/// # Routage par flags (faccessat vs faccessat2)
1313///
1314/// Le syscall noyau `faccessat` historique ne prend **que 3 arguments**
1315/// (`dirfd`, `path`, `mode`) : il **ignore silencieusement** tout flag
1316/// comportemental. Honorer `AT_SYMLINK_NOFOLLOW` / `AT_EACCESS` exige le
1317/// syscall `faccessat2` (numéro 439 sur x86_64 **et** aarch64, Linux 5.8+,
1318/// 4 arguments). Comme la glibc, ce wrapper route donc selon `flags` :
1319///
1320/// - `flags.is_empty()` → `faccessat` (3-arg, universellement disponible) ;
1321/// - `flags` non vide → `faccessat2` (4-arg, qui **applique** réellement les
1322///   flags).
1323///
1324/// La signature publique est inchangée ; seul le syscall sous-jacent diffère.
1325/// Sur un noyau < 5.8 sans `faccessat2`, un appel avec flags retourne `ENOSYS`
1326/// (l'appelant n'obtient jamais un résultat où le flag aurait été ignoré).
1327///
1328/// # Errors
1329///
1330/// - `EACCES` : accès refusé.
1331/// - `ENOENT` : chemin inexistant.
1332/// - `ENOSYS` : `faccessat2` indisponible (noyau < 5.8) et `flags` non vide.
1333///
1334/// # Examples
1335///
1336/// ```no_run
1337/// use air_sys_syscall::fs::faccessat;
1338/// use air_sys_types::fs::{AccessFlags, AccessMode, DirFd};
1339///
1340/// let result = faccessat(DirFd::Cwd, c"./script.sh", AccessMode::X_OK, AccessFlags::empty());
1341/// ```
1342pub fn faccessat(
1343    dirfd: DirFd<'_>,
1344    path: &CStr,
1345    mode: AccessMode,
1346    flags: AccessFlags,
1347) -> Result<(), Errno> {
1348    let raw_dirfd = dirfd_to_raw(dirfd);
1349
1350    let ret = if flags.is_empty() {
1351        // SAFETY: SYS_faccessat (x86_64 = 269) / SYS_faccessat (aarch64 = 48).
1352        // 3 arguments : (dirfd, path, mode). path est un CStr nul-terminé valide
1353        // pour la durée du syscall. Flags vides → le syscall 3-arg historique
1354        // suffit (il ne porte de toute façon aucun flag) et reste universellement
1355        // disponible.
1356        unsafe {
1357            syscall3(
1358                nr::FACCESSAT,
1359                fd_to_u64(raw_dirfd),
1360                path.as_ptr() as u64,
1361                u64::from(mode.bits()),
1362            )
1363        }
1364    } else {
1365        // SAFETY: SYS_faccessat2 (x86_64 = 439) / SYS_faccessat2 (aarch64 = 439).
1366        // 4 arguments : (dirfd, path, mode, flags). path est un CStr nul-terminé
1367        // valide pour la durée du syscall. C'est le seul syscall qui HONORE
1368        // réellement AT_SYMLINK_NOFOLLOW / AT_EACCESS — le 3-arg les ignore.
1369        unsafe {
1370            syscall4(
1371                nr::FACCESSAT2,
1372                fd_to_u64(raw_dirfd),
1373                path.as_ptr() as u64,
1374                u64::from(mode.bits()),
1375                i32_to_u64(flags.bits()),
1376            )
1377        }
1378    };
1379
1380    if ret < 0 {
1381        return Err(errno_from_negative_syscall_ret(ret));
1382    }
1383    Ok(())
1384}
1385
1386// ─────────────────────────────────────────────────────────────────────────
1387// Répertoires
1388// ─────────────────────────────────────────────────────────────────────────
1389
1390/// Crée un répertoire.
1391///
1392/// # Errors
1393///
1394/// - `EEXIST` : chemin existant.
1395/// - `ENOENT` : répertoire parent inexistant.
1396/// - `EACCES` : permissions insuffisantes.
1397///
1398/// # Examples
1399///
1400/// ```no_run
1401/// use air_sys_syscall::fs::mkdirat;
1402/// use air_sys_types::fs::DirFd;
1403///
1404/// mkdirat(DirFd::Cwd, c"./newdir", 0o755).expect("mkdirat");
1405/// ```
1406pub fn mkdirat(dirfd: DirFd<'_>, path: &CStr, mode: Mode) -> Result<(), Errno> {
1407    let raw_dirfd = dirfd_to_raw(dirfd);
1408
1409    // SAFETY: path_ptr est un CStr nul-terminé valide.
1410    let ret = unsafe {
1411        syscall3(
1412            nr::MKDIRAT,
1413            fd_to_u64(raw_dirfd),
1414            path.as_ptr() as u64,
1415            u64::from(mode),
1416        )
1417    };
1418
1419    if ret < 0 {
1420        return Err(errno_from_negative_syscall_ret(ret));
1421    }
1422    Ok(())
1423}
1424
1425/// Lit les entrées d'un répertoire.
1426///
1427/// Wrappeur de `getdents64(2)`. Le paramètre `buffer` est un buffer brut
1428/// dont le format binaire est parsé en interne. Utiliser
1429/// [`parse_dirents`] pour itérer sur les entrées.
1430///
1431/// **Allocation intrinsèque :** le buffer est alloué par l'appelant (pas
1432/// de contrainte sur la taille, mais 4096+ octets est recommandé).
1433///
1434/// # Parameters
1435///
1436/// - `fd` : FD d'un répertoire ouvert avec `DIRECTORY`.
1437/// - `buffer` : buffer de lecture (au moins quelques Ko recommandés).
1438///
1439/// # Errors
1440///
1441/// - `EINVAL` : buffer trop petit pour contenir une entrée.
1442/// - `ENOTDIR` : FD n'est pas un répertoire.
1443/// - `EBADF` : FD invalide.
1444///
1445/// # Examples
1446///
1447/// ```no_run
1448/// use air_sys_syscall::fs::{getdents64, parse_dirents};
1449/// use air_sys_types::fd::BorrowedFd;
1450///
1451/// # fn example(dir_fd: BorrowedFd<'_>) {
1452/// let mut buffer = vec![0u8; 4096];
1453/// let n = getdents64(dir_fd, &mut buffer).expect("getdents64");
1454/// for entry in parse_dirents(&buffer[..n]) {
1455///     println!("{:?}", entry.name);
1456/// }
1457/// # }
1458/// ```
1459pub fn getdents64(fd: BorrowedFd<'_>, buffer: &mut [u8]) -> Result<usize, Errno> {
1460    // SAFETY:
1461    // - buffer est un slice mutable valide pour la durée du syscall.
1462    // - getdents64 écrit au plus buffer.len() octets dans buffer.
1463    let ret = unsafe {
1464        syscall3(
1465            nr::GETDENTS64,
1466            fd_to_u64(fd.as_raw_fd()),
1467            buffer.as_mut_ptr() as u64,
1468            buffer.len() as u64,
1469        )
1470    };
1471
1472    if ret < 0 {
1473        return Err(errno_from_negative_syscall_ret(ret));
1474    }
1475
1476    #[allow(clippy::cast_sign_loss)]
1477    ret_to_usize_ok(ret)
1478}
1479
1480/// Itère sur les entrées de répertoire depuis un buffer `getdents64`.
1481///
1482/// Retourne un itérateur sur les [`DirEntry`] parsées depuis `buffer`.
1483/// Le buffer doit être valide pour toute la durée de l'itération.
1484///
1485/// # Examples
1486///
1487/// ```no_run
1488/// use air_sys_syscall::fs::parse_dirents;
1489///
1490/// # fn example(buffer: &[u8]) {
1491/// for entry in parse_dirents(buffer) {
1492///     println!("inode={} name={:?}", entry.inode, entry.name);
1493/// }
1494/// # }
1495/// ```
1496pub fn parse_dirents(buffer: &[u8]) -> impl Iterator<Item = DirEntry> + '_ {
1497    ParseDirents {
1498        buf: buffer,
1499        offset: 0,
1500    }
1501}
1502
1503/// Itérateur sur les entrées `linux_dirent64` dans un buffer brut.
1504struct ParseDirents<'a> {
1505    buf: &'a [u8],
1506    offset: usize,
1507}
1508
1509impl<'a> Iterator for ParseDirents<'a> {
1510    type Item = DirEntry;
1511
1512    fn next(&mut self) -> Option<Self::Item> {
1513        if self.offset >= self.buf.len() {
1514            return None;
1515        }
1516
1517        let buffer = self.buf.get(self.offset..)?;
1518
1519        // struct linux_dirent64 header: ino(8) + offset(8) + reclen(2) + type(1) = 19 bytes minimum
1520        if buffer.len() < 19 {
1521            return None;
1522        }
1523
1524        let ino = u64::from_ne_bytes(buffer.get(0..8)?.try_into().ok()?);
1525        let offset = i64::from_ne_bytes(buffer.get(8..16)?.try_into().ok()?);
1526        let reclen = u16::from_ne_bytes(buffer.get(16..18)?.try_into().ok()?) as usize;
1527        let file_type_byte = *buffer.get(18)?;
1528
1529        if reclen == 0 || reclen > buffer.len() {
1530            return None;
1531        }
1532
1533        // Name starts at byte 19, null-terminated, within the record.
1534        let name_region = buffer.get(19..reclen)?;
1535        let nul_pos = name_region
1536            .iter()
1537            .position(|&b| b == 0)
1538            .unwrap_or(name_region.len());
1539        let name_bytes = name_region.get(..nul_pos)?;
1540
1541        let file_type = match file_type_byte {
1542            1 => DirEntryType::Fifo,
1543            2 => DirEntryType::Character,
1544            4 => DirEntryType::Directory,
1545            6 => DirEntryType::Block,
1546            8 => DirEntryType::Regular,
1547            10 => DirEntryType::Symlink,
1548            12 => DirEntryType::Socket,
1549            _ => DirEntryType::Unknown,
1550        };
1551
1552        // Nom = octets bruts (sans le `NUL`), aucune présomption d'encodage
1553        // (Principe 3, doctrine « chemins = octets ») : copie propriétaire.
1554        let name = name_bytes.to_vec();
1555
1556        self.offset = self.offset.checked_add(reclen)?;
1557
1558        Some(DirEntry {
1559            inode: ino,
1560            offset,
1561            file_type,
1562            name,
1563        })
1564    }
1565}
1566
1567/// Supprime un fichier ou un répertoire vide.
1568///
1569/// Wrappeur de `unlinkat(2)`. Passer `AT_REMOVEDIR` dans `flags` pour
1570/// supprimer un répertoire (équivalent à `rmdir`).
1571///
1572/// # Errors
1573///
1574/// - `ENOENT` : fichier inexistant.
1575/// - `ENOTEMPTY` / `EEXIST` : répertoire non vide (avec `AT_REMOVEDIR`).
1576/// - `EACCES` : permissions insuffisantes.
1577///
1578/// # Examples
1579///
1580/// ```no_run
1581/// use air_sys_syscall::fs::unlinkat;
1582/// use air_sys_types::fs::DirFd;
1583///
1584/// unlinkat(DirFd::Cwd, c"./old.tmp", 0).expect("unlinkat");
1585/// ```
1586pub fn unlinkat(dirfd: DirFd<'_>, path: &CStr, flags: i32) -> Result<(), Errno> {
1587    let raw_dirfd = dirfd_to_raw(dirfd);
1588
1589    // SAFETY: path_ptr est un CStr nul-terminé valide.
1590    let ret = unsafe {
1591        syscall3(
1592            nr::UNLINKAT,
1593            fd_to_u64(raw_dirfd),
1594            path.as_ptr() as u64,
1595            i32_to_u64(flags),
1596        )
1597    };
1598
1599    if ret < 0 {
1600        return Err(errno_from_negative_syscall_ret(ret));
1601    }
1602    Ok(())
1603}
1604
1605/// Renomme (et éventuellement échange) des entrées filesystem.
1606///
1607/// Wrappeur de `renameat2(2)` (Linux 3.15+). `RenameFlags::EXCHANGE`
1608/// permet un échange atomique de deux entrées.
1609///
1610/// # Errors
1611///
1612/// - `EEXIST` : destination existante avec `NOREPLACE`.
1613/// - `ENOENT` : source inexistante.
1614/// - `EXDEV` : source et destination sur des FS différents.
1615///
1616/// # Examples
1617///
1618/// ```no_run
1619/// use air_sys_syscall::fs::renameat2;
1620/// use air_sys_types::fs::{DirFd, RenameFlags};
1621///
1622/// renameat2(DirFd::Cwd, c"./new.tmp", DirFd::Cwd, c"./current", RenameFlags::NOREPLACE)
1623///     .expect("renameat2");
1624/// ```
1625pub fn renameat2(
1626    old_dirfd: DirFd<'_>,
1627    old_path: &CStr,
1628    new_dirfd: DirFd<'_>,
1629    new_path: &CStr,
1630    flags: RenameFlags,
1631) -> Result<(), Errno> {
1632    let raw_old = dirfd_to_raw(old_dirfd);
1633    let raw_new = dirfd_to_raw(new_dirfd);
1634
1635    // SAFETY: old_path et new_path sont des CStr nul-terminés valides.
1636    let ret = unsafe {
1637        syscall5(
1638            nr::RENAMEAT2,
1639            fd_to_u64(raw_old),
1640            old_path.as_ptr() as u64,
1641            fd_to_u64(raw_new),
1642            new_path.as_ptr() as u64,
1643            u64::from(flags.bits()),
1644        )
1645    };
1646
1647    if ret < 0 {
1648        return Err(errno_from_negative_syscall_ret(ret));
1649    }
1650    Ok(())
1651}
1652
1653/// Crée un lien physique.
1654pub fn linkat(
1655    old_dirfd: DirFd<'_>,
1656    old_path: &CStr,
1657    new_dirfd: DirFd<'_>,
1658    new_path: &CStr,
1659    flags: i32,
1660) -> Result<(), Errno> {
1661    let raw_old = dirfd_to_raw(old_dirfd);
1662    let raw_new = dirfd_to_raw(new_dirfd);
1663
1664    // SAFETY: les deux CStr sont nul-terminés et valides pour la durée du syscall.
1665    let ret = unsafe {
1666        syscall5(
1667            nr::LINKAT,
1668            fd_to_u64(raw_old),
1669            old_path.as_ptr() as u64,
1670            fd_to_u64(raw_new),
1671            new_path.as_ptr() as u64,
1672            i32_to_u64(flags),
1673        )
1674    };
1675
1676    if ret < 0 {
1677        return Err(errno_from_negative_syscall_ret(ret));
1678    }
1679    Ok(())
1680}
1681
1682/// Crée un lien symbolique.
1683pub fn symlinkat(target: &CStr, new_dirfd: DirFd<'_>, link_path: &CStr) -> Result<(), Errno> {
1684    let raw_new = dirfd_to_raw(new_dirfd);
1685
1686    // SAFETY: target et link_path sont des CStr nul-terminés valides.
1687    let ret = unsafe {
1688        syscall3(
1689            nr::SYMLINKAT,
1690            target.as_ptr() as u64,
1691            fd_to_u64(raw_new),
1692            link_path.as_ptr() as u64,
1693        )
1694    };
1695
1696    if ret < 0 {
1697        return Err(errno_from_negative_syscall_ret(ret));
1698    }
1699    Ok(())
1700}
1701
1702/// Lit la cible d'un lien symbolique.
1703///
1704/// **Allocation heap :** le résultat est une `Vec<u8>` car la longueur
1705/// n'est connue qu'à l'exécution (nécessité documentée, ADR-021
1706/// convention 4).
1707///
1708/// # Errors
1709///
1710/// - `ENOENT` : lien inexistant.
1711/// - `EINVAL` : le chemin n'est pas un lien symbolique.
1712///
1713/// # Examples
1714///
1715/// ```no_run
1716/// use air_sys_syscall::fs::readlinkat;
1717/// use air_sys_types::fs::DirFd;
1718///
1719/// let target = readlinkat(DirFd::Cwd, c"./link").expect("readlinkat");
1720/// ```
1721pub fn readlinkat(dirfd: DirFd<'_>, path: &CStr) -> Result<Vec<u8>, Errno> {
1722    // Commence avec PATH_MAX (4096). Si la cible est plus longue (rare),
1723    // on double le buffer et on réessaie.
1724    let mut buffer = vec![0u8; 4096];
1725    let raw_dirfd = dirfd_to_raw(dirfd);
1726
1727    loop {
1728        let cap = buffer.len();
1729
1730        // SAFETY:
1731        // - path est un CStr nul-terminé valide.
1732        // - buffer est un Vec mutable de taille `cap`.
1733        // - readlinkat(2) écrit au plus `cap` octets (sans NUL terminal).
1734        let ret = unsafe {
1735            syscall4(
1736                nr::READLINKAT,
1737                fd_to_u64(raw_dirfd),
1738                path.as_ptr() as u64,
1739                buffer.as_mut_ptr() as u64,
1740                cap as u64,
1741            )
1742        };
1743
1744        if ret < 0 {
1745            return Err(errno_from_negative_syscall_ret(ret));
1746        }
1747
1748        let n = ret_to_usize(ret);
1749
1750        if n < cap {
1751            // Résultat tient dans le buffer.
1752            buffer.truncate(n);
1753            return Ok(buffer);
1754        }
1755
1756        // Résultat peut-être tronqué : doubler le buffer et réessayer.
1757        // Checked pour éviter overflow sur des systèmes pathologiques.
1758        let new_cap = cap.checked_mul(2).ok_or_else(|| {
1759            Errno::from_nonzero(NonZeroI32::new(libc_enomem()).expect("ENOMEM non-zero"))
1760        })?;
1761        buffer.resize(new_cap, 0);
1762    }
1763}
1764
1765// ENOMEM = 12, utilisé uniquement dans le chemin d'erreur de readlinkat.
1766#[inline(always)]
1767fn libc_enomem() -> i32 {
1768    12
1769}
1770
1771/// Crée un nœud de périphérique, une FIFO ou un fichier régulier.
1772pub fn mknodat(dirfd: DirFd<'_>, path: &CStr, mode: Mode, dev: u64) -> Result<(), Errno> {
1773    let raw_dirfd = dirfd_to_raw(dirfd);
1774
1775    // SAFETY: path est un CStr nul-terminé valide.
1776    let ret = unsafe {
1777        syscall4(
1778            nr::MKNODAT,
1779            fd_to_u64(raw_dirfd),
1780            path.as_ptr() as u64,
1781            u64::from(mode),
1782            dev,
1783        )
1784    };
1785
1786    if ret < 0 {
1787        return Err(errno_from_negative_syscall_ret(ret));
1788    }
1789    Ok(())
1790}
1791
1792// ─────────────────────────────────────────────────────────────────────────
1793// Permissions
1794// ─────────────────────────────────────────────────────────────────────────
1795
1796/// Modifie les permissions d'un fichier.
1797pub fn fchmodat(dirfd: DirFd<'_>, path: &CStr, mode: Mode, flags: i32) -> Result<(), Errno> {
1798    let raw_dirfd = dirfd_to_raw(dirfd);
1799
1800    // SAFETY: path est un CStr nul-terminé valide.
1801    let ret = unsafe {
1802        syscall4(
1803            nr::FCHMODAT,
1804            fd_to_u64(raw_dirfd),
1805            path.as_ptr() as u64,
1806            u64::from(mode),
1807            i32_to_u64(flags),
1808        )
1809    };
1810
1811    if ret < 0 {
1812        return Err(errno_from_negative_syscall_ret(ret));
1813    }
1814    Ok(())
1815}
1816
1817/// Modifie les permissions d'un fichier **en honorant les drapeaux `AT_*`**.
1818///
1819/// Wrappeur de `fchmodat2(2)` (numéro 452 sur x86_64 **et** aarch64, Linux ≥ 6.6).
1820///
1821/// # Pourquoi un second wrapper de `chmod` par chemin
1822///
1823/// Le syscall noyau `fchmodat` historique ne prend que **3 arguments**
1824/// (`dirfd`, `path`, `mode`) : la couche C ajoute bien un paramètre `flags` dans
1825/// sa signature, mais le noyau ne le reçoit jamais — `AT_SYMLINK_NOFOLLOW` y est
1826/// donc **silencieusement ignoré**, et le chemin est toujours re-résolu en
1827/// **suivant** le lien symbolique final. C'est le même piège que
1828/// `faccessat` / [`faccessat2`], résolu de la même façon : un syscall à 4
1829/// arguments qui, lui, **applique** les drapeaux.
1830///
1831/// [`ChmodFlags::SYMLINK_NOFOLLOW`] ferme donc une fenêtre TOCTOU : entre le
1832/// moment où l'on crée un nœud (un socket Unix, par exemple) et celui où l'on
1833/// restreint ses permissions, un tiers ayant droit d'écriture sur le répertoire
1834/// conteneur peut substituer un lien symbolique et détourner le `chmod` vers un
1835/// fichier tiers. Avec ce drapeau, la substitution ne réussit pas : Linux ne sait
1836/// pas changer les permissions d'un lien symbolique et rend `EOPNOTSUPP`.
1837/// L'appelant obtient donc, en plus du `chmod`, la preuve que le chemin
1838/// désignait bien le nœud attendu — ou une erreur qu'il peut traiter.
1839///
1840/// `EINTR` est remonté tel quel : aucun retry automatique en couche 0 (ADR-021).
1841///
1842/// # Errors
1843///
1844/// - `EOPNOTSUPP` : `SYMLINK_NOFOLLOW` demandé et le chemin final est un lien
1845///   symbolique (Linux ne modifie pas les permissions d'un lien).
1846/// - `ENOENT` : chemin inexistant.
1847/// - `EPERM` / `EACCES` : pas le droit de modifier ce nœud.
1848/// - `ENOSYS` : noyau < 6.6 (Air cible 6.12+, cf. ADR-004).
1849///
1850/// # Examples
1851///
1852/// ```no_run
1853/// use air_sys_syscall::fs::fchmodat2;
1854/// use air_sys_types::fs::{ChmodFlags, DirFd};
1855///
1856/// // Restreint le socket que nous venons de lier — sans jamais suivre un lien.
1857/// let result = fchmodat2(DirFd::Cwd, c"/run/air/forward.sock", 0o600, ChmodFlags::SYMLINK_NOFOLLOW);
1858/// ```
1859pub fn fchmodat2(
1860    dirfd: DirFd<'_>,
1861    path: &CStr,
1862    mode: Mode,
1863    flags: ChmodFlags,
1864) -> Result<(), Errno> {
1865    let raw_dirfd = dirfd_to_raw(dirfd);
1866
1867    // SAFETY: SYS_fchmodat2 (x86_64 = 452) / SYS_fchmodat2 (aarch64 = 452).
1868    // 4 arguments : (dirfd, path, mode, flags). path est un CStr nul-terminé valide
1869    // pour la durée du syscall. C'est le seul syscall de chmod par chemin qui
1870    // HONORE réellement AT_SYMLINK_NOFOLLOW — le 3-arg fchmodat l'ignore.
1871    let ret = unsafe {
1872        syscall4(
1873            nr::FCHMODAT2,
1874            fd_to_u64(raw_dirfd),
1875            path.as_ptr() as u64,
1876            u64::from(mode),
1877            i32_to_u64(flags.bits()),
1878        )
1879    };
1880
1881    if ret < 0 {
1882        return Err(errno_from_negative_syscall_ret(ret));
1883    }
1884    Ok(())
1885}
1886
1887/// Modifie le propriétaire d'un fichier.
1888pub fn fchownat(
1889    dirfd: DirFd<'_>,
1890    path: &CStr,
1891    uid: u32,
1892    gid: u32,
1893    flags: i32,
1894) -> Result<(), Errno> {
1895    let raw_dirfd = dirfd_to_raw(dirfd);
1896
1897    // SAFETY: path est un CStr nul-terminé valide.
1898    let ret = unsafe {
1899        syscall5(
1900            nr::FCHOWNAT,
1901            fd_to_u64(raw_dirfd),
1902            path.as_ptr() as u64,
1903            u64::from(uid),
1904            u64::from(gid),
1905            i32_to_u64(flags),
1906        )
1907    };
1908
1909    if ret < 0 {
1910        return Err(errno_from_negative_syscall_ret(ret));
1911    }
1912    Ok(())
1913}
1914
1915/// Modifie les horodatages d'accès et de modification d'un fichier.
1916///
1917/// # Parameters
1918///
1919/// - `atime` : nouvel horodatage d'accès (`Now`, `Omit`, ou valeur).
1920/// - `mtime` : nouvel horodatage de modification.
1921///
1922/// # Examples
1923///
1924/// ```no_run
1925/// use air_sys_syscall::fs::utimensat;
1926/// use air_sys_types::fs::{DirFd, UtimeValue};
1927///
1928/// utimensat(DirFd::Cwd, c"./file", UtimeValue::Now, UtimeValue::Omit, 0)
1929///     .expect("utimensat");
1930/// ```
1931pub fn utimensat(
1932    dirfd: DirFd<'_>,
1933    path: &CStr,
1934    atime: UtimeValue,
1935    mtime: UtimeValue,
1936    flags: i32,
1937) -> Result<(), Errno> {
1938    let raw_dirfd = dirfd_to_raw(dirfd);
1939
1940    // struct timespec : [i64; 2] = [tv_sec, tv_nsec]
1941    let atime_ts = utimevalue_to_kernel(atime);
1942    let mtime_ts = utimevalue_to_kernel(mtime);
1943    // Paire consécutive en mémoire, comme struct timespec[2].
1944    let times: [[i64; 2]; 2] = [atime_ts, mtime_ts];
1945
1946    // SAFETY:
1947    // - path est un CStr nul-terminé valide.
1948    // - times est un tableau de deux struct timespec (2×[i64;2]) sur la pile, valide.
1949    // - utimensat(2) lit les deux entrées de times pour mettre à jour les horodatages.
1950    let ret = unsafe {
1951        syscall4(
1952            nr::UTIMENSAT,
1953            fd_to_u64(raw_dirfd),
1954            path.as_ptr() as u64,
1955            times.as_ptr() as u64,
1956            i32_to_u64(flags),
1957        )
1958    };
1959
1960    if ret < 0 {
1961        return Err(errno_from_negative_syscall_ret(ret));
1962    }
1963    Ok(())
1964}
1965
1966/// Modifie les permissions d'un fichier **par descripteur** (`fchmod(2)`) —
1967/// **descellement additif [ADR-085](../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md)**
1968/// pour la face libc (`int fchmod(int, mode_t)`). Complète `fchmodat` (par chemin).
1969pub fn fchmod(fd: BorrowedFd<'_>, mode: Mode) -> Result<(), Errno> {
1970    // SAFETY: fchmod(2) ne touche aucune mémoire utilisateur.
1971    let ret = unsafe { syscall2(nr::FCHMOD, fd_to_u64(fd.as_raw_fd()), u64::from(mode)) };
1972
1973    if ret < 0 {
1974        return Err(errno_from_negative_syscall_ret(ret));
1975    }
1976    Ok(())
1977}
1978
1979/// Modifie le propriétaire d'un fichier **par descripteur** (`fchown(2)`) —
1980/// descellement additif [ADR-085] pour la face libc (`int fchown(int, uid_t, gid_t)`).
1981/// Complète `fchownat` (par chemin).
1982///
1983/// [ADR-085]: ../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md
1984pub fn fchown(fd: BorrowedFd<'_>, uid: u32, gid: u32) -> Result<(), Errno> {
1985    // SAFETY: fchown(2) ne touche aucune mémoire utilisateur.
1986    let ret = unsafe {
1987        syscall3(
1988            nr::FCHOWN,
1989            fd_to_u64(fd.as_raw_fd()),
1990            u64::from(uid),
1991            u64::from(gid),
1992        )
1993    };
1994
1995    if ret < 0 {
1996        return Err(errno_from_negative_syscall_ret(ret));
1997    }
1998    Ok(())
1999}
2000
2001/// Modifie les horodatages d'accès/modification d'un fichier **par descripteur**
2002/// (`utimensat(fd, NULL, …)`) — descellement additif [ADR-085] pour la face libc
2003/// (`int futimens(int, const struct timespec[2])`). Complète `utimensat` (dont la
2004/// variante couche 0 exige un `&CStr`, jamais `NULL`).
2005///
2006/// [ADR-085]: ../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md
2007pub fn futimens(fd: BorrowedFd<'_>, atime: UtimeValue, mtime: UtimeValue) -> Result<(), Errno> {
2008    // struct timespec[2] : [tv_sec, tv_nsec] × 2, consécutifs en mémoire.
2009    let times: [[i64; 2]; 2] = [utimevalue_to_kernel(atime), utimevalue_to_kernel(mtime)];
2010
2011    // SAFETY:
2012    // - pointeur de chemin **NULL** (0) : `utimensat` opère alors sur le fd (`dirfd`
2013    //   emprunté ci-dessus), sémantique exacte de `futimens(3)`.
2014    // - times est un tableau de deux struct timespec sur la pile, valide et lu par le
2015    //   kernel pour poser les horodatages.
2016    let ret = unsafe {
2017        syscall4(
2018            nr::UTIMENSAT,
2019            fd_to_u64(fd.as_raw_fd()),
2020            0, // path == NULL ⇒ opère sur le descripteur
2021            times.as_ptr() as u64,
2022            0, // flags
2023        )
2024    };
2025
2026    if ret < 0 {
2027        return Err(errno_from_negative_syscall_ret(ret));
2028    }
2029    Ok(())
2030}
2031
2032/// Transfert **zéro-copie** kernel de `count` octets de `in_fd` vers `out_fd`
2033/// (`sendfile(2)`) — descellement additif [ADR-085] pour la face libc. `offset` :
2034/// `Some` = position de départ **explicite** (que le kernel met à jour ; la position de
2035/// `in_fd` reste inchangée) ; `None` = position **courante** de `in_fd` (que le kernel
2036/// avance). Rend le nombre d'octets **effectivement** transférés (`0` = fin de `in_fd`,
2037/// transfert possiblement partiel — l'appelant boucle).
2038///
2039/// [ADR-085]: ../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md
2040pub fn sendfile(
2041    out_fd: BorrowedFd<'_>,
2042    in_fd: BorrowedFd<'_>,
2043    offset: Option<&mut u64>,
2044    count: usize,
2045) -> Result<usize, Errno> {
2046    // `Some` ⇒ pointeur vers l'offset (le kernel le lit **et** y réécrit la nouvelle
2047    // position) ; `None` ⇒ `NULL` (le kernel utilise/avance la position de `in_fd`).
2048    let offset_ptr = match offset {
2049        Some(value) => core::ptr::from_mut(value) as u64,
2050        None => 0,
2051    };
2052    // SAFETY:
2053    // - `out_fd`/`in_fd` sont des `BorrowedFd` valides le temps de l'appel.
2054    // - `offset_ptr` est nul, **ou** pointe un `u64` valide lu/écrit par le kernel.
2055    // - `sendfile(2)` ne touche aucune autre mémoire utilisateur.
2056    let ret = unsafe {
2057        syscall4(
2058            nr::SENDFILE,
2059            fd_to_u64(out_fd.as_raw_fd()),
2060            fd_to_u64(in_fd.as_raw_fd()),
2061            offset_ptr,
2062            count as u64,
2063        )
2064    };
2065
2066    if ret < 0 {
2067        return Err(errno_from_negative_syscall_ret(ret));
2068    }
2069    // `ret >= 0` : octets transférés (au plus `count <= usize::MAX`).
2070    #[allow(clippy::cast_sign_loss)]
2071    ret_to_usize_ok(ret)
2072}
2073
2074fn utimevalue_to_kernel(v: UtimeValue) -> [i64; 2] {
2075    match v {
2076        UtimeValue::Now => [0, UTIME_NOW],
2077        UtimeValue::Omit => [0, UTIME_OMIT],
2078        UtimeValue::Time(ts) => [ts.seconds, i64::from(ts.nanoseconds)],
2079    }
2080}
2081
2082// ─────────────────────────────────────────────────────────────────────────
2083// Opérations spéciales
2084// ─────────────────────────────────────────────────────────────────────────
2085
2086/// Tronque un fichier à une longueur donnée via un FD.
2087///
2088/// # Errors
2089///
2090/// - `EINVAL` : longueur négative.
2091/// - `EBADF` : FD invalide ou non ouvert en écriture.
2092///
2093/// # Examples
2094///
2095/// ```no_run
2096/// use air_sys_syscall::fs::ftruncate;
2097/// use air_sys_types::fd::BorrowedFd;
2098///
2099/// # fn example(fd: BorrowedFd<'_>) {
2100/// ftruncate(fd, 4096).expect("ftruncate");
2101/// # }
2102/// ```
2103pub fn ftruncate(fd: BorrowedFd<'_>, length: i64) -> Result<(), Errno> {
2104    // SAFETY: ftruncate(2) ne touche aucune mémoire utilisateur.
2105    let ret = unsafe { syscall2(nr::FTRUNCATE, fd_to_u64(fd.as_raw_fd()), i64_to_u64(length)) };
2106
2107    if ret < 0 {
2108        return Err(errno_from_negative_syscall_ret(ret));
2109    }
2110    Ok(())
2111}
2112
2113/// Tronque un fichier via son chemin.
2114///
2115/// Note: utilise `openat` + `ftruncate` car `truncate(2)` est un syscall
2116/// legacy non exposé directement (chemin basé). Préférer `ftruncate`.
2117pub fn truncate(dirfd: DirFd<'_>, path: &CStr, length: i64) -> Result<(), Errno> {
2118    use air_sys_types::fs::OpenFlags;
2119    let fd = openat(dirfd, path, OpenFlags::WRONLY, 0)?;
2120    // SAFETY: fd vient d'être ouvert avec succès, BorrowedFd depuis OwnedFd.
2121    let borrowed = unsafe { BorrowedFd::borrow_raw(fd.as_raw_fd()) };
2122    let result = ftruncate(borrowed, length);
2123    drop(fd);
2124    result
2125}
2126
2127/// Force l'écriture des données et métadonnées d'un FD sur le support.
2128///
2129/// # Errors
2130///
2131/// - `EIO` : erreur d'E/S lors de la synchronisation.
2132///
2133/// # Examples
2134///
2135/// ```no_run
2136/// use air_sys_syscall::fs::fsync;
2137/// use air_sys_types::fd::BorrowedFd;
2138///
2139/// # fn example(fd: BorrowedFd<'_>) {
2140/// fsync(fd).expect("fsync");
2141/// # }
2142/// ```
2143pub fn fsync(fd: BorrowedFd<'_>) -> Result<(), Errno> {
2144    // SAFETY: fsync(2) ne touche aucune mémoire utilisateur.
2145    let ret = unsafe { syscall1(nr::FSYNC, fd_to_u64(fd.as_raw_fd())) };
2146
2147    if ret < 0 {
2148        return Err(errno_from_negative_syscall_ret(ret));
2149    }
2150    Ok(())
2151}
2152
2153/// Force l'écriture des données d'un FD (sans les métadonnées non essentielles).
2154pub fn fdatasync(fd: BorrowedFd<'_>) -> Result<(), Errno> {
2155    // SAFETY: fdatasync(2) ne touche aucune mémoire utilisateur.
2156    let ret = unsafe { syscall1(nr::FDATASYNC, fd_to_u64(fd.as_raw_fd())) };
2157
2158    if ret < 0 {
2159        return Err(errno_from_negative_syscall_ret(ret));
2160    }
2161    Ok(())
2162}
2163
2164/// Pré-alloue ou manipule l'espace d'un fichier.
2165///
2166/// # Errors
2167///
2168/// - `EOPNOTSUPP` : opération non supportée par le filesystem.
2169/// - `ENOSPC` : espace insuffisant.
2170///
2171/// # Examples
2172///
2173/// ```no_run
2174/// use air_sys_syscall::fs::fallocate;
2175/// use air_sys_types::fs::FallocateMode;
2176/// use air_sys_types::fd::BorrowedFd;
2177///
2178/// # fn example(fd: BorrowedFd<'_>) {
2179/// // Pré-allouer 1 Mo sans modifier la taille visible
2180/// fallocate(fd, FallocateMode::KEEP_SIZE, 0, 1_048_576).expect("fallocate");
2181/// # }
2182/// ```
2183pub fn fallocate(
2184    fd: BorrowedFd<'_>,
2185    mode: FallocateMode,
2186    offset: i64,
2187    length: i64,
2188) -> Result<(), Errno> {
2189    // SAFETY: fallocate(2) ne touche aucune mémoire utilisateur.
2190    let ret = unsafe {
2191        syscall4(
2192            nr::FALLOCATE,
2193            fd_to_u64(fd.as_raw_fd()),
2194            i32_to_u64(mode.bits()),
2195            i64_to_u64(offset),
2196            i64_to_u64(length),
2197        )
2198    };
2199
2200    if ret < 0 {
2201        return Err(errno_from_negative_syscall_ret(ret));
2202    }
2203    Ok(())
2204}
2205
2206/// Copie zero-copy entre deux fichiers (même FS si possible).
2207///
2208/// Wrappeur de `copy_file_range(2)` (Linux 4.5+).
2209///
2210/// # Errors
2211///
2212/// - `EINVAL` : paramètres invalides.
2213/// - `EXDEV` : FS différents et copy_file_range inter-FS non supporté.
2214///
2215/// # Examples
2216///
2217/// ```no_run
2218/// use air_sys_syscall::fs::copy_file_range;
2219/// use air_sys_types::fd::BorrowedFd;
2220///
2221/// # fn example(src: BorrowedFd<'_>, dst: BorrowedFd<'_>) {
2222/// let n = copy_file_range(src, None, dst, None, 4096, 0)
2223///     .expect("copy_file_range");
2224/// # }
2225/// ```
2226pub fn copy_file_range(
2227    fd_in: BorrowedFd<'_>,
2228    offset_in: Option<&mut u64>,
2229    fd_out: BorrowedFd<'_>,
2230    offset_out: Option<&mut u64>,
2231    length: usize,
2232    flags: u32,
2233) -> Result<usize, Errno> {
2234    let off_in_ptr: u64 = match offset_in {
2235        Some(p) => p as *mut u64 as u64,
2236        None => 0,
2237    };
2238    let off_out_ptr: u64 = match offset_out {
2239        Some(p) => p as *mut u64 as u64,
2240        None => 0,
2241    };
2242
2243    // SAFETY:
2244    // - off_in_ptr et off_out_ptr sont soit NULL (0) soit des pointeurs sur u64 valides.
2245    // - copy_file_range lit/écrit éventuellement les offsets pointés.
2246    // - fd_in et fd_out sont des BorrowedFd valides.
2247    let ret = unsafe {
2248        syscall6(
2249            nr::COPY_FILE_RANGE,
2250            fd_to_u64(fd_in.as_raw_fd()),
2251            off_in_ptr,
2252            fd_to_u64(fd_out.as_raw_fd()),
2253            off_out_ptr,
2254            length as u64,
2255            u64::from(flags),
2256        )
2257    };
2258
2259    if ret < 0 {
2260        return Err(errno_from_negative_syscall_ret(ret));
2261    }
2262
2263    #[allow(clippy::cast_sign_loss)]
2264    ret_to_usize_ok(ret)
2265}
2266
2267/// Pose ou lève un verrou BSD sur un fichier (`flock(2)`).
2268pub fn flock(fd: BorrowedFd<'_>, operation: i32) -> Result<(), Errno> {
2269    // SAFETY: flock(2) ne touche aucune mémoire utilisateur.
2270    let ret = unsafe { syscall2(nr::FLOCK, fd_to_u64(fd.as_raw_fd()), i32_to_u64(operation)) };
2271
2272    if ret < 0 {
2273        return Err(errno_from_negative_syscall_ret(ret));
2274    }
2275    Ok(())
2276}
2277
2278/// Lit les statistiques du filesystem identifié par un chemin.
2279pub fn statfs(path: &CStr) -> Result<StatFsResult, Errno> {
2280    let mut kfs = KernelStatfs {
2281        f_type: 0,
2282        f_bsize: 0,
2283        f_blocks: 0,
2284        f_bfree: 0,
2285        f_bavail: 0,
2286        f_files: 0,
2287        f_ffree: 0,
2288        f_fsid: [0; 2],
2289        f_namelen: 0,
2290        f_frsize: 0,
2291        f_flags: 0,
2292        f_spare: [0; 4],
2293    };
2294
2295    // SAFETY:
2296    // - path est un CStr nul-terminé valide.
2297    // - &mut kfs pointe sur KernelStatfs local (120 octets), correctement aligné.
2298    let ret = unsafe {
2299        syscall2(
2300            nr::STATFS,
2301            path.as_ptr() as u64,
2302            &mut kfs as *mut KernelStatfs as u64,
2303        )
2304    };
2305
2306    if ret < 0 {
2307        return Err(errno_from_negative_syscall_ret(ret));
2308    }
2309
2310    Ok(kernel_statfs_to_result(&kfs))
2311}
2312
2313/// Lit les statistiques du filesystem contenant un FD.
2314pub fn fstatfs(fd: BorrowedFd<'_>) -> Result<StatFsResult, Errno> {
2315    let mut kfs = KernelStatfs {
2316        f_type: 0,
2317        f_bsize: 0,
2318        f_blocks: 0,
2319        f_bfree: 0,
2320        f_bavail: 0,
2321        f_files: 0,
2322        f_ffree: 0,
2323        f_fsid: [0; 2],
2324        f_namelen: 0,
2325        f_frsize: 0,
2326        f_flags: 0,
2327        f_spare: [0; 4],
2328    };
2329
2330    // SAFETY:
2331    // - fd est un BorrowedFd valide.
2332    // - &mut kfs pointe sur KernelStatfs local (120 octets), correctement aligné.
2333    let ret = unsafe {
2334        syscall2(
2335            nr::FSTATFS,
2336            fd_to_u64(fd.as_raw_fd()),
2337            &mut kfs as *mut KernelStatfs as u64,
2338        )
2339    };
2340
2341    if ret < 0 {
2342        return Err(errno_from_negative_syscall_ret(ret));
2343    }
2344
2345    Ok(kernel_statfs_to_result(&kfs))
2346}
2347
2348fn kernel_statfs_to_result(kfs: &KernelStatfs) -> StatFsResult {
2349    StatFsResult {
2350        f_type: FsType(kfs.f_type),
2351        f_bsize: kfs.f_bsize,
2352        f_frsize: kfs.f_frsize,
2353        f_blocks: kfs.f_blocks,
2354        f_bfree: kfs.f_bfree,
2355        f_bavail: kfs.f_bavail,
2356        f_files: kfs.f_files,
2357        f_ffree: kfs.f_ffree,
2358        f_fsid: kfs.f_fsid,
2359        f_namelen: kfs.f_namelen,
2360        f_flags: kfs.f_flags,
2361    }
2362}
2363
2364/// Synchronise une plage d'un fichier sur le disque.
2365///
2366/// Wrappeur de `sync_file_range(2)` (Linux 2.6.17+).
2367///
2368/// # Errors
2369///
2370/// - `EBADF` : FD invalide.
2371/// - `EINVAL` : paramètres invalides.
2372pub fn sync_file_range(
2373    fd: BorrowedFd<'_>,
2374    offset: i64,
2375    nbytes: u64,
2376    flags: u32,
2377) -> Result<(), Errno> {
2378    // SAFETY: sync_file_range(2) ne touche aucune mémoire utilisateur.
2379    let ret = unsafe {
2380        syscall4(
2381            nr::SYNC_FILE_RANGE,
2382            fd_to_u64(fd.as_raw_fd()),
2383            i64_to_u64(offset),
2384            nbytes,
2385            u64::from(flags),
2386        )
2387    };
2388
2389    if ret < 0 {
2390        return Err(errno_from_negative_syscall_ret(ret));
2391    }
2392    Ok(())
2393}
2394
2395// ─────────────────────────────────────────────────────────────────────────
2396// fcntl (opérations individuelles — ADR-021 convention 3)
2397// ─────────────────────────────────────────────────────────────────────────
2398
2399/// Duplique un FD en allouant le plus petit numéro ≥ `min_fd` (`F_DUPFD_CLOEXEC`).
2400///
2401/// # Errors
2402///
2403/// - `EMFILE` : quota FD du processus atteint.
2404/// - `EBADF` : FD invalide.
2405///
2406/// # Examples
2407///
2408/// ```no_run
2409/// use air_sys_syscall::fs::dup_fd;
2410/// use air_sys_types::fd::BorrowedFd;
2411///
2412/// # fn example(fd: BorrowedFd<'_>) {
2413/// let new_fd = dup_fd(fd, 0).expect("dup_fd");
2414/// # }
2415/// ```
2416pub fn dup_fd(fd: BorrowedFd<'_>, min_fd: RawFd) -> Result<OwnedFd, Errno> {
2417    // SAFETY: fcntl F_DUPFD_CLOEXEC ne touche aucune mémoire utilisateur.
2418    let ret = unsafe {
2419        syscall3(
2420            nr::FCNTL,
2421            fd_to_u64(fd.as_raw_fd()),
2422            i32_to_u64(F_DUPFD_CLOEXEC),
2423            fd_to_u64(min_fd),
2424        )
2425    };
2426
2427    if ret < 0 {
2428        return Err(errno_from_negative_syscall_ret(ret));
2429    }
2430
2431    // SAFETY: ret est un fd valide retourné par le kernel.
2432    #[allow(clippy::cast_possible_truncation)]
2433    Ok(unsafe { OwnedFd::from_raw_fd(ret as i32) })
2434}
2435
2436/// Lit les flags du descripteur (`F_GETFD`).
2437pub fn get_fd_flags(fd: BorrowedFd<'_>) -> Result<FdFlags, Errno> {
2438    // SAFETY: F_GETFD ne touche aucune mémoire utilisateur.
2439    let ret = unsafe { syscall3(nr::FCNTL, fd_to_u64(fd.as_raw_fd()), i32_to_u64(F_GETFD), 0) };
2440
2441    if ret < 0 {
2442        return Err(errno_from_negative_syscall_ret(ret));
2443    }
2444
2445    #[allow(clippy::cast_possible_truncation)]
2446    Ok(FdFlags::from_bits_truncate(ret as i32))
2447}
2448
2449/// Modifie les flags du descripteur (`F_SETFD`).
2450pub fn set_fd_flags(fd: BorrowedFd<'_>, flags: FdFlags) -> Result<(), Errno> {
2451    // SAFETY: F_SETFD ne touche aucune mémoire utilisateur.
2452    let ret = unsafe {
2453        syscall3(
2454            nr::FCNTL,
2455            fd_to_u64(fd.as_raw_fd()),
2456            i32_to_u64(F_SETFD),
2457            i32_to_u64(flags.bits()),
2458        )
2459    };
2460
2461    if ret < 0 {
2462        return Err(errno_from_negative_syscall_ret(ret));
2463    }
2464    Ok(())
2465}
2466
2467/// Lit les flags de statut du fichier (`F_GETFL`).
2468pub fn get_status_flags(fd: BorrowedFd<'_>) -> Result<StatusFlags, Errno> {
2469    // SAFETY: F_GETFL ne touche aucune mémoire utilisateur.
2470    let ret = unsafe { syscall3(nr::FCNTL, fd_to_u64(fd.as_raw_fd()), i32_to_u64(F_GETFL), 0) };
2471
2472    if ret < 0 {
2473        return Err(errno_from_negative_syscall_ret(ret));
2474    }
2475
2476    #[allow(clippy::cast_possible_truncation)]
2477    Ok(StatusFlags::from_bits_truncate(ret as i32))
2478}
2479
2480/// Modifie les flags de statut du fichier (`F_SETFL`).
2481pub fn set_status_flags(fd: BorrowedFd<'_>, flags: StatusFlags) -> Result<(), Errno> {
2482    // SAFETY: F_SETFL ne touche aucune mémoire utilisateur.
2483    let ret = unsafe {
2484        syscall3(
2485            nr::FCNTL,
2486            fd_to_u64(fd.as_raw_fd()),
2487            i32_to_u64(F_SETFL),
2488            i32_to_u64(flags.bits()),
2489        )
2490    };
2491
2492    if ret < 0 {
2493        return Err(errno_from_negative_syscall_ret(ret));
2494    }
2495    Ok(())
2496}
2497
2498/// Modifie la capacité d'un pipe (`F_SETPIPE_SZ`).
2499///
2500/// Retourne la nouvelle taille effective (peut être supérieure à `size`).
2501pub fn set_pipe_size(fd: BorrowedFd<'_>, size: usize) -> Result<usize, Errno> {
2502    // SAFETY: F_SETPIPE_SZ ne touche aucune mémoire utilisateur.
2503    let ret = unsafe {
2504        syscall3(
2505            nr::FCNTL,
2506            fd_to_u64(fd.as_raw_fd()),
2507            i32_to_u64(F_SETPIPE_SZ),
2508            size as u64,
2509        )
2510    };
2511
2512    if ret < 0 {
2513        return Err(errno_from_negative_syscall_ret(ret));
2514    }
2515
2516    #[allow(clippy::cast_sign_loss)]
2517    ret_to_usize_ok(ret)
2518}
2519
2520/// Lit la capacité d'un pipe (`F_GETPIPE_SZ`).
2521pub fn get_pipe_size(fd: BorrowedFd<'_>) -> Result<usize, Errno> {
2522    // SAFETY: F_GETPIPE_SZ ne touche aucune mémoire utilisateur.
2523    let ret = unsafe {
2524        syscall3(
2525            nr::FCNTL,
2526            fd_to_u64(fd.as_raw_fd()),
2527            i32_to_u64(F_GETPIPE_SZ),
2528            0,
2529        )
2530    };
2531
2532    if ret < 0 {
2533        return Err(errno_from_negative_syscall_ret(ret));
2534    }
2535
2536    #[allow(clippy::cast_sign_loss)]
2537    ret_to_usize_ok(ret)
2538}
2539
2540/// Tente d'acquérir un verrou POSIX non-bloquant (`F_SETLK`).
2541///
2542/// Retourne `true` si le verrou a été acquis, `false` si bloqué.
2543pub fn try_lock(fd: BorrowedFd<'_>, lock: &FileLock) -> Result<bool, Errno> {
2544    let kflock = file_lock_to_kernel(lock);
2545
2546    // SAFETY:
2547    // - kflock est une struct KernelFlock valide sur la pile.
2548    // - fcntl F_SETLK lit la struct à l'adresse fournie.
2549    let ret = unsafe {
2550        syscall3(
2551            nr::FCNTL,
2552            fd_to_u64(fd.as_raw_fd()),
2553            i32_to_u64(F_SETLK),
2554            &kflock as *const KernelFlock as u64,
2555        )
2556    };
2557
2558    if ret < 0 {
2559        let err = errno_from_negative_syscall_ret(ret);
2560        // EACCES ou EAGAIN signifient que le verrou est tenu par un autre.
2561        if err == Errno::EACCES || err == Errno::EAGAIN {
2562            return Ok(false);
2563        }
2564        return Err(err);
2565    }
2566    Ok(true)
2567}
2568
2569/// Acquiert un verrou POSIX en mode bloquant (`F_SETLKW`).
2570///
2571/// # Errors
2572///
2573/// - `EINTR` : interrompu par un signal (pas de retry automatique).
2574/// - `EDEADLK` : deadlock détecté.
2575pub fn lock(fd: BorrowedFd<'_>, lock: &FileLock) -> Result<(), Errno> {
2576    let kflock = file_lock_to_kernel(lock);
2577
2578    // SAFETY:
2579    // - kflock est une struct KernelFlock valide sur la pile.
2580    // - fcntl F_SETLKW lit la struct à l'adresse fournie.
2581    let ret = unsafe {
2582        syscall3(
2583            nr::FCNTL,
2584            fd_to_u64(fd.as_raw_fd()),
2585            i32_to_u64(F_SETLKW),
2586            &kflock as *const KernelFlock as u64,
2587        )
2588    };
2589
2590    if ret < 0 {
2591        return Err(errno_from_negative_syscall_ret(ret));
2592    }
2593    Ok(())
2594}
2595
2596fn file_lock_to_kernel(lock: &FileLock) -> KernelFlock {
2597    KernelFlock {
2598        l_type: lock.lock_type as i16,
2599        l_whence: lock.whence as i16,
2600        _pad: 0,
2601        l_start: lock.start,
2602        l_len: lock.length,
2603        l_pid: lock.pid,
2604        _pad2: 0,
2605    }
2606}
2607
2608/// Ajoute des scellements à un `memfd` (`F_ADD_SEALS`).
2609pub fn add_seals(fd: BorrowedFd<'_>, seals: Seals) -> Result<(), Errno> {
2610    // SAFETY: F_ADD_SEALS ne touche aucune mémoire utilisateur.
2611    let ret = unsafe {
2612        syscall3(
2613            nr::FCNTL,
2614            fd_to_u64(fd.as_raw_fd()),
2615            i32_to_u64(F_ADD_SEALS),
2616            u64::from(seals.bits()),
2617        )
2618    };
2619
2620    if ret < 0 {
2621        return Err(errno_from_negative_syscall_ret(ret));
2622    }
2623    Ok(())
2624}
2625
2626/// Lit les scellements actifs d'un `memfd` (`F_GET_SEALS`).
2627pub fn get_seals(fd: BorrowedFd<'_>) -> Result<Seals, Errno> {
2628    // SAFETY: F_GET_SEALS ne touche aucune mémoire utilisateur.
2629    let ret = unsafe {
2630        syscall3(
2631            nr::FCNTL,
2632            fd_to_u64(fd.as_raw_fd()),
2633            i32_to_u64(F_GET_SEALS),
2634            0,
2635        )
2636    };
2637
2638    if ret < 0 {
2639        return Err(errno_from_negative_syscall_ret(ret));
2640    }
2641
2642    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2643    Ok(Seals::from_bits_truncate(ret as u32))
2644}
2645
2646// ─────────────────────────────────────────────────────────────────────────
2647// Handles persistants
2648// ─────────────────────────────────────────────────────────────────────────
2649
2650/// Obtient un handle persistant pour un fichier.
2651///
2652/// Wrappeur de `name_to_handle_at(2)` (Linux 2.6.39+). Le handle
2653/// retourné identifie le fichier même après renommage.
2654///
2655/// # Errors
2656///
2657/// - `EOPNOTSUPP` : filesystem ne supporte pas les handles.
2658/// - `ENOENT` : fichier inexistant.
2659///
2660/// # Examples
2661///
2662/// ```no_run
2663/// use air_sys_syscall::fs::name_to_handle_at;
2664/// use air_sys_types::fs::{DirFd, NameToHandleFlags};
2665///
2666/// let handle = name_to_handle_at(DirFd::Cwd, c"./data.bin", NameToHandleFlags::empty())
2667///     .expect("name_to_handle_at");
2668/// ```
2669pub fn name_to_handle_at(
2670    dirfd: DirFd<'_>,
2671    path: &CStr,
2672    flags: NameToHandleFlags,
2673) -> Result<FileHandle, Errno> {
2674    let raw_dirfd = dirfd_to_raw(dirfd);
2675    let mut khandle = KernelFileHandle {
2676        handle_bytes: 128,
2677        handle_type: 0,
2678        f_handle: [0u8; 128],
2679    };
2680    let mut mount_id: i32 = 0;
2681
2682    // SAFETY:
2683    // - path est un CStr nul-terminé valide.
2684    // - khandle est initialisé avec handle_bytes=128 (capacité maximale).
2685    // - &mut mount_id est un i32 local valide.
2686    // - Le kernel écrit le type et les octets du handle si handle_bytes suffit.
2687    let ret = unsafe {
2688        syscall5(
2689            nr::NAME_TO_HANDLE_AT,
2690            fd_to_u64(raw_dirfd),
2691            path.as_ptr() as u64,
2692            &mut khandle as *mut KernelFileHandle as u64,
2693            &mut mount_id as *mut i32 as u64,
2694            i32_to_u64(flags.bits()),
2695        )
2696    };
2697
2698    if ret < 0 {
2699        return Err(errno_from_negative_syscall_ret(ret));
2700    }
2701
2702    // handle_bytes est u32, la conversion en usize est exacte sur les cibles 64 bits Air.
2703    #[allow(clippy::cast_lossless)]
2704    let nbytes = khandle.handle_bytes as usize;
2705    let data = khandle
2706        .f_handle
2707        .get(..nbytes.min(128))
2708        .unwrap_or(&khandle.f_handle[..])
2709        .to_vec();
2710
2711    Ok(FileHandle {
2712        handle_type: khandle.handle_type,
2713        handle_bytes: data,
2714        mount_id,
2715    })
2716}
2717
2718/// Ouvre un fichier depuis un handle persistant.
2719///
2720/// # Parameters
2721///
2722/// - `mount_fd` : FD d'un fichier sur le même système de fichiers.
2723/// - `handle` : handle retourné par [`name_to_handle_at`].
2724/// - `flags` : flags d'ouverture.
2725///
2726/// # Errors
2727///
2728/// - `ESTALE` : handle invalide (fichier supprimé ou FS remonté).
2729/// - `EPERM` : `CAP_DAC_READ_SEARCH` requis.
2730///
2731/// # Examples
2732///
2733/// ```no_run
2734/// use air_sys_syscall::fs::open_by_handle_at;
2735/// use air_sys_types::fs::{FileHandle, OpenFlags};
2736/// use air_sys_types::fd::BorrowedFd;
2737///
2738/// # fn example(mount_fd: BorrowedFd<'_>, handle: &FileHandle) {
2739/// let fd = open_by_handle_at(mount_fd, handle, OpenFlags::RDONLY)
2740///     .expect("open_by_handle_at");
2741/// # }
2742/// ```
2743pub fn open_by_handle_at(
2744    mount_fd: BorrowedFd<'_>,
2745    handle: &FileHandle,
2746    flags: OpenFlags,
2747) -> Result<OwnedFd, Errno> {
2748    // Reconstruit une KernelFileHandle depuis le FileHandle Air.
2749    let nbytes = handle.handle_bytes.len().min(128);
2750    // nbytes <= 128, donc la troncature usize → u32 est exacte.
2751    #[allow(clippy::cast_possible_truncation)]
2752    let nbytes_u32 = nbytes as u32;
2753    let mut khandle = KernelFileHandle {
2754        handle_bytes: nbytes_u32,
2755        handle_type: handle.handle_type,
2756        f_handle: [0u8; 128],
2757    };
2758    // SAFETY: nbytes <= 128, donc la copie ne déborde pas.
2759    khandle.f_handle[..nbytes].copy_from_slice(&handle.handle_bytes[..nbytes]);
2760
2761    let raw_flags = flags.bits() | O_CLOEXEC;
2762    #[allow(clippy::cast_possible_truncation)]
2763    let raw_flags_i32 = raw_flags as i32;
2764
2765    // SAFETY:
2766    // - khandle est un KernelFileHandle valide sur la pile.
2767    // - mount_fd est un BorrowedFd valide.
2768    // - Le kernel lit les handle_bytes octets de f_handle.
2769    let ret = unsafe {
2770        syscall3(
2771            nr::OPEN_BY_HANDLE_AT,
2772            fd_to_u64(mount_fd.as_raw_fd()),
2773            &khandle as *const KernelFileHandle as u64,
2774            i32_to_u64(raw_flags_i32),
2775        )
2776    };
2777
2778    if ret < 0 {
2779        return Err(errno_from_negative_syscall_ret(ret));
2780    }
2781
2782    // SAFETY: ret est un fd valide retourné par le kernel après succès.
2783    #[allow(clippy::cast_possible_truncation)]
2784    Ok(unsafe { OwnedFd::from_raw_fd(ret as i32) })
2785}
2786
2787// ─────────────────────────────────────────────────────────────────────────
2788// Tests
2789// ─────────────────────────────────────────────────────────────────────────
2790
2791#[cfg(all(test, target_os = "linux"))]
2792mod tests;