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}