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;