Skip to main content

air_sys_syscall/
system.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 `system`.
6//!
7//! Cf. `docs/specs/layer-0/family-system.md`.
8
9#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
10compile_error!("air-sys-syscall::system supporte uniquement x86_64 et aarch64 (ADR-014).");
11
12use alloc::ffi::CString;
13use core::ffi::CStr;
14use core::num::NonZeroI32;
15
16use air_sys_types::Errno;
17use air_sys_types::system::{GetrandomFlags, SystemInfo, UtsName};
18
19// ─────────────────────────────────────────────────────────────────────────
20// Kernel structs (repr(C), utilisées uniquement localement)
21// ─────────────────────────────────────────────────────────────────────────
22
23/// Représentation locale de `struct utsname` (Linux) : 6 champs de 65 octets
24/// chacun, chaînes C terminées par NUL.
25#[repr(C)]
26struct KernelUtsname {
27    sysname: [u8; 65],
28    nodename: [u8; 65],
29    release: [u8; 65],
30    version: [u8; 65],
31    machine: [u8; 65],
32    domainname: [u8; 65],
33}
34
35impl KernelUtsname {
36    fn zeroed() -> Self {
37        Self {
38            sysname: [0u8; 65],
39            nodename: [0u8; 65],
40            release: [0u8; 65],
41            version: [0u8; 65],
42            machine: [0u8; 65],
43            domainname: [0u8; 65],
44        }
45    }
46}
47
48/// Représentation locale de `struct sysinfo` sur LP64 (x86_64 / aarch64).
49///
50/// Le padding est arch-dépendant. Sur les cibles 64 bits Linux (LP64),
51/// la structure fait exactement 112 octets.
52#[repr(C)]
53struct KernelSysinfo {
54    uptime: i64,
55    loads: [u64; 3],
56    totalram: u64,
57    freeram: u64,
58    sharedram: u64,
59    bufferram: u64,
60    totalswap: u64,
61    freeswap: u64,
62    procs: u16,
63    pad1: [u8; 6], // padding d'alignement LP64 jusqu'à l'offset 80
64    totalhigh: u64,
65    freehigh: u64,
66    mem_unit: u32,
67    pad2: [u8; 4], // padding final à 112 octets
68}
69
70const _: () = assert!(core::mem::size_of::<KernelSysinfo>() == 112);
71
72impl KernelSysinfo {
73    fn zeroed() -> Self {
74        Self {
75            uptime: 0,
76            loads: [0; 3],
77            totalram: 0,
78            freeram: 0,
79            sharedram: 0,
80            bufferram: 0,
81            totalswap: 0,
82            freeswap: 0,
83            procs: 0,
84            pad1: [0; 6],
85            totalhigh: 0,
86            freehigh: 0,
87            mem_unit: 0,
88            pad2: [0; 4],
89        }
90    }
91}
92
93// ─────────────────────────────────────────────────────────────────────────
94// Helper errno
95// ─────────────────────────────────────────────────────────────────────────
96
97fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
98    debug_assert!(ret < 0 && ret > -4096);
99    #[allow(clippy::cast_possible_truncation)]
100    let raw = ret.wrapping_neg() as i32;
101    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
102    Errno::from_nonzero(nz)
103}
104
105// ─────────────────────────────────────────────────────────────────────────
106// Helper : extrait une CString à partir d'un buffer null-terminated
107// ─────────────────────────────────────────────────────────────────────────
108
109/// Extrait la portion avant le premier NUL d'un buffer kernel null-terminé,
110/// et la convertit en [`CString`].
111///
112/// Convention 4 ADR-021 exception explicite : la sémantique exige un type
113/// owned (le buffer kernel est local temporaire ; la `CString` doit sortir
114/// du frame). Allocation bornée à la longueur du champ (65 octets max).
115fn cstring_from_uts_field(field: &[u8; 65]) -> CString {
116    let length = field.iter().position(|&b| b == 0).unwrap_or(65);
117    // CString::new exige l'absence de NUL interne ; on passe uniquement les
118    // octets avant le premier NUL trouvé — le kernel garantit la terminaison.
119    CString::new(&field[..length]).expect("champ utsname sans NUL interne avant terminateur")
120}
121
122// ─────────────────────────────────────────────────────────────────────────
123// getrandom
124// ─────────────────────────────────────────────────────────────────────────
125
126/// Remplit `buffer` avec des octets aléatoires cryptographiquement sûrs.
127///
128/// Utilise le syscall `getrandom(2)` (Linux 3.17+), qui puise dans le
129/// CSPRNG kernel — mécanisme recommandé par Air pour toute génération
130/// d'entropie. Ne passe jamais par `/dev/urandom` ni par la libc.
131///
132/// Peut retourner **moins** d'octets que `buffer.len()` (surtout pour les
133/// grands buffers). L'appelant doit boucler si nécessaire.
134///
135/// # Parameters
136///
137/// - `buffer` : buffer de destination à remplir.
138/// - `flags` : drapeaux de comportement (`GetrandomFlags::empty()` pour
139///   le cas nominal).
140///
141/// # Errors
142///
143/// - `EINTR` : interrompu par un signal (conforme à ADR-021 convention 2 ;
144///   l'appelant doit retry explicitement si désiré).
145/// - `EAGAIN` : entropie non disponible et `NONBLOCK` était positionné
146///   (au démarrage, avant l'initialisation du pool).
147/// - `EFAULT` : pointeur invalide (ne se produit pas via cette API safe).
148///
149/// # Examples
150///
151/// ```no_run
152/// use air_sys_syscall::system::getrandom;
153/// use air_sys_types::GetrandomFlags;
154///
155/// let mut buffer = [0u8; 32];
156/// let n = getrandom(&mut buffer, GetrandomFlags::empty()).expect("getrandom");
157/// assert!(n > 0);
158/// ```
159pub fn getrandom(buffer: &mut [u8], flags: GetrandomFlags) -> Result<usize, Errno> {
160    let buf_ptr = buffer.as_mut_ptr() as u64;
161    // Longueur du buffer : sur les cibles LP64 d'Air (x86_64/aarch64),
162    // usize == u64, la conversion est exacte. `cast_lossless` non applicable
163    // ici car usize -> u64 est lossless sur 64 bits ; on le documente.
164    #[allow(clippy::cast_lossless)]
165    let buf_len = buffer.len() as u64;
166    let flags_u64 = u64::from(flags.bits());
167
168    // SAFETY:
169    // - getrandom(2) (x86_64 = 318, aarch64 = 278) écrit `count` octets au
170    //   maximum dans `buffer` (pointeur valide pour toute la durée du syscall,
171    //   fourni par la référence mutable `buffer`).
172    // - Pas de `readonly` : le kernel écrit dans `*buf_ptr`.
173    let ret = unsafe { raw_syscall_getrandom(buf_ptr, buf_len, flags_u64) };
174    if ret < 0 {
175        return Err(errno_from_negative_syscall_ret(ret));
176    }
177    // ret >= 0 : nombre d'octets écrits. Borné par buffer.len() <= usize::MAX.
178    // Sur LP64, i64 et usize ont la même largeur ; le cast est exact.
179    // La troncature ne peut pas se produire car ret est borné par buffer.len(),
180    // qui est lui-même un usize — donc ret < usize::MAX sur LP64.
181    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
182    Ok(ret as usize)
183}
184
185// ─────────────────────────────────────────────────────────────────────────
186// uname
187// ─────────────────────────────────────────────────────────────────────────
188
189/// Retourne les informations sur le système d'exploitation et le kernel.
190///
191/// Wrappeur de `uname(2)`. Retourne un [`UtsName`] avec les champs
192/// `sysname`, `nodename`, `release`, `version`, `machine` et
193/// `domainname` sous forme de [`CString`] propriétaires.
194///
195/// # Errors
196///
197/// `uname(2)` ne retourne jamais d'erreur en pratique sur Linux moderne
198/// (la glibc et le kernel ne documentent aucun errno). Retour d'erreur
199/// inclus par convention défensive.
200///
201/// # Examples
202///
203/// ```no_run
204/// use air_sys_syscall::system::uname;
205///
206/// let info = uname().expect("uname");
207/// // info.machine vaut "x86_64" ou "aarch64" sur les plateformes Air
208/// ```
209pub fn uname() -> Result<UtsName, Errno> {
210    let mut buffer = KernelUtsname::zeroed();
211    let buf_ptr = (&mut buffer as *mut KernelUtsname) as u64;
212
213    // SAFETY:
214    // - uname(2) (x86_64 = 63, aarch64 = 160) écrit `sizeof(struct utsname)`
215    //   octets dans le pointeur fourni. `buffer` est local valide pour toute la
216    //   durée du syscall.
217    // - Pas de `readonly` : le kernel écrit dans `*buf_ptr`.
218    let ret = unsafe { raw_syscall_uname(buf_ptr) };
219    if ret < 0 {
220        return Err(errno_from_negative_syscall_ret(ret));
221    }
222
223    Ok(UtsName {
224        sysname: cstring_from_uts_field(&buffer.sysname),
225        nodename: cstring_from_uts_field(&buffer.nodename),
226        release: cstring_from_uts_field(&buffer.release),
227        version: cstring_from_uts_field(&buffer.version),
228        machine: cstring_from_uts_field(&buffer.machine),
229        domainname: cstring_from_uts_field(&buffer.domainname),
230    })
231}
232
233// ─────────────────────────────────────────────────────────────────────────
234// sysinfo
235// ─────────────────────────────────────────────────────────────────────────
236
237/// Retourne des statistiques globales sur l'état du système.
238///
239/// Wrappeur de `sysinfo(2)`. Les valeurs de RAM et swap sont converties
240/// en octets (multiplication par `mem_unit` effectuée en interne).
241/// Les `load_average` sont normalisés en `f64`.
242///
243/// # Errors
244///
245/// `sysinfo(2)` ne retourne normalement pas d'erreur.
246///
247/// # Examples
248///
249/// ```no_run
250/// use air_sys_syscall::system::sysinfo;
251///
252/// let info = sysinfo().expect("sysinfo");
253/// // info.free_ram indique la RAM libre en octets
254/// ```
255pub fn sysinfo() -> Result<SystemInfo, Errno> {
256    let mut info = KernelSysinfo::zeroed();
257    let info_ptr = (&mut info as *mut KernelSysinfo) as u64;
258
259    // SAFETY:
260    // - sysinfo(2) (x86_64 = 99, aarch64 = 179) écrit exactement 112 octets
261    //   dans le pointeur fourni (struct sysinfo LP64). `info` est local valide
262    //   pour toute la durée du syscall.
263    // - Pas de `readonly` : le kernel écrit dans `*info_ptr`.
264    let ret = unsafe { raw_syscall_sysinfo(info_ptr) };
265    if ret < 0 {
266        return Err(errno_from_negative_syscall_ret(ret));
267    }
268
269    // mem_unit est l'unité des valeurs mémoire du kernel. Sur Linux moderne
270    // mem_unit vaut toujours 1, mais la spec impose la multiplication.
271    let unit = u64::from(info.mem_unit);
272
273    // Saturation sur overflow : mem_unit * valeur peut théoriquement déborder
274    // un u64 sur des systèmes avec mem_unit > 1 et RAM extrême. La saturation
275    // est préférable à un résultat incorrect silencieux (Principe 2).
276    let scale = |v: u64| v.saturating_mul(unit);
277
278    // uptime : i64 de secondes. Valeur toujours >= 0 après boot.
279    let uptime_secs = u64::try_from(info.uptime).unwrap_or(0);
280    let uptime = core::time::Duration::from_secs(uptime_secs);
281
282    // Load averages : divisés par 65536.0 pour obtenir une valeur en nombre
283    // de processus. Le cast f64 d'un u64 est une widening conversion (lossless
284    // pour les valeurs dans la plage de représentation IEEE 754 double).
285    #[allow(clippy::cast_precision_loss)]
286    let load_average = [
287        info.loads[0] as f64 / 65536.0,
288        info.loads[1] as f64 / 65536.0,
289        info.loads[2] as f64 / 65536.0,
290    ];
291
292    Ok(SystemInfo {
293        uptime,
294        load_average,
295        total_ram: scale(info.totalram),
296        free_ram: scale(info.freeram),
297        shared_ram: scale(info.sharedram),
298        buffer_ram: scale(info.bufferram),
299        total_swap: scale(info.totalswap),
300        free_swap: scale(info.freeswap),
301        processes: info.procs,
302        total_high: scale(info.totalhigh),
303        free_high: scale(info.freehigh),
304        mem_unit: info.mem_unit,
305    })
306}
307
308// ─────────────────────────────────────────────────────────────────────────
309// gethostname (via uname)
310// ─────────────────────────────────────────────────────────────────────────
311
312/// Retourne le nom d'hôte du système.
313///
314/// Implémenté en interne via `uname(2)` (extraction du champ `nodename`)
315/// plutôt qu'un syscall `gethostname` séparé — économise un syscall et
316/// réutilise le code déjà testé.
317///
318/// # Errors
319///
320/// Voir [`uname`].
321///
322/// # Examples
323///
324/// ```no_run
325/// use air_sys_syscall::system::gethostname;
326///
327/// let name = gethostname().expect("gethostname");
328/// ```
329pub fn gethostname() -> Result<CString, Errno> {
330    let info = uname()?;
331    Ok(info.nodename)
332}
333
334// ─────────────────────────────────────────────────────────────────────────
335// sethostname
336// ─────────────────────────────────────────────────────────────────────────
337
338/// Modifie le nom d'hôte du système.
339///
340/// Wrappeur de `sethostname(2)`. Exige `CAP_SYS_ADMIN`. Rarement
341/// utilisé par les applications Air normales — plutôt réservé au
342/// launcher dans un UTS namespace.
343///
344/// # Parameters
345///
346/// - `name` : nouveau nom d'hôte. Doit avoir une longueur ≤
347///   `HOST_NAME_MAX` (64 octets sur Linux). Les octets NUL intermédiaires
348///   sont interdits.
349///
350/// # Errors
351///
352/// - `EPERM` : `CAP_SYS_ADMIN` manquant.
353/// - `EINVAL` : nom trop long (> 64 octets).
354///
355/// # Examples
356///
357/// ```no_run
358/// use air_sys_syscall::system::sethostname;
359/// use core::ffi::CStr;
360///
361/// // Requiert CAP_SYS_ADMIN
362/// sethostname(c"air-node-01").expect("sethostname");
363/// ```
364pub fn sethostname(name: &CStr) -> Result<(), Errno> {
365    let bytes = name.to_bytes(); // octets sans le NUL terminal
366    let ptr = bytes.as_ptr() as u64;
367    // Longueur sans le NUL terminal (sémantique sethostname(2)).
368    // Sur LP64, usize == u64, cast exact.
369    #[allow(clippy::cast_lossless)]
370    let length = bytes.len() as u64;
371
372    // SAFETY:
373    // - sethostname(2) (x86_64 = 170, aarch64 = 161) lit `length` octets à
374    //   l'adresse `ptr`. `name.to_bytes()` fournit une slice valide pour
375    //   toute la durée du syscall (lifetime de `name` couvre l'appel).
376    // - `readonly` correct : le kernel ne modifie pas la mémoire passée.
377    let ret = unsafe { raw_syscall_sethostname(ptr, length) };
378    if ret < 0 {
379        return Err(errno_from_negative_syscall_ret(ret));
380    }
381    Ok(())
382}
383
384// ─────────────────────────────────────────────────────────────────────────
385// Raw syscall helpers — x86_64
386// ─────────────────────────────────────────────────────────────────────────
387
388#[cfg(target_arch = "x86_64")]
389#[inline]
390unsafe fn raw_syscall_getrandom(buffer: u64, count: u64, flags: u64) -> i64 {
391    let ret: i64;
392    // SAFETY:
393    // - SYS_getrandom (x86_64 = 318). Le kernel écrit au plus `count` octets
394    //   à l'adresse `buffer` (validité garantie par l'appelant — référence mutable
395    //   au buffer).
396    // - ABI x86_64 : numéro en RAX, args en RDI/RSI/RDX ; retour en RAX ;
397    //   clobbe RCX (RIP) et R11 (RFLAGS).
398    // - Pas de `readonly` : le kernel écrit dans `*buffer`.
399    unsafe {
400        core::arch::asm!(
401            "syscall",
402            in("rax") 318_i64,
403            in("rdi") buffer,
404            in("rsi") count,
405            in("rdx") flags,
406            lateout("rax") ret,
407            lateout("rcx") _,
408            lateout("r11") _,
409            options(nostack, preserves_flags),
410        );
411    }
412    ret
413}
414
415#[cfg(target_arch = "x86_64")]
416#[inline]
417unsafe fn raw_syscall_uname(buffer: u64) -> i64 {
418    let ret: i64;
419    // SAFETY:
420    // - SYS_uname (x86_64 = 63). Le kernel écrit `sizeof(struct utsname)`
421    //   (390 octets) à l'adresse `buffer` (validité garantie par l'appelant —
422    //   `KernelUtsname` local).
423    // - ABI x86_64 : numéro en RAX, arg1 en RDI ; retour en RAX ; clobbe
424    //   RCX et R11.
425    // - Pas de `readonly` : le kernel écrit dans `*buffer`.
426    unsafe {
427        core::arch::asm!(
428            "syscall",
429            in("rax") 63_i64,
430            in("rdi") buffer,
431            lateout("rax") ret,
432            lateout("rcx") _,
433            lateout("r11") _,
434            options(nostack, preserves_flags),
435        );
436    }
437    ret
438}
439
440#[cfg(target_arch = "x86_64")]
441#[inline]
442unsafe fn raw_syscall_sysinfo(buffer: u64) -> i64 {
443    let ret: i64;
444    // SAFETY:
445    // - SYS_sysinfo (x86_64 = 99). Le kernel écrit exactement 112 octets
446    //   (struct sysinfo LP64) à l'adresse `buffer` (validité garantie par
447    //   l'appelant — `KernelSysinfo` local, 112 octets vérifiés à la
448    //   compilation).
449    // - ABI x86_64 : numéro en RAX, arg1 en RDI ; retour en RAX ; clobbe
450    //   RCX et R11.
451    // - Pas de `readonly` : le kernel écrit dans `*buffer`.
452    unsafe {
453        core::arch::asm!(
454            "syscall",
455            in("rax") 99_i64,
456            in("rdi") buffer,
457            lateout("rax") ret,
458            lateout("rcx") _,
459            lateout("r11") _,
460            options(nostack, preserves_flags),
461        );
462    }
463    ret
464}
465
466#[cfg(target_arch = "x86_64")]
467#[inline]
468unsafe fn raw_syscall_sethostname(name: u64, length: u64) -> i64 {
469    let ret: i64;
470    // SAFETY:
471    // - SYS_sethostname (x86_64 = 170). Le kernel lit `length` octets à
472    //   l'adresse `name` (validité garantie par l'appelant — bytes valide
473    //   pour la durée du syscall).
474    // - ABI x86_64 : numéro en RAX, args en RDI/RSI ; retour en RAX ;
475    //   clobbe RCX et R11.
476    // - `readonly` correct : le kernel lit uniquement la mémoire fournie,
477    //   sans l'écrire.
478    unsafe {
479        core::arch::asm!(
480            "syscall",
481            in("rax") 170_i64,
482            in("rdi") name,
483            in("rsi") length,
484            lateout("rax") ret,
485            lateout("rcx") _,
486            lateout("r11") _,
487            options(nostack, preserves_flags, readonly),
488        );
489    }
490    ret
491}
492
493// ─────────────────────────────────────────────────────────────────────────
494// Raw syscall helpers — aarch64
495// ─────────────────────────────────────────────────────────────────────────
496
497#[cfg(target_arch = "aarch64")]
498#[inline]
499unsafe fn raw_syscall_getrandom(buffer: u64, count: u64, flags: u64) -> i64 {
500    let ret: i64;
501    // SAFETY:
502    // - SYS_getrandom (aarch64 = 278). Le kernel écrit au plus `count`
503    //   octets à l'adresse `buffer` (validité garantie par l'appelant).
504    // - ABI aarch64 : numéro en X8, args en X0/X1/X2, retour en X0.
505    //   `inout("x0")` passe buffer en entrée et lit ret en sortie.
506    // - Pas de `readonly` : le kernel écrit dans `*buffer`.
507    unsafe {
508        core::arch::asm!(
509            "svc 0",
510            in("x8") 278_i64,
511            inout("x0") buffer => ret,
512            in("x1") count,
513            in("x2") flags,
514            options(nostack, preserves_flags),
515        );
516    }
517    ret
518}
519
520#[cfg(target_arch = "aarch64")]
521#[inline]
522unsafe fn raw_syscall_uname(buffer: u64) -> i64 {
523    let ret: i64;
524    // SAFETY:
525    // - SYS_uname (aarch64 = 160). Identique x86_64 en sémantique.
526    // - ABI aarch64 : numéro en X8, arg1 en X0, retour en X0.
527    // - Pas de `readonly` : le kernel écrit dans `*buffer`.
528    unsafe {
529        core::arch::asm!(
530            "svc 0",
531            in("x8") 160_i64,
532            inout("x0") buffer => ret,
533            options(nostack, preserves_flags),
534        );
535    }
536    ret
537}
538
539#[cfg(target_arch = "aarch64")]
540#[inline]
541unsafe fn raw_syscall_sysinfo(buffer: u64) -> i64 {
542    let ret: i64;
543    // SAFETY:
544    // - SYS_sysinfo (aarch64 = 179). Identique x86_64 en sémantique.
545    // - ABI aarch64 : numéro en X8, arg1 en X0, retour en X0.
546    // - Pas de `readonly` : le kernel écrit dans `*buffer`.
547    unsafe {
548        core::arch::asm!(
549            "svc 0",
550            in("x8") 179_i64,
551            inout("x0") buffer => ret,
552            options(nostack, preserves_flags),
553        );
554    }
555    ret
556}
557
558#[cfg(target_arch = "aarch64")]
559#[inline]
560unsafe fn raw_syscall_sethostname(name: u64, length: u64) -> i64 {
561    let ret: i64;
562    // SAFETY:
563    // - SYS_sethostname (aarch64 = 161). Identique x86_64 en sémantique.
564    // - ABI aarch64 : numéro en X8, args en X0/X1, retour en X0.
565    // - `readonly` correct : le kernel lit uniquement la mémoire fournie.
566    unsafe {
567        core::arch::asm!(
568            "svc 0",
569            in("x8") 161_i64,
570            inout("x0") name => ret,
571            in("x1") length,
572            options(nostack, preserves_flags, readonly),
573        );
574    }
575    ret
576}
577
578// ─────────────────────────────────────────────────────────────────────────
579// Tests
580// ─────────────────────────────────────────────────────────────────────────
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585
586    // ── getrandom ─────────────────────────────────────────────────────────
587
588    #[cfg(target_os = "linux")]
589    #[test]
590    fn getrandom_fills_nonzero_bytes() {
591        let mut buffer = [0u8; 32];
592        let n = getrandom(&mut buffer, GetrandomFlags::empty()).expect("getrandom");
593        assert!(n > 0);
594        assert!(n <= 32);
595    }
596
597    #[cfg(target_os = "linux")]
598    #[test]
599    fn getrandom_two_calls_differ() {
600        // La probabilité que deux appels retournent exactement les mêmes 32
601        // octets est de 2⁻²⁵⁶ — négligeable en pratique.
602        let mut a = [0u8; 32];
603        let mut b = [0u8; 32];
604        getrandom(&mut a, GetrandomFlags::empty()).expect("getrandom a");
605        getrandom(&mut b, GetrandomFlags::empty()).expect("getrandom b");
606        assert_ne!(
607            a, b,
608            "deux appels successifs doivent retourner des octets différents"
609        );
610    }
611
612    #[cfg(target_os = "linux")]
613    #[test]
614    fn getrandom_empty_buf_returns_zero() {
615        let mut buffer: [u8; 0] = [];
616        let n = getrandom(&mut buffer, GetrandomFlags::empty()).expect("getrandom vide");
617        assert_eq!(n, 0);
618    }
619
620    #[cfg(target_os = "linux")]
621    #[test]
622    fn getrandom_insecure_flag_works() {
623        // INSECURE permet l'appel même avant l'init du pool (utile en test).
624        let mut buffer = [0u8; 16];
625        let n = getrandom(&mut buffer, GetrandomFlags::INSECURE).expect("getrandom INSECURE");
626        // Asserts séparés : un `&&` introduirait des branches de condition
627        // dont le côté faux n'est jamais pris (couverture branch).
628        assert!(n > 0);
629        assert!(n <= 16);
630    }
631
632    // ── uname ─────────────────────────────────────────────────────────────
633
634    #[cfg(target_os = "linux")]
635    #[test]
636    fn uname_returns_linux_sysname() {
637        let info = uname().expect("uname");
638        assert_eq!(
639            info.sysname.to_str().expect("sysname UTF-8"),
640            "Linux",
641            "sysname doit être 'Linux' sur toutes les cibles Air"
642        );
643    }
644
645    #[cfg(target_os = "linux")]
646    #[test]
647    fn uname_machine_is_air_target() {
648        let info = uname().expect("uname");
649        let machine = info.machine.to_str().expect("machine UTF-8");
650        // Assert exact par arche : un `||` laisserait une moitié de la
651        // disjonction non couverte sur chaque arche (l'autre opérande n'est
652        // jamais évalué après court-circuit).
653        #[cfg(target_arch = "x86_64")]
654        assert_eq!(machine, "x86_64");
655        #[cfg(target_arch = "aarch64")]
656        assert_eq!(machine, "aarch64");
657    }
658
659    #[cfg(target_os = "linux")]
660    #[test]
661    fn uname_release_nonempty() {
662        let info = uname().expect("uname");
663        assert!(
664            !info.release.to_bytes().is_empty(),
665            "release ne doit pas être vide"
666        );
667    }
668
669    #[cfg(target_os = "linux")]
670    #[test]
671    fn uname_nodename_nonempty() {
672        let info = uname().expect("uname");
673        // Le hostname doit toujours être défini (même en container).
674        assert!(
675            !info.nodename.to_bytes().is_empty(),
676            "nodename ne doit pas être vide"
677        );
678    }
679
680    // ── sysinfo ───────────────────────────────────────────────────────────
681
682    #[cfg(target_os = "linux")]
683    #[test]
684    fn sysinfo_uptime_is_positive() {
685        let info = sysinfo().expect("sysinfo");
686        assert!(info.uptime.as_secs() > 0, "uptime doit être > 0 après boot");
687    }
688
689    #[cfg(target_os = "linux")]
690    #[test]
691    fn sysinfo_total_ram_is_nonzero() {
692        let info = sysinfo().expect("sysinfo");
693        assert!(info.total_ram > 0, "total_ram doit être > 0");
694    }
695
696    #[cfg(target_os = "linux")]
697    #[test]
698    fn sysinfo_free_ram_le_total() {
699        let info = sysinfo().expect("sysinfo");
700        assert!(
701            info.free_ram <= info.total_ram,
702            "free_ram ({}) doit être ≤ total_ram ({})",
703            info.free_ram,
704            info.total_ram
705        );
706    }
707
708    #[cfg(target_os = "linux")]
709    #[test]
710    fn sysinfo_load_averages_non_negative() {
711        let info = sysinfo().expect("sysinfo");
712        for (i, &la) in info.load_average.iter().enumerate() {
713            assert!(la >= 0.0, "load_average[{i}] doit être >= 0.0, trouvé {la}");
714        }
715    }
716
717    #[cfg(target_os = "linux")]
718    #[test]
719    fn sysinfo_mem_unit_is_one() {
720        // Sur Linux moderne, mem_unit vaut toujours 1 (les valeurs sont déjà
721        // en octets). Si ce test échoue, il signale un changement de kernel
722        // inattendu et le code de scaling doit être revérifié.
723        let info = sysinfo().expect("sysinfo");
724        assert_eq!(info.mem_unit, 1, "mem_unit doit être 1 sur Linux moderne");
725    }
726
727    // ── gethostname ───────────────────────────────────────────────────────
728
729    #[cfg(target_os = "linux")]
730    #[test]
731    fn gethostname_matches_uname_nodename() {
732        let hostname = gethostname().expect("gethostname");
733        let info = uname().expect("uname");
734        assert_eq!(
735            hostname, info.nodename,
736            "gethostname doit retourner la même valeur que uname().nodename"
737        );
738    }
739
740    #[cfg(target_os = "linux")]
741    #[test]
742    fn gethostname_returns_nonempty_name() {
743        let name = gethostname().expect("gethostname");
744        assert!(
745            !name.to_bytes().is_empty(),
746            "hostname ne doit pas être vide"
747        );
748    }
749
750    // ── sethostname ───────────────────────────────────────────────────────
751
752    #[cfg(target_os = "linux")]
753    #[test]
754    fn sethostname_without_cap_sys_admin_returns_eperm() {
755        // En environnement non privilégié (CI, développement), sethostname
756        // doit retourner EPERM. Ce test valide le chemin d'erreur sans
757        // exiger les privileges.
758        let result = sethostname(c"air-test-host");
759        assert!(
760            matches!(result, Err(Errno::EPERM) | Ok(())),
761            "sethostname doit retourner EPERM (non privilégié) ou Ok (privilégié)"
762        );
763    }
764
765    // ── helpers internes ──────────────────────────────────────────────────
766
767    #[test]
768    fn cstring_from_uts_field_handles_full_field_without_nul() {
769        // Champ sans NUL (improbable en pratique, mais défensivement testé) :
770        // on doit extraire les 65 octets.
771        let field = [b'x'; 65];
772        let cs = cstring_from_uts_field(&field);
773        assert_eq!(cs.to_bytes().len(), 65);
774    }
775
776    #[test]
777    fn cstring_from_uts_field_stops_at_first_nul() {
778        let mut field = [0u8; 65];
779        field[0] = b'L';
780        field[1] = b'i';
781        field[2] = b'n';
782        field[3] = b'u';
783        field[4] = b'x';
784        // field[5] est déjà 0 (NUL terminal).
785        let cs = cstring_from_uts_field(&field);
786        assert_eq!(cs.to_str().expect("UTF-8"), "Linux");
787    }
788
789    #[test]
790    fn errno_from_negative_syscall_ret_maps_eperm() {
791        // EPERM = 1, donc -1 en retour de syscall.
792        assert_eq!(errno_from_negative_syscall_ret(-1), Errno::EPERM);
793    }
794
795    #[test]
796    fn errno_from_negative_syscall_ret_maps_einval() {
797        // EINVAL = 22.
798        assert_eq!(errno_from_negative_syscall_ret(-22), Errno::EINVAL);
799    }
800
801    // Couvre les deux côtés faux du `debug_assert!(ret < 0 && ret > -4096)` :
802    // garde de défense en profondeur jamais violée par les appelants réels,
803    // prouvée ici en l'appelant hors domaine (panique attendue).
804    #[test]
805    #[should_panic(expected = "ret < 0")]
806    fn errno_from_negative_syscall_ret_panics_on_non_negative() {
807        let _ = errno_from_negative_syscall_ret(0);
808    }
809
810    #[test]
811    #[should_panic(expected = "ret < 0")]
812    fn errno_from_negative_syscall_ret_panics_below_errno_range() {
813        let _ = errno_from_negative_syscall_ret(-5000);
814    }
815}