Skip to main content

air_sys_syscall/
net.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 `net` — sockets synchrones.
6//!
7//! Cf. `docs/specs/layer-0/family-net.md`.
8//!
9//! Toutes les opérations de cette famille sont synchrones. Les variantes
10//! io_uring seront ajoutées au Temps 2b (ADR-022), en réutilisant les
11//! mêmes types.
12
13#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
14compile_error!("air-sys-syscall::net supporte uniquement x86_64 et aarch64 (ADR-014).");
15
16use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
17use alloc::ffi::CString;
18use alloc::vec::Vec;
19use core::ffi::CStr;
20use core::num::NonZeroI32;
21
22use air_sys_types::net::{
23    AcceptFlags, AcceptResult, IoSlice, IoSliceMut, IpMembershipV4, IpMembershipV6, Ipv4SocketAddr,
24    Ipv6SocketAddr, LingerOption, MessageFlags, ReceiveMessageRequest, ReceiveMessageResult,
25    SendMessageRequest, SendMessageResult, ShutdownMode, SocketAddr, SocketDomain, SocketType,
26    UnixCredentials, UnixSocketAddr,
27};
28use air_sys_types::process::Pid;
29use air_sys_types::{Errno, OwnedFd as AirOwnedFd, Timeval};
30
31// ─────────────────────────────────────────────────────────────────────────
32// Helper commun : conversion d'une valeur de retour syscall négative en Errno.
33// ─────────────────────────────────────────────────────────────────────────
34
35fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
36    debug_assert!(ret < 0 && ret > -4096);
37    #[allow(clippy::cast_possible_truncation)]
38    let raw = ret.wrapping_neg() as i32;
39    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
40    Errno::from_nonzero(nz)
41}
42
43// ─────────────────────────────────────────────────────────────────────────
44// Constantes socket
45// ─────────────────────────────────────────────────────────────────────────
46
47/// `SOCK_CLOEXEC` : ajouté systématiquement par les wrappers socket/socketpair/accept4.
48const SOCK_CLOEXEC: i32 = 0x0008_0000;
49
50/// `MSG_NOSIGNAL` : activé par défaut sur send/sendmsg.
51const MSG_NOSIGNAL: i32 = 0x4000;
52
53// ─────────────────────────────────────────────────────────────────────────
54// Constantes sockopt
55// ─────────────────────────────────────────────────────────────────────────
56
57const SOL_SOCKET: i32 = 1;
58const SO_KEEPALIVE: i32 = 9;
59const SO_REUSEADDR: i32 = 2;
60const SO_REUSEPORT: i32 = 15;
61const SO_SNDBUF: i32 = 7;
62const SO_RCVBUF: i32 = 8;
63const SO_ERROR: i32 = 4;
64const SO_LINGER: i32 = 13;
65const SO_PEERCRED: i32 = 17;
66const SO_BINDTODEVICE: i32 = 25;
67// Options à `timeval` : sur LP64 (x86_64/aarch64, cibles Air), `SO_RCVTIMEO`/
68// `SO_SNDTIMEO` valent les constantes « OLD » (le `time_t` y est déjà 64-bit),
69// avec un `struct timeval` de 16 octets. Cf. `Timeval` (air-sys-types).
70const SO_RCVTIMEO: i32 = 20;
71const SO_SNDTIMEO: i32 = 21;
72
73const IPPROTO_TCP: i32 = 6;
74const TCP_NODELAY: i32 = 1;
75const TCP_KEEPIDLE: i32 = 4;
76const TCP_KEEPINTVL: i32 = 5;
77const TCP_KEEPCNT: i32 = 6;
78
79const IPPROTO_IP: i32 = 0;
80const IP_TTL: i32 = 2;
81const IP_ADD_MEMBERSHIP: i32 = 35;
82const IP_DROP_MEMBERSHIP: i32 = 36;
83
84const IPPROTO_IPV6: i32 = 41;
85const IPV6_V6ONLY: i32 = 26;
86// `IPV6_JOIN_GROUP` / `IPV6_LEAVE_GROUP` (alias `IPV6_ADD/DROP_MEMBERSHIP`).
87const IPV6_ADD_MEMBERSHIP: i32 = 20;
88const IPV6_DROP_MEMBERSHIP: i32 = 21;
89
90/// Taille de `struct timeval` sur LP64 (deux `long` de 8 octets).
91const TIMEVAL_LEN: u32 = 16;
92/// Taille de `struct ip_mreq` (deux `struct in_addr`).
93const IP_MREQ_LEN: u32 = 8;
94/// Taille de `struct ipv6_mreq` (`in6_addr` + `int ifindex`).
95const IPV6_MREQ_LEN: u32 = 20;
96
97// Les tailles ABI ci-dessus sont **verrouillées** sur le layout `#[repr(C)]` des
98// types couche 0 : un dérapage casserait la compilation (assertions const).
99const _: () = assert!(core::mem::size_of::<Timeval>() == TIMEVAL_LEN as usize);
100const _: () = assert!(core::mem::size_of::<IpMembershipV4>() == IP_MREQ_LEN as usize);
101const _: () = assert!(core::mem::size_of::<IpMembershipV6>() == IPV6_MREQ_LEN as usize);
102
103// ─────────────────────────────────────────────────────────────────────────
104// Taille maximale de sockaddr (ss_maxsize POSIX)
105// ─────────────────────────────────────────────────────────────────────────
106
107pub(crate) const MAX_SOCKADDR_LEN: usize = 128;
108
109/// Buffer de stockage sockaddr brut aligné sur u64.
110#[repr(C, align(8))]
111#[derive(Debug)]
112pub(crate) struct RawSockaddrStorage {
113    pub(crate) bytes: [u8; MAX_SOCKADDR_LEN],
114}
115
116/// Convertit un `SocketAddr` Air en représentation kernel brute.
117/// Retourne `(buffer, longueur_effective)`.
118pub(crate) fn socket_addr_to_raw(address: &SocketAddr) -> (RawSockaddrStorage, u32) {
119    let mut storage = RawSockaddrStorage {
120        bytes: [0u8; MAX_SOCKADDR_LEN],
121    };
122    match address {
123        SocketAddr::Unix(unix) => {
124            // sa_family = AF_UNIX = 1 (LE)
125            storage.bytes[0..2].copy_from_slice(&1u16.to_ne_bytes());
126            match unix {
127                UnixSocketAddr::Path(path) => {
128                    let bytes = path.as_bytes();
129                    let length = bytes.len().min(107);
130                    storage.bytes[2..2_usize.saturating_add(length)]
131                        .copy_from_slice(&bytes[..length]);
132                    storage.bytes[2_usize.saturating_add(length)] = 0; // NUL terminateur
133                    // addrlen = sizeof(sa_family) + length + NUL
134                    #[allow(clippy::cast_possible_truncation)]
135                    let addrlen = (2_usize.saturating_add(length).saturating_add(1)) as u32;
136                    (storage, addrlen)
137                }
138                UnixSocketAddr::Abstract(name) => {
139                    // Premier octet = '\0' (marqueur abstrait), puis le nom
140                    storage.bytes[2] = 0;
141                    let length = name.len().min(107);
142                    storage.bytes[3..3_usize.saturating_add(length)]
143                        .copy_from_slice(&name[..length]);
144                    // addrlen = sizeof(sa_family) + 1 (NUL) + length
145                    #[allow(clippy::cast_possible_truncation)]
146                    let addrlen = (2_usize.saturating_add(1).saturating_add(length)) as u32;
147                    (storage, addrlen)
148                }
149                UnixSocketAddr::Unnamed => (storage, 2),
150            }
151        }
152        SocketAddr::Ipv4(ipv4) => {
153            // sa_family = AF_INET = 2
154            storage.bytes[0..2].copy_from_slice(&2u16.to_ne_bytes());
155            // port en big-endian
156            let port_be = ipv4.port.to_be();
157            storage.bytes[2..4].copy_from_slice(&port_be.to_ne_bytes());
158            storage.bytes[4..8].copy_from_slice(&ipv4.address);
159            // sin_zero reste à 0 (déjà initialisé)
160            (storage, 16)
161        }
162        SocketAddr::Ipv6(ipv6) => {
163            // sa_family = AF_INET6 = 10
164            storage.bytes[0..2].copy_from_slice(&10u16.to_ne_bytes());
165            let port_be = ipv6.port.to_be();
166            storage.bytes[2..4].copy_from_slice(&port_be.to_ne_bytes());
167            storage.bytes[4..8].copy_from_slice(&ipv6.flowinfo.to_ne_bytes());
168            storage.bytes[8..24].copy_from_slice(&ipv6.address);
169            storage.bytes[24..28].copy_from_slice(&ipv6.scope_id.to_ne_bytes());
170            (storage, 28)
171        }
172    }
173}
174
175/// Convertit un buffer sockaddr brut en `SocketAddr` Air.
176/// Retourne `None` si le buffer est trop court ou la famille inconnue.
177pub(crate) fn raw_to_socket_addr(buffer: &[u8], length: u32) -> Option<SocketAddr> {
178    if length < 2 {
179        return None;
180    }
181    let len_usize = length as usize;
182    if buffer.len() < len_usize {
183        return None;
184    }
185    let family = u16::from_ne_bytes([buffer[0], buffer[1]]);
186    match family {
187        1 => {
188            // AF_UNIX
189            if length == 2 {
190                return Some(SocketAddr::Unix(UnixSocketAddr::Unnamed));
191            }
192            if length < 3 {
193                return None;
194            }
195            let path_bytes = buffer.get(2..len_usize)?;
196            if path_bytes.first() == Some(&0) {
197                // Abstract : le premier octet est '\0', suivi du nom
198                let name = path_bytes.get(1..).unwrap_or(&[]).to_vec();
199                Some(SocketAddr::Unix(UnixSocketAddr::Abstract(name)))
200            } else {
201                // Path : NUL-terminé
202                let nul = path_bytes
203                    .iter()
204                    .position(|&b| b == 0)
205                    .unwrap_or(path_bytes.len());
206                let cstr = CString::new(&path_bytes[..nul]).ok()?;
207                Some(SocketAddr::Unix(UnixSocketAddr::Path(cstr)))
208            }
209        }
210        2 => {
211            // AF_INET
212            if length < 16 {
213                return None;
214            }
215            let port = u16::from_be_bytes([buffer[2], buffer[3]]);
216            let address = [buffer[4], buffer[5], buffer[6], buffer[7]];
217            Some(SocketAddr::Ipv4(Ipv4SocketAddr { address, port }))
218        }
219        10 => {
220            // AF_INET6
221            if length < 28 {
222                return None;
223            }
224            let port = u16::from_be_bytes([buffer[2], buffer[3]]);
225            let flowinfo = u32::from_ne_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]);
226            let mut address = [0u8; 16];
227            address.copy_from_slice(buffer.get(8..24)?);
228            let scope_id = u32::from_ne_bytes([buffer[24], buffer[25], buffer[26], buffer[27]]);
229            Some(SocketAddr::Ipv6(Ipv6SocketAddr {
230                address,
231                port,
232                flowinfo,
233                scope_id,
234            }))
235        }
236        _ => None,
237    }
238}
239
240// ─────────────────────────────────────────────────────────────────────────
241// Codec sockaddr PUBLIC pour la face libc (ADR-070, descellement additif couche 0
242// — cadre ADR-051). Fines expositions des codecs internes ci-dessus : une seule
243// autorité de layout sockaddr (kernel=bible), zéro duplication côté libc.
244// ─────────────────────────────────────────────────────────────────────────
245
246/// **Décode** un `sockaddr` kernel brut (octets + `socklen_t`) en [`SocketAddr`] typé.
247/// Exposition publique de [`raw_to_socket_addr`] pour la face **libc** : `bytes` est le
248/// buffer présenté par le C, `length` la longueur associée. Rend `None` si la famille
249/// est inconnue ou la longueur insuffisante — aucune présomption sur les octets
250/// (Principe 3), aucun `unsafe`.
251#[must_use]
252pub fn socket_addr_from_bytes(bytes: &[u8], length: u32) -> Option<SocketAddr> {
253    raw_to_socket_addr(bytes, length)
254}
255
256/// **Encode** un [`SocketAddr`] typé en `sockaddr` kernel brut dans `out`, rendant la
257/// **longueur effective** (`socklen_t`) écrite, ou `None` si `out` est trop court.
258/// Exposition publique de [`socket_addr_to_raw`] pour la face libc **sans** fuiter le
259/// type de stockage interne (copie bornée dans le buffer de l'appelant).
260#[must_use]
261pub fn socket_addr_to_bytes(address: &SocketAddr, out: &mut [u8]) -> Option<u32> {
262    let (storage, length) = socket_addr_to_raw(address);
263    let length_usize = usize::try_from(length).ok()?;
264    let source = storage.bytes.get(..length_usize)?;
265    let target = out.get_mut(..length_usize)?;
266    target.copy_from_slice(source);
267    Some(length)
268}
269
270// ─────────────────────────────────────────────────────────────────────────
271// Helpers cmsg (ancillaires SCM_RIGHTS)
272// ─────────────────────────────────────────────────────────────────────────
273
274/// Construit un buffer `cmsghdr` + données FDs pour `SCM_RIGHTS` (LP64).
275///
276/// Structure `cmsghdr` sur LP64 :
277/// - cmsg_len  : u64 (8 octets) — taille incluant le header
278/// - cmsg_level: i32 (4 octets) = SOL_SOCKET = 1
279/// - cmsg_type : i32 (4 octets) = SCM_RIGHTS = 1
280/// - données   : i32 * nfds
281pub(crate) fn build_scm_rights_cmsg(fds: &[BorrowedFd<'_>]) -> Vec<u8> {
282    if fds.is_empty() {
283        return Vec::new();
284    }
285    let data_len = fds.len().saturating_mul(4); // chaque fd = i32
286    let cmsg_len = 16_usize.saturating_add(data_len); // header 16 + data
287    let total = cmsg_len.saturating_add(7) & !7_usize; // alignement 8
288    let mut buffer = vec![0u8; total];
289    // cmsg_len en u64 (LP64)
290    #[allow(clippy::cast_possible_truncation)]
291    buffer[0..8].copy_from_slice(&(cmsg_len as u64).to_ne_bytes());
292    // cmsg_level = SOL_SOCKET = 1
293    buffer[8..12].copy_from_slice(&1i32.to_ne_bytes());
294    // cmsg_type = SCM_RIGHTS = 1
295    buffer[12..16].copy_from_slice(&1i32.to_ne_bytes());
296    // FDs
297    for (i, fd) in fds.iter().enumerate() {
298        let raw = fd.as_raw_fd();
299        let offset = 16_usize.saturating_add(i.saturating_mul(4));
300        buffer[offset..offset.saturating_add(4)].copy_from_slice(&raw.to_ne_bytes());
301    }
302    buffer
303}
304
305/// Parse un buffer cmsg et extrait les `OwnedFd` reçus via `SCM_RIGHTS`.
306pub(crate) fn parse_scm_rights(cmsg_buf: &[u8], cmsg_len: usize) -> Vec<OwnedFd> {
307    let mut fds = Vec::new();
308    let mut offset = 0_usize;
309    while offset.saturating_add(16) <= cmsg_len && offset.saturating_add(16) <= cmsg_buf.len() {
310        let len_bytes: [u8; 8] = match cmsg_buf[offset..offset.saturating_add(8)].try_into() {
311            Ok(b) => b,
312            Err(_) => break,
313        };
314        // MAX_SOCKADDR_LEN = 128 < usize::MAX sur toutes les cibles supportées (LP64).
315        // La troncature de u64 à usize est intentionnelle : les tailles cmsg sont
316        // bornées à quelques centaines d'octets en pratique.
317        #[allow(clippy::cast_possible_truncation)]
318        let length = u64::from_ne_bytes(len_bytes) as usize;
319        let level_bytes: [u8; 4] =
320            match cmsg_buf[offset.saturating_add(8)..offset.saturating_add(12)].try_into() {
321                Ok(b) => b,
322                Err(_) => break,
323            };
324        let level = i32::from_ne_bytes(level_bytes);
325        let ty_bytes: [u8; 4] =
326            match cmsg_buf[offset.saturating_add(12)..offset.saturating_add(16)].try_into() {
327                Ok(b) => b,
328                Err(_) => break,
329            };
330        let ty = i32::from_ne_bytes(ty_bytes);
331
332        // SOL_SOCKET=1, SCM_RIGHTS=1
333        if level == 1 && ty == 1 && length >= 16 {
334            let mut fd_off = offset.saturating_add(16);
335            let end = offset.saturating_add(length);
336            while fd_off.saturating_add(4) <= end && fd_off.saturating_add(4) <= cmsg_buf.len() {
337                let raw_bytes: [u8; 4] = match cmsg_buf[fd_off..fd_off.saturating_add(4)].try_into()
338                {
339                    Ok(b) => b,
340                    Err(_) => break,
341                };
342                let raw = i32::from_ne_bytes(raw_bytes);
343                // SAFETY: Le kernel a retourné des FDs valides dans le cmsg SCM_RIGHTS.
344                // Ces FDs nous appartiennent maintenant (le kernel en transfère l'ownership).
345                fds.push(unsafe { OwnedFd::from_raw_fd(raw) });
346                fd_off = fd_off.saturating_add(4);
347            }
348        }
349        let aligned = length.saturating_add(7) & !7_usize;
350        if aligned == 0 {
351            break;
352        }
353        offset = offset.saturating_add(aligned);
354    }
355    fds
356}
357
358// ─────────────────────────────────────────────────────────────────────────
359// Structure KernelMsghdr (LP64)
360// ─────────────────────────────────────────────────────────────────────────
361
362/// Représentation kernel de `struct msghdr` en mode LP64 (Linux x86_64/aarch64).
363///
364/// Les champs `size_t` font 8 octets ; on insère un padding explicite entre
365/// `msg_namelen` (u32) et `msg_iov` (u64) pour respecter l'alignement LP64.
366#[repr(C)]
367struct KernelMsghdr {
368    msg_name: u64,       // *mut void = u64 (NULL si pas d'adresse)
369    msg_namelen: u32,    // longueur de l'adresse
370    _pad: u32,           // alignement LP64
371    msg_iov: u64,        // *mut iovec
372    msg_iovlen: u64,     // size_t
373    msg_control: u64,    // *mut void (cmsg)
374    msg_controllen: u64, // size_t
375    msg_flags: i32,      // drapeaux retournés
376    _pad2: u32,
377}
378
379// ─────────────────────────────────────────────────────────────────────────
380// Helper getsockopt/setsockopt bas-niveau
381// ─────────────────────────────────────────────────────────────────────────
382
383/// Lit une option socket en entier (`i32`).
384fn getsockopt_i32(sock: BorrowedFd<'_>, level: i32, optname: i32) -> Result<i32, Errno> {
385    let mut val: i32 = 0;
386    let mut length: u32 = 4;
387    let val_ptr: *mut i32 = &mut val;
388    let len_ptr: *mut u32 = &mut length;
389    // SAFETY:
390    // - getsockopt(2) écrit `*length` octets (au maximum `*len_ptr`) dans `*val_ptr`.
391    // - val et length sont locaux, valides pour la durée du syscall.
392    // - sock est garanti ouvert par BorrowedFd.
393    let ret = unsafe {
394        raw_syscall_getsockopt(
395            sock.as_raw_fd(),
396            level,
397            optname,
398            val_ptr as u64,
399            len_ptr as u64,
400        )
401    };
402    if ret < 0 {
403        return Err(errno_from_negative_syscall_ret(ret));
404    }
405    Ok(val)
406}
407
408/// Définit une option socket en entier (`i32`).
409fn setsockopt_i32(sock: BorrowedFd<'_>, level: i32, optname: i32, val: i32) -> Result<(), Errno> {
410    let val_ref: &i32 = &val;
411    let val_ptr: *const i32 = val_ref;
412    let length: u32 = 4;
413    // SAFETY:
414    // - setsockopt(2) lit `length` octets depuis `*val_ptr`.
415    // - val est local, valide pour la durée du syscall.
416    // - sock est garanti ouvert par BorrowedFd.
417    let ret =
418        unsafe { raw_syscall_setsockopt(sock.as_raw_fd(), level, optname, val_ptr as u64, length) };
419    if ret < 0 {
420        return Err(errno_from_negative_syscall_ret(ret));
421    }
422    Ok(())
423}
424
425// ─────────────────────────────────────────────────────────────────────────
426// setsockopt/getsockopt PUBLICS à valeur entière pour la face libc (ADR-071,
427// descellement additif couche 0 — cadre ADR-051). La quasi-totalité des options
428// (SO_REUSEADDR/TCP_NODELAY/SO_KEEPALIVE/SO_ERROR/IP_TOS/…) sont des `int`.
429// ─────────────────────────────────────────────────────────────────────────
430
431/// **Définit** une option socket à valeur **entière** (`setsockopt` avec `optlen == 4`).
432/// Exposition publique de [`setsockopt_i32`] pour la face libc : `level`/`optname` sont
433/// les entiers bruts fournis par le C (validés par le kernel).
434///
435/// # Errors
436/// L'`errno` de `setsockopt(2)` (`EBADF`/`ENOPROTOOPT`/`EINVAL`…), propagé.
437pub fn set_socket_option_int(
438    sock: BorrowedFd<'_>,
439    level: i32,
440    optname: i32,
441    value: i32,
442) -> Result<(), Errno> {
443    setsockopt_i32(sock, level, optname, value)
444}
445
446/// **Lit** une option socket à valeur **entière** (`getsockopt`). Exposition publique
447/// de [`getsockopt_i32`] pour la face libc.
448///
449/// # Errors
450/// L'`errno` de `getsockopt(2)`, propagé.
451pub fn get_socket_option_int(sock: BorrowedFd<'_>, level: i32, optname: i32) -> Result<i32, Errno> {
452    getsockopt_i32(sock, level, optname)
453}
454
455// ─────────────────────────────────────────────────────────────────────────
456// Setup des sockets
457// ─────────────────────────────────────────────────────────────────────────
458
459/// Crée un socket.
460///
461/// Wrappeur de `socket(2)`. `SOCK_CLOEXEC` est ajouté automatiquement
462/// par le wrapper pour garantir que le FD ne fuit pas vers les processus
463/// fils via `exec`.
464///
465/// # Parameters
466///
467/// - `domain` : famille de protocole ([`SocketDomain::Unix`] pour l'IPC
468///   Air via Conduit).
469/// - `ty` : type de socket ([`SocketType::Stream`] pour TCP/Unix stream).
470/// - `protocol` : protocole (0 = défaut pour le type).
471///
472/// # Errors
473///
474/// - `EAFNOSUPPORT` : famille de protocole non supportée.
475/// - `EPROTONOSUPPORT` : protocole non supporté pour ce type.
476/// - `EMFILE` : quota de FD du processus atteint.
477/// - `ENFILE` : quota de FD système atteint.
478/// - `ENOMEM` : mémoire kernel insuffisante.
479///
480/// # Examples
481///
482/// ```no_run
483/// use air_sys_syscall::net::socket;
484/// use air_sys_types::net::{SocketDomain, SocketType};
485///
486/// let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket");
487/// ```
488pub fn socket(domain: SocketDomain, ty: SocketType, protocol: i32) -> Result<AirOwnedFd, Errno> {
489    let domain_raw = domain as i32;
490    let ty_raw = (ty as i32) | SOCK_CLOEXEC;
491    // SAFETY:
492    // - socket(2) ne touche à aucune mémoire utilisateur (args entiers).
493    // - En cas de succès, retourne un FD valide >= 0 ; en cas d'erreur, ret < 0.
494    // - SOCK_CLOEXEC est setté atomiquement par le kernel.
495    let ret = unsafe { raw_syscall_socket(domain_raw, ty_raw, protocol) };
496    if ret < 0 {
497        return Err(errno_from_negative_syscall_ret(ret));
498    }
499    #[allow(clippy::cast_possible_truncation)]
500    let raw_fd = ret as i32;
501    // SAFETY: Le kernel a retourné un FD valide >= 0.
502    Ok(unsafe { AirOwnedFd::from_raw_fd(raw_fd) })
503}
504
505/// Lie un socket à une adresse locale.
506///
507/// Wrappeur de `bind(2)`. Pour les sockets Unix path-based, supprimer
508/// le chemin existant avec `unlinkat` (en ignorant `ENOENT`) avant
509/// d'appeler `bind` est la convention recommandée.
510///
511/// # Parameters
512///
513/// - `sock` : FD du socket.
514/// - `address` : adresse locale à lier.
515///
516/// # Errors
517///
518/// - `EADDRINUSE` : adresse déjà utilisée.
519/// - `EADDRNOTAVAIL` : adresse non disponible sur cet hôte.
520/// - `EINVAL` : socket déjà lié ou adresse invalide.
521/// - `EACCES` : permission refusée (ex. bind sur un port < 1024 sans
522///   privilèges).
523///
524/// # Examples
525///
526/// ```no_run
527/// use air_sys_syscall::net::{socket, bind};
528/// use air_sys_types::net::{SocketAddr, SocketDomain, SocketType, UnixSocketAddr};
529/// use air_sys_types::fd::AsFd;
530///
531/// let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket");
532/// bind(fd.as_fd(), &SocketAddr::Unix(UnixSocketAddr::Path(
533///     std::ffi::CString::new("/tmp/air.sock").expect("cstring"),
534/// ))).expect("bind");
535/// ```
536pub fn bind(sock: BorrowedFd<'_>, address: &SocketAddr) -> Result<(), Errno> {
537    let (raw, addrlen) = socket_addr_to_raw(address);
538    let addr_ptr: *const u8 = raw.bytes.as_ptr();
539    // SAFETY:
540    // - bind(2) lit `addrlen` octets depuis `addr_ptr` (la sockaddr).
541    // - `raw` est local, valide pour la durée du syscall.
542    // - sock est garanti ouvert par BorrowedFd.
543    let ret = unsafe { raw_syscall_bind(sock.as_raw_fd(), addr_ptr as u64, addrlen) };
544    if ret < 0 {
545        return Err(errno_from_negative_syscall_ret(ret));
546    }
547    Ok(())
548}
549
550/// Met un socket en mode écoute.
551///
552/// Wrappeur de `listen(2)`.
553///
554/// # Parameters
555///
556/// - `sock` : FD du socket déjà lié avec [`bind`].
557/// - `backlog` : taille de la file des connexions en attente d'`accept`.
558///
559/// # Errors
560///
561/// - `EADDRINUSE` : adresse déjà utilisée.
562/// - `EOPNOTSUPP` : type de socket ne supporte pas `listen`.
563///
564/// # Examples
565///
566/// ```no_run
567/// use air_sys_syscall::net::listen;
568/// use air_sys_types::fd::BorrowedFd;
569///
570/// # fn example(fd: BorrowedFd<'_>) {
571/// listen(fd, 128).expect("listen");
572/// # }
573/// ```
574pub fn listen(sock: BorrowedFd<'_>, backlog: u32) -> Result<(), Errno> {
575    // backlog est passé comme i32 au kernel ; saturer à i32::MAX si trop grand.
576    #[allow(clippy::cast_possible_truncation)]
577    let backlog_i32 = if backlog > i32::MAX as u32 {
578        i32::MAX
579    } else {
580        backlog as i32
581    };
582    // SAFETY:
583    // - listen(2) ne touche à aucune mémoire utilisateur.
584    // - sock est garanti ouvert par BorrowedFd.
585    let ret = unsafe { raw_syscall_listen(sock.as_raw_fd(), backlog_i32) };
586    if ret < 0 {
587        return Err(errno_from_negative_syscall_ret(ret));
588    }
589    Ok(())
590}
591
592// ─────────────────────────────────────────────────────────────────────────
593// Opérations connectées synchrones
594// ─────────────────────────────────────────────────────────────────────────
595
596/// Initie une connexion vers un pair.
597///
598/// Wrappeur de `connect(2)`. Pour les sockets non-bloquants, retourne
599/// immédiatement `EINPROGRESS` ; la connexion est terminée quand le
600/// socket devient writable (via `poll`/`epoll`/io_uring).
601///
602/// # Parameters
603///
604/// - `sock` : FD du socket.
605/// - `address` : adresse du pair.
606///
607/// # Errors
608///
609/// - `ECONNREFUSED` : le pair refuse la connexion (Unix : socket absent).
610/// - `EINPROGRESS` : connexion en cours (socket non-bloquant).
611/// - `EALREADY` : connexion déjà en cours.
612/// - `EISCONN` : socket déjà connecté.
613/// - `ETIMEDOUT` : expiration du délai de connexion.
614///
615/// # Examples
616///
617/// ```no_run
618/// use air_sys_syscall::net::{socket, connect};
619/// use air_sys_types::net::{SocketAddr, SocketDomain, SocketType, UnixSocketAddr};
620/// use air_sys_types::fd::AsFd;
621///
622/// let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket");
623/// connect(fd.as_fd(), &SocketAddr::Unix(UnixSocketAddr::Path(
624///     std::ffi::CString::new("/run/conduit.sock").expect("cstring"),
625/// ))).expect("connect");
626/// ```
627pub fn connect(sock: BorrowedFd<'_>, address: &SocketAddr) -> Result<(), Errno> {
628    let (raw, addrlen) = socket_addr_to_raw(address);
629    let addr_ptr: *const u8 = raw.bytes.as_ptr();
630    // SAFETY:
631    // - connect(2) lit `addrlen` octets depuis `addr_ptr`.
632    // - `raw` est local, valide pour la durée du syscall.
633    // - sock est garanti ouvert par BorrowedFd.
634    let ret = unsafe { raw_syscall_connect(sock.as_raw_fd(), addr_ptr as u64, addrlen) };
635    if ret < 0 {
636        return Err(errno_from_negative_syscall_ret(ret));
637    }
638    Ok(())
639}
640
641/// Accepte une connexion entrante.
642///
643/// Wrappeur de `accept4(2)` (préféré à `accept` car CLOEXEC atomique).
644/// Le wrapper active `AcceptFlags::CLOEXEC` par défaut.
645///
646/// # Parameters
647///
648/// - `listener` : FD du socket en écoute.
649/// - `flags` : [`AcceptFlags`] (CLOEXEC implicite).
650///
651/// # Errors
652///
653/// - `EAGAIN` / `EWOULDBLOCK` : pas de connexion disponible (non-bloquant).
654/// - `EINTR` : interrompu par un signal (ADR-021 convention 2).
655/// - `ECONNABORTED` : connexion abandonnée avant l'accept.
656///
657/// # Examples
658///
659/// ```no_run
660/// use air_sys_syscall::net::accept4;
661/// use air_sys_types::net::AcceptFlags;
662/// use air_sys_types::fd::BorrowedFd;
663///
664/// # fn example(listener: BorrowedFd<'_>) {
665/// let result = accept4(listener, AcceptFlags::CLOEXEC).expect("accept4");
666/// let _client_fd = result.fd;
667/// # }
668/// ```
669pub fn accept4(listener: BorrowedFd<'_>, flags: AcceptFlags) -> Result<AcceptResult, Errno> {
670    let mut addr_buf = RawSockaddrStorage {
671        bytes: [0u8; MAX_SOCKADDR_LEN],
672    };
673    // MAX_SOCKADDR_LEN = 128, valeur connue < u32::MAX : la troncature n'est pas possible.
674    #[allow(clippy::cast_possible_truncation)]
675    let mut addrlen: u32 = MAX_SOCKADDR_LEN as u32;
676    let addr_ptr: *mut u8 = addr_buf.bytes.as_mut_ptr();
677    let addrlen_ptr: *mut u32 = &mut addrlen;
678    // Forcer CLOEXEC même si l'appelant l'a oublié
679    let effective_flags = (flags | AcceptFlags::CLOEXEC).bits();
680    // SAFETY:
681    // - accept4(2) écrit l'adresse du pair dans `addr_ptr` (au plus `addrlen` octets)
682    //   et met à jour `*addrlen_ptr` avec la longueur réelle.
683    // - addr_buf et addrlen sont locaux, valides pour la durée du syscall.
684    // - En cas de succès, retourne un nouveau FD valide >= 0.
685    // - listener est garanti ouvert par BorrowedFd.
686    let ret = unsafe {
687        raw_syscall_accept4(
688            listener.as_raw_fd(),
689            addr_ptr as u64,
690            addrlen_ptr as u64,
691            effective_flags,
692        )
693    };
694    if ret < 0 {
695        return Err(errno_from_negative_syscall_ret(ret));
696    }
697    #[allow(clippy::cast_possible_truncation)]
698    let raw_fd = ret as i32;
699    // SAFETY: Le kernel a retourné un FD valide.
700    let fd = unsafe { AirOwnedFd::from_raw_fd(raw_fd) };
701    let address = raw_to_socket_addr(&addr_buf.bytes, addrlen);
702    Ok(AcceptResult { fd, address })
703}
704
705/// Envoie des données sur un socket connecté.
706///
707/// Wrappeur de `send(2)`. `MessageFlags::NOSIGNAL` est activé automatiquement
708/// pour éviter `SIGPIPE` sur les connexions fermées.
709///
710/// # Parameters
711///
712/// - `sock` : FD du socket connecté.
713/// - `buffer` : données à envoyer.
714/// - `flags` : drapeaux [`MessageFlags`].
715///
716/// # Errors
717///
718/// - `EAGAIN` : socket non-bloquant et buffer kernel plein.
719/// - `EINTR` : interrompu par un signal.
720/// - `EPIPE` / `ECONNRESET` : connexion fermée par le pair.
721/// - `EMSGSIZE` : message trop grand pour ce type de socket.
722///
723/// # Examples
724///
725/// ```no_run
726/// use air_sys_syscall::net::send;
727/// use air_sys_types::net::MessageFlags;
728/// use air_sys_types::fd::BorrowedFd;
729///
730/// # fn example(fd: BorrowedFd<'_>) {
731/// let n = send(fd, b"hello", MessageFlags::empty()).expect("send");
732/// # }
733/// ```
734pub fn send(sock: BorrowedFd<'_>, buffer: &[u8], flags: MessageFlags) -> Result<usize, Errno> {
735    let buf_ptr: *const u8 = buffer.as_ptr();
736    // MSG_NOSIGNAL ajouté automatiquement
737    #[allow(clippy::cast_sign_loss)]
738    let effective_flags = (flags.bits() | MSG_NOSIGNAL) as u64;
739    // SAFETY:
740    // - sendto(2) lit `buffer.len()` octets depuis `buf_ptr` (immutable borrow garanti).
741    // - Les args address/addrlen sont NULL/0 pour un socket connecté.
742    // - sock est garanti ouvert par BorrowedFd.
743    let ret = unsafe {
744        raw_syscall_sendto(
745            sock.as_raw_fd(),
746            buf_ptr as u64,
747            buffer.len(),
748            effective_flags,
749            0,
750            0,
751        )
752    };
753    if ret < 0 {
754        return Err(errno_from_negative_syscall_ret(ret));
755    }
756    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
757    Ok(ret as usize)
758}
759
760/// Reçoit des données d'un socket connecté.
761///
762/// Wrappeur de `recv(2)`. Retourne 0 si le pair a fermé la connexion
763/// proprement (EOF).
764///
765/// # Parameters
766///
767/// - `sock` : FD du socket.
768/// - `buffer` : buffer de destination.
769/// - `flags` : drapeaux [`MessageFlags`].
770///
771/// # Errors
772///
773/// - `EAGAIN` : pas de données disponibles (non-bloquant).
774/// - `EINTR` : interrompu par un signal.
775/// - `ECONNRESET` : connexion réinitialisée par le pair.
776///
777/// # Examples
778///
779/// ```no_run
780/// use air_sys_syscall::net::recv;
781/// use air_sys_types::net::MessageFlags;
782/// use air_sys_types::fd::BorrowedFd;
783///
784/// # fn example(fd: BorrowedFd<'_>) {
785/// let mut buffer = [0u8; 1024];
786/// let n = recv(fd, &mut buffer, MessageFlags::empty()).expect("recv");
787/// # }
788/// ```
789pub fn recv(sock: BorrowedFd<'_>, buffer: &mut [u8], flags: MessageFlags) -> Result<usize, Errno> {
790    let buf_ptr: *mut u8 = buffer.as_mut_ptr();
791    #[allow(clippy::cast_sign_loss)]
792    let flags_u64 = flags.bits() as u64;
793    // SAFETY:
794    // - recvfrom(2) écrit au plus `buffer.len()` octets dans `buf_ptr`.
795    // - buffer est local (mutable borrow exclusif), valide pour la durée du syscall.
796    // - Les args address/addrlen sont NULL/0 : on n'a pas besoin de l'adresse source.
797    // - sock est garanti ouvert par BorrowedFd.
798    let ret = unsafe {
799        raw_syscall_recvfrom(
800            sock.as_raw_fd(),
801            buf_ptr as u64,
802            buffer.len(),
803            flags_u64,
804            0,
805            0,
806        )
807    };
808    if ret < 0 {
809        return Err(errno_from_negative_syscall_ret(ret));
810    }
811    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
812    Ok(ret as usize)
813}
814
815/// Envoie un message avec données de contrôle (ancillaires).
816///
817/// Wrappeur de `sendmsg(2)`. Supporte l'envoi multi-buffer et le
818/// passage de FDs via `SCM_RIGHTS`. `NOSIGNAL` est activé par défaut.
819///
820/// # Parameters
821///
822/// - `sock` : FD du socket.
823/// - `request` : description du message à envoyer.
824///
825/// # Errors
826///
827/// Voir [`send`]. De plus :
828/// - `EINVAL` : nombre de FDs dans `request.fds` dépasse `SCM_MAX_FD`.
829///
830/// # Examples
831///
832/// ```no_run
833/// use air_sys_syscall::net::sendmsg;
834/// use air_sys_types::net::{IoSlice, MessageFlags, SendMessageRequest};
835/// use air_sys_types::fd::BorrowedFd;
836///
837/// # fn example(fd: BorrowedFd<'_>) {
838/// let data = b"hello";
839/// let iov = [IoSlice::new(data)];
840/// let req = SendMessageRequest {
841///     iov: &iov,
842///     address: None,
843///     fds: &[],
844///     flags: MessageFlags::empty(),
845/// };
846/// sendmsg(fd, &req).expect("sendmsg");
847/// # }
848/// ```
849pub fn sendmsg(
850    sock: BorrowedFd<'_>,
851    request: &SendMessageRequest<'_>,
852) -> Result<SendMessageResult, Errno> {
853    // Convertir l'adresse optionnelle
854    let (addr_storage, addr_storage_len) = if let Some(address) = request.address {
855        let (s, l) = socket_addr_to_raw(address);
856        (Some(s), l)
857    } else {
858        (None, 0u32)
859    };
860
861    // Construire le buffer cmsg (SCM_RIGHTS) si des FDs sont présents
862    let cmsg_buf = build_scm_rights_cmsg(request.fds);
863
864    let iov_ptr: *const IoSlice<'_> = request.iov.as_ptr();
865
866    let (name_ptr, name_len) = if let Some(ref s) = addr_storage {
867        (s.bytes.as_ptr() as u64, addr_storage_len)
868    } else {
869        (0u64, 0u32)
870    };
871
872    let (ctrl_ptr, ctrl_len) = if cmsg_buf.is_empty() {
873        (0u64, 0u64)
874    } else {
875        (cmsg_buf.as_ptr() as u64, cmsg_buf.len() as u64)
876    };
877
878    let mut msghdr = KernelMsghdr {
879        msg_name: name_ptr,
880        msg_namelen: name_len,
881        _pad: 0,
882        msg_iov: iov_ptr as u64,
883        msg_iovlen: request.iov.len() as u64,
884        msg_control: ctrl_ptr,
885        msg_controllen: ctrl_len,
886        msg_flags: 0,
887        _pad2: 0,
888    };
889
890    #[allow(clippy::cast_sign_loss)]
891    let effective_flags = (request.flags.bits() | MSG_NOSIGNAL) as u64;
892    let hdr_ptr: *mut KernelMsghdr = &mut msghdr;
893
894    // SAFETY:
895    // - sendmsg(2) lit la structure msghdr pointée par hdr_ptr et les buffers iov.
896    // - Tous les pointeurs (iov, cmsg, address) sont valides pour la durée du syscall,
897    //   garantis par les lifetimes de SendMessageRequest.
898    // - IoSlice est #[repr(C)] compatible avec struct iovec.
899    // - sock est garanti ouvert par BorrowedFd.
900    let ret = unsafe { raw_syscall_sendmsg(sock.as_raw_fd(), hdr_ptr as u64, effective_flags) };
901    if ret < 0 {
902        return Err(errno_from_negative_syscall_ret(ret));
903    }
904    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
905    Ok(SendMessageResult {
906        bytes_sent: ret as usize,
907    })
908}
909
910/// Reçoit un message avec données de contrôle (ancillaires).
911///
912/// Wrappeur de `recvmsg(2)`. Les FDs reçus via `SCM_RIGHTS` sont
913/// automatiquement encapsulés dans des [`OwnedFd`] avec `CLOEXEC`
914/// si [`MessageFlags::CMSG_CLOEXEC`] est dans `request.flags`.
915///
916/// # Parameters
917///
918/// - `sock` : FD du socket.
919/// - `request` : buffers de destination et flags.
920///
921/// # Errors
922///
923/// Voir [`recv`].
924///
925/// # Examples
926///
927/// ```no_run
928/// use air_sys_syscall::net::recvmsg;
929/// use air_sys_types::net::{IoSliceMut, MessageFlags, ReceiveMessageRequest};
930/// use air_sys_types::fd::BorrowedFd;
931///
932/// # fn example(fd: BorrowedFd<'_>) {
933/// let mut buffer = [0u8; 1024];
934/// let mut iov = [IoSliceMut::new(&mut buffer)];
935/// let mut req = ReceiveMessageRequest {
936///     iov: &mut iov,
937///     flags: MessageFlags::CMSG_CLOEXEC,
938/// };
939/// let result = recvmsg(fd, &mut req).expect("recvmsg");
940/// // result.fds contient les OwnedFd reçus via SCM_RIGHTS
941/// # }
942/// ```
943pub fn recvmsg(
944    sock: BorrowedFd<'_>,
945    request: &mut ReceiveMessageRequest<'_>,
946) -> Result<ReceiveMessageResult, Errno> {
947    // Buffer cmsg de 1 KiO pour recevoir les données de contrôle (FDs, creds…)
948    let mut cmsg_buf = [0u8; 1024];
949    let mut addr_buf = RawSockaddrStorage {
950        bytes: [0u8; MAX_SOCKADDR_LEN],
951    };
952    // MAX_SOCKADDR_LEN = 128, valeur connue < u32::MAX : la troncature n'est pas possible.
953    #[allow(clippy::cast_possible_truncation)]
954    let mut addrlen: u32 = MAX_SOCKADDR_LEN as u32;
955
956    let iov_ptr: *mut IoSliceMut<'_> = request.iov.as_mut_ptr();
957    let addr_ptr: *mut u8 = addr_buf.bytes.as_mut_ptr();
958    let addrlen_ptr: *mut u32 = &mut addrlen;
959    let cmsg_ptr: *mut u8 = cmsg_buf.as_mut_ptr();
960
961    let mut msghdr = KernelMsghdr {
962        msg_name: addr_ptr as u64,
963        msg_namelen: addrlen,
964        _pad: 0,
965        msg_iov: iov_ptr as u64,
966        msg_iovlen: request.iov.len() as u64,
967        msg_control: cmsg_ptr as u64,
968        msg_controllen: cmsg_buf.len() as u64,
969        msg_flags: 0,
970        _pad2: 0,
971    };
972
973    #[allow(clippy::cast_sign_loss)]
974    let flags_u64 = request.flags.bits() as u64;
975    let hdr_ptr: *mut KernelMsghdr = &mut msghdr;
976
977    // SAFETY:
978    // - recvmsg(2) écrit dans les buffers iov et cmsg, et met à jour msghdr.
979    // - Tous les buffers (iov, cmsg, address) sont locaux valides pour la durée du syscall.
980    // - IoSliceMut est #[repr(C)] compatible avec struct iovec.
981    // - Le kernel met à jour msg_namelen avec la longueur réelle de l'adresse.
982    // - sock est garanti ouvert par BorrowedFd.
983    // Note : on passe également addrlen_ptr mais en réalité le kernel met à jour
984    // msg_namelen dans la structure msghdr ; on le récupère après l'appel.
985    let ret = unsafe { raw_syscall_recvmsg(sock.as_raw_fd(), hdr_ptr as u64, flags_u64) };
986    if ret < 0 {
987        return Err(errno_from_negative_syscall_ret(ret));
988    }
989
990    // Lire msg_namelen mis à jour par le kernel
991    let returned_addrlen = msghdr.msg_namelen;
992    // Lire msg_controllen mis à jour par le kernel
993    #[allow(clippy::cast_possible_truncation)]
994    let returned_cmsg_len = msghdr.msg_controllen as usize;
995    // Lire msg_flags mis à jour par le kernel
996    let returned_flags_bits = msghdr.msg_flags;
997
998    let _ = addrlen_ptr; // addr_len mis à jour via msghdr.msg_namelen
999
1000    let address = raw_to_socket_addr(&addr_buf.bytes, returned_addrlen);
1001    let fds = parse_scm_rights(&cmsg_buf, returned_cmsg_len);
1002
1003    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1004    let bytes_received = ret as usize;
1005
1006    let flags = MessageFlags::from_bits_truncate(returned_flags_bits);
1007
1008    Ok(ReceiveMessageResult {
1009        bytes_received,
1010        address,
1011        fds,
1012        flags,
1013    })
1014}
1015
1016/// Ferme partiellement ou totalement un socket connecté.
1017///
1018/// Wrappeur de `shutdown(2)`. Différent de `close` : `shutdown` termine
1019/// la connexion de façon propre sans fermer le FD.
1020///
1021/// # Parameters
1022///
1023/// - `sock` : FD du socket.
1024/// - `mode` : direction(s) à fermer.
1025///
1026/// # Errors
1027///
1028/// - `ENOTCONN` : socket non connecté.
1029/// - `ENOTSOCK` : FD n'est pas un socket.
1030///
1031/// # Examples
1032///
1033/// ```no_run
1034/// use air_sys_syscall::net::shutdown;
1035/// use air_sys_types::net::ShutdownMode;
1036/// use air_sys_types::fd::BorrowedFd;
1037///
1038/// # fn example(fd: BorrowedFd<'_>) {
1039/// shutdown(fd, ShutdownMode::Write).expect("shutdown");
1040/// # }
1041/// ```
1042pub fn shutdown(sock: BorrowedFd<'_>, mode: ShutdownMode) -> Result<(), Errno> {
1043    let how = mode as i32;
1044    // SAFETY:
1045    // - shutdown(2) ne touche à aucune mémoire utilisateur.
1046    // - sock est garanti ouvert par BorrowedFd.
1047    let ret = unsafe { raw_syscall_shutdown(sock.as_raw_fd(), how) };
1048    if ret < 0 {
1049        return Err(errno_from_negative_syscall_ret(ret));
1050    }
1051    Ok(())
1052}
1053
1054// ─────────────────────────────────────────────────────────────────────────
1055// Opérations utilitaires
1056// ─────────────────────────────────────────────────────────────────────────
1057
1058/// Retourne l'adresse locale du socket.
1059///
1060/// # Errors
1061///
1062/// - `ENOTSOCK` : FD n'est pas un socket.
1063/// - `ENOBUFS` : mémoire système insuffisante.
1064///
1065/// # Examples
1066///
1067/// ```no_run
1068/// use air_sys_syscall::net::getsockname;
1069/// use air_sys_types::fd::BorrowedFd;
1070///
1071/// # fn example(fd: BorrowedFd<'_>) {
1072/// let address = getsockname(fd).expect("getsockname");
1073/// # }
1074/// ```
1075pub fn getsockname(sock: BorrowedFd<'_>) -> Result<SocketAddr, Errno> {
1076    let mut addr_buf = RawSockaddrStorage {
1077        bytes: [0u8; MAX_SOCKADDR_LEN],
1078    };
1079    // MAX_SOCKADDR_LEN = 128, valeur connue < u32::MAX : la troncature n'est pas possible.
1080    #[allow(clippy::cast_possible_truncation)]
1081    let mut addrlen: u32 = MAX_SOCKADDR_LEN as u32;
1082    let addr_ptr: *mut u8 = addr_buf.bytes.as_mut_ptr();
1083    let addrlen_ptr: *mut u32 = &mut addrlen;
1084    // SAFETY:
1085    // - getsockname(2) écrit l'adresse locale dans addr_ptr (au plus *addrlen_ptr octets)
1086    //   et met à jour *addrlen_ptr avec la longueur réelle.
1087    // - addr_buf et addrlen sont locaux, valides pour la durée du syscall.
1088    // - sock est garanti ouvert par BorrowedFd.
1089    let ret =
1090        unsafe { raw_syscall_getsockname(sock.as_raw_fd(), addr_ptr as u64, addrlen_ptr as u64) };
1091    if ret < 0 {
1092        return Err(errno_from_negative_syscall_ret(ret));
1093    }
1094    raw_to_socket_addr(&addr_buf.bytes, addrlen).ok_or_else(|| {
1095        // Famille inconnue ou buffer trop court : errno EAFNOSUPPORT
1096        #[allow(clippy::unwrap_used)]
1097        Errno::from_nonzero(NonZeroI32::new(97).unwrap()) // EAFNOSUPPORT = 97
1098    })
1099}
1100
1101/// Retourne l'adresse du pair connecté.
1102///
1103/// # Errors
1104///
1105/// - `ENOTCONN` : socket non connecté.
1106/// - `ENOTSOCK` : FD n'est pas un socket.
1107///
1108/// # Examples
1109///
1110/// ```no_run
1111/// use air_sys_syscall::net::getpeername;
1112/// use air_sys_types::fd::BorrowedFd;
1113///
1114/// # fn example(fd: BorrowedFd<'_>) {
1115/// let address = getpeername(fd).expect("getpeername");
1116/// # }
1117/// ```
1118pub fn getpeername(sock: BorrowedFd<'_>) -> Result<SocketAddr, Errno> {
1119    let mut addr_buf = RawSockaddrStorage {
1120        bytes: [0u8; MAX_SOCKADDR_LEN],
1121    };
1122    // MAX_SOCKADDR_LEN = 128, valeur connue < u32::MAX : la troncature n'est pas possible.
1123    #[allow(clippy::cast_possible_truncation)]
1124    let mut addrlen: u32 = MAX_SOCKADDR_LEN as u32;
1125    let addr_ptr: *mut u8 = addr_buf.bytes.as_mut_ptr();
1126    let addrlen_ptr: *mut u32 = &mut addrlen;
1127    // SAFETY:
1128    // - getpeername(2) écrit l'adresse du pair dans addr_ptr et met à jour *addrlen_ptr.
1129    // - addr_buf et addrlen sont locaux, valides pour la durée du syscall.
1130    // - sock est garanti ouvert par BorrowedFd.
1131    let ret =
1132        unsafe { raw_syscall_getpeername(sock.as_raw_fd(), addr_ptr as u64, addrlen_ptr as u64) };
1133    if ret < 0 {
1134        return Err(errno_from_negative_syscall_ret(ret));
1135    }
1136    // COUVERTURE (ADR-035, STRUCTURAL — cf. docs/COVERAGE-EXCEPTIONS.md, section
1137    // `net`). La closure `None` (`EAFNOSUPPORT`) est inatteignable par
1138    // construction : `getpeername(2)` réussit sur un socket créé via l'API safe,
1139    // dont le domaine ∈ {Unix(1), Ipv4(2), Ipv6(10)} (`SocketDomain`) — les trois
1140    // familles que `raw_to_socket_addr` décode toujours en `Some`. Les gardes de
1141    // troncature internes (`len < 16`/`< 28`) sont testées séparément.
1142    raw_to_socket_addr(&addr_buf.bytes, addrlen).ok_or_else(|| {
1143        #[allow(clippy::unwrap_used)]
1144        Errno::from_nonzero(NonZeroI32::new(97).unwrap()) // EAFNOSUPPORT
1145    })
1146}
1147
1148/// Crée une paire de sockets connectés.
1149///
1150/// Wrappeur de `socketpair(2)`. Équivalent à créer un pipe mais
1151/// pour les sockets Unix. Les deux FDs retournés sont symétriques.
1152///
1153/// Seuls les sockets Unix (`AF_UNIX`) supportent `socketpair` sur Linux.
1154/// `SOCK_CLOEXEC` est ajouté automatiquement.
1155///
1156/// # Parameters
1157///
1158/// - `domain` : doit être [`SocketDomain::Unix`] sur Linux.
1159/// - `ty` : type de socket.
1160/// - `protocol` : 0 = défaut.
1161///
1162/// # Errors
1163///
1164/// - `EAFNOSUPPORT` : domaine non supporté (non-Unix).
1165/// - `EMFILE`/`ENFILE` : quotas FD atteints.
1166///
1167/// # Examples
1168///
1169/// ```no_run
1170/// use air_sys_syscall::net::socketpair;
1171/// use air_sys_types::net::{SocketDomain, SocketType};
1172///
1173/// let (fd_a, fd_b) = socketpair(SocketDomain::Unix, SocketType::Stream, 0)
1174///     .expect("socketpair");
1175/// ```
1176pub fn socketpair(
1177    domain: SocketDomain,
1178    ty: SocketType,
1179    protocol: i32,
1180) -> Result<(AirOwnedFd, AirOwnedFd), Errno> {
1181    let mut sv = [0i32; 2];
1182    let sv_ptr: *mut i32 = sv.as_mut_ptr();
1183    let domain_raw = domain as i32;
1184    let ty_raw = (ty as i32) | SOCK_CLOEXEC;
1185    // SAFETY:
1186    // - socketpair(2) écrit deux FDs dans sv[0..2].
1187    // - sv est local, valide pour la durée du syscall.
1188    // - En cas de succès, sv[0] et sv[1] sont des FDs valides >= 0.
1189    let ret = unsafe { raw_syscall_socketpair(domain_raw, ty_raw, protocol, sv_ptr as u64) };
1190    if ret < 0 {
1191        return Err(errno_from_negative_syscall_ret(ret));
1192    }
1193    // SAFETY: Le kernel a retourné deux FDs valides dans sv.
1194    let fd_a = unsafe { AirOwnedFd::from_raw_fd(sv[0]) };
1195    let fd_b = unsafe { AirOwnedFd::from_raw_fd(sv[1]) };
1196    Ok((fd_a, fd_b))
1197}
1198
1199// ─────────────────────────────────────────────────────────────────────────
1200// Options de socket (typées par option — ADR-021 convention 3)
1201// ─────────────────────────────────────────────────────────────────────────
1202
1203/// Lit l'option `SO_KEEPALIVE`.
1204pub fn get_so_keepalive(sock: BorrowedFd<'_>) -> Result<bool, Errno> {
1205    getsockopt_i32(sock, SOL_SOCKET, SO_KEEPALIVE).map(|v| v != 0)
1206}
1207
1208/// Définit l'option `SO_KEEPALIVE`.
1209pub fn set_so_keepalive(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno> {
1210    setsockopt_i32(sock, SOL_SOCKET, SO_KEEPALIVE, i32::from(enable))
1211}
1212
1213/// Lit l'option `SO_REUSEADDR`.
1214pub fn get_so_reuseaddr(sock: BorrowedFd<'_>) -> Result<bool, Errno> {
1215    getsockopt_i32(sock, SOL_SOCKET, SO_REUSEADDR).map(|v| v != 0)
1216}
1217
1218/// Définit l'option `SO_REUSEADDR`.
1219pub fn set_so_reuseaddr(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno> {
1220    setsockopt_i32(sock, SOL_SOCKET, SO_REUSEADDR, i32::from(enable))
1221}
1222
1223/// Lit l'option `SO_REUSEPORT`.
1224pub fn get_so_reuseport(sock: BorrowedFd<'_>) -> Result<bool, Errno> {
1225    getsockopt_i32(sock, SOL_SOCKET, SO_REUSEPORT).map(|v| v != 0)
1226}
1227
1228/// Définit l'option `SO_REUSEPORT`.
1229pub fn set_so_reuseport(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno> {
1230    setsockopt_i32(sock, SOL_SOCKET, SO_REUSEPORT, i32::from(enable))
1231}
1232
1233/// Lit l'option `TCP_NODELAY` (désactivation de l'algorithme Nagle).
1234pub fn get_tcp_nodelay(sock: BorrowedFd<'_>) -> Result<bool, Errno> {
1235    getsockopt_i32(sock, IPPROTO_TCP, TCP_NODELAY).map(|v| v != 0)
1236}
1237
1238/// Définit l'option `TCP_NODELAY`.
1239pub fn set_tcp_nodelay(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno> {
1240    setsockopt_i32(sock, IPPROTO_TCP, TCP_NODELAY, i32::from(enable))
1241}
1242
1243/// Lit la taille du buffer de réception (`SO_RCVBUF`).
1244pub fn get_so_rcvbuf(sock: BorrowedFd<'_>) -> Result<u32, Errno> {
1245    getsockopt_i32(sock, SOL_SOCKET, SO_RCVBUF).map(|v| {
1246        // Le kernel peut retourner une valeur positive ; on la traite comme u32.
1247        #[allow(clippy::cast_sign_loss)]
1248        let u = v as u32;
1249        u
1250    })
1251}
1252
1253/// Définit la taille du buffer de réception.
1254pub fn set_so_rcvbuf(sock: BorrowedFd<'_>, size: u32) -> Result<(), Errno> {
1255    // Le kernel interprète la valeur comme i32 ; saturer à i32::MAX si trop grand.
1256    #[allow(clippy::cast_possible_truncation)]
1257    let val = if size > i32::MAX as u32 {
1258        i32::MAX
1259    } else {
1260        size as i32
1261    };
1262    setsockopt_i32(sock, SOL_SOCKET, SO_RCVBUF, val)
1263}
1264
1265/// Lit la taille du buffer d'envoi (`SO_SNDBUF`).
1266pub fn get_so_sndbuf(sock: BorrowedFd<'_>) -> Result<u32, Errno> {
1267    getsockopt_i32(sock, SOL_SOCKET, SO_SNDBUF).map(|v| {
1268        #[allow(clippy::cast_sign_loss)]
1269        let u = v as u32;
1270        u
1271    })
1272}
1273
1274/// Définit la taille du buffer d'envoi.
1275pub fn set_so_sndbuf(sock: BorrowedFd<'_>, size: u32) -> Result<(), Errno> {
1276    #[allow(clippy::cast_possible_truncation)]
1277    let val = if size > i32::MAX as u32 {
1278        i32::MAX
1279    } else {
1280        size as i32
1281    };
1282    setsockopt_i32(sock, SOL_SOCKET, SO_SNDBUF, val)
1283}
1284
1285/// Lit l'erreur socket en attente (`SO_ERROR`) et l'efface.
1286///
1287/// Retourne `None` si aucune erreur n'est en attente.
1288pub fn get_so_error(sock: BorrowedFd<'_>) -> Result<Option<Errno>, Errno> {
1289    let val = getsockopt_i32(sock, SOL_SOCKET, SO_ERROR)?;
1290    if val == 0 {
1291        Ok(None)
1292    } else {
1293        let nz = NonZeroI32::new(val).ok_or_else(|| {
1294            // Ne se produit pas : val != 0 ici
1295            #[allow(clippy::unwrap_used)]
1296            Errno::from_nonzero(NonZeroI32::new(22).unwrap()) // EINVAL
1297        })?;
1298        Ok(Some(Errno::from_nonzero(nz)))
1299    }
1300}
1301
1302/// Lit les credentials du pair Unix (`SO_PEERCRED`).
1303///
1304/// Uniquement sur les sockets Unix (domain `AF_UNIX`).
1305pub fn get_so_peercred(sock: BorrowedFd<'_>) -> Result<UnixCredentials, Errno> {
1306    // struct ucred { pid_t pid; uid_t uid; gid_t gid; } = 3 * i32 = 12 octets
1307    let mut cred = [0i32; 3];
1308    let mut length: u32 = 12;
1309    let cred_ptr: *mut i32 = cred.as_mut_ptr();
1310    let len_ptr: *mut u32 = &mut length;
1311    // SAFETY:
1312    // - getsockopt(2) écrit la structure ucred (12 octets) dans cred.
1313    // - cred et length sont locaux, valides pour la durée du syscall.
1314    // - sock est garanti ouvert par BorrowedFd.
1315    let ret = unsafe {
1316        raw_syscall_getsockopt(
1317            sock.as_raw_fd(),
1318            SOL_SOCKET,
1319            SO_PEERCRED,
1320            cred_ptr as u64,
1321            len_ptr as u64,
1322        )
1323    };
1324    if ret < 0 {
1325        return Err(errno_from_negative_syscall_ret(ret));
1326    }
1327    // cred[0] = pid (i32), cred[1] = uid (u32), cred[2] = gid (u32)
1328    let pid_raw = cred[0];
1329    #[allow(clippy::cast_sign_loss)]
1330    let uid = cred[1] as u32;
1331    #[allow(clippy::cast_sign_loss)]
1332    let gid = cred[2] as u32;
1333    let nz = NonZeroI32::new(pid_raw).ok_or_else(|| {
1334        #[allow(clippy::unwrap_used)]
1335        Errno::from_nonzero(NonZeroI32::new(22).unwrap()) // EINVAL : pid=0 inattendu
1336    })?;
1337    Ok(UnixCredentials {
1338        pid: Pid::from_nonzero(nz),
1339        uid,
1340        gid,
1341    })
1342}
1343
1344/// Lit l'option `SO_LINGER`.
1345pub fn get_so_linger(sock: BorrowedFd<'_>) -> Result<LingerOption, Errno> {
1346    // struct linger { int l_onoff; int l_linger; } = 8 octets
1347    let mut linger = [0i32; 2];
1348    let mut length: u32 = 8;
1349    let linger_ptr: *mut i32 = linger.as_mut_ptr();
1350    let len_ptr: *mut u32 = &mut length;
1351    // SAFETY:
1352    // - getsockopt(2) écrit la structure linger (8 octets) dans linger.
1353    // - linger et length sont locaux, valides pour la durée du syscall.
1354    // - sock est garanti ouvert par BorrowedFd.
1355    let ret = unsafe {
1356        raw_syscall_getsockopt(
1357            sock.as_raw_fd(),
1358            SOL_SOCKET,
1359            SO_LINGER,
1360            linger_ptr as u64,
1361            len_ptr as u64,
1362        )
1363    };
1364    if ret < 0 {
1365        return Err(errno_from_negative_syscall_ret(ret));
1366    }
1367    if linger[0] == 0 {
1368        Ok(LingerOption::Disabled)
1369    } else {
1370        #[allow(clippy::cast_sign_loss)]
1371        let secs = linger[1] as u64;
1372        Ok(LingerOption::Enabled(core::time::Duration::from_secs(secs)))
1373    }
1374}
1375
1376/// Définit l'option `SO_LINGER`.
1377pub fn set_so_linger(sock: BorrowedFd<'_>, linger: LingerOption) -> Result<(), Errno> {
1378    let (l_onoff, l_linger): (i32, i32) = match linger {
1379        LingerOption::Disabled => (0, 0),
1380        LingerOption::Enabled(d) => {
1381            // Saturer à i32::MAX secondes (≈ 68 ans)
1382            let secs = d.as_secs();
1383            #[allow(clippy::cast_possible_truncation)]
1384            let secs_i32 = if secs > i32::MAX as u64 {
1385                i32::MAX
1386            } else {
1387                secs as i32
1388            };
1389            (1, secs_i32)
1390        }
1391    };
1392    let buffer = [l_onoff, l_linger];
1393    let buf_ptr: *const i32 = buffer.as_ptr();
1394    let length: u32 = 8;
1395    // SAFETY:
1396    // - setsockopt(2) lit 8 octets depuis buf_ptr (struct linger).
1397    // - buffer est local, valide pour la durée du syscall.
1398    // - sock est garanti ouvert par BorrowedFd.
1399    let ret = unsafe {
1400        raw_syscall_setsockopt(
1401            sock.as_raw_fd(),
1402            SOL_SOCKET,
1403            SO_LINGER,
1404            buf_ptr as u64,
1405            length,
1406        )
1407    };
1408    if ret < 0 {
1409        return Err(errno_from_negative_syscall_ret(ret));
1410    }
1411    Ok(())
1412}
1413
1414/// Lie le socket à une interface réseau (`SO_BINDTODEVICE`).
1415pub fn set_so_bindtodevice(sock: BorrowedFd<'_>, ifname: &CStr) -> Result<(), Errno> {
1416    let bytes = ifname.to_bytes_with_nul();
1417    let ptr: *const u8 = bytes.as_ptr();
1418    #[allow(clippy::cast_possible_truncation)]
1419    let length = bytes.len() as u32;
1420    // SAFETY:
1421    // - setsockopt(2) lit `length` octets depuis `ptr` (le nom d'interface NUL-terminé).
1422    // - Le CStr garantit la validité du pointeur et la présence du NUL terminateur.
1423    // - sock est garanti ouvert par BorrowedFd.
1424    let ret = unsafe {
1425        raw_syscall_setsockopt(
1426            sock.as_raw_fd(),
1427            SOL_SOCKET,
1428            SO_BINDTODEVICE,
1429            ptr as u64,
1430            length,
1431        )
1432    };
1433    if ret < 0 {
1434        return Err(errno_from_negative_syscall_ret(ret));
1435    }
1436    Ok(())
1437}
1438
1439/// Lit l'interface liée au socket.
1440pub fn get_so_bindtodevice(sock: BorrowedFd<'_>) -> Result<CString, Errno> {
1441    // IFNAMSIZ = 16 octets suffisent pour un nom d'interface Linux
1442    let mut buffer = [0u8; 16];
1443    let mut length: u32 = 16;
1444    let buf_ptr: *mut u8 = buffer.as_mut_ptr();
1445    let len_ptr: *mut u32 = &mut length;
1446    // SAFETY:
1447    // - getsockopt(2) écrit le nom de l'interface (NUL-terminé) dans buffer.
1448    // - buffer et length sont locaux, valides pour la durée du syscall.
1449    // - sock est garanti ouvert par BorrowedFd.
1450    let ret = unsafe {
1451        raw_syscall_getsockopt(
1452            sock.as_raw_fd(),
1453            SOL_SOCKET,
1454            SO_BINDTODEVICE,
1455            buf_ptr as u64,
1456            len_ptr as u64,
1457        )
1458    };
1459    if ret < 0 {
1460        return Err(errno_from_negative_syscall_ret(ret));
1461    }
1462    // Trouver le NUL terminateur pour construire le CString
1463    let nul_pos = buffer
1464        .iter()
1465        .position(|&b| b == 0)
1466        .unwrap_or(length as usize);
1467    // COUVERTURE (ADR-035, STRUCTURAL — cf. docs/COVERAGE-EXCEPTIONS.md, section
1468    // `net`). La closure `map_err` (`EINVAL`) est inatteignable par construction :
1469    // `nul_pos` est la position du **premier** `NUL` (ou `length` à défaut), donc
1470    // `buffer[..nul_pos]` ne contient aucun `NUL` interne → `CString::new`
1471    // retourne toujours `Ok`.
1472    CString::new(&buffer[..nul_pos]).map_err(|_| {
1473        #[allow(clippy::unwrap_used)]
1474        Errno::from_nonzero(NonZeroI32::new(22).unwrap()) // EINVAL
1475    })
1476}
1477
1478/// Définit le délai `TCP_KEEPIDLE` (secondes d'inactivité avant premier keepalive).
1479pub fn set_tcp_keepidle(sock: BorrowedFd<'_>, seconds: u32) -> Result<(), Errno> {
1480    #[allow(clippy::cast_possible_truncation)]
1481    let val = if seconds > i32::MAX as u32 {
1482        i32::MAX
1483    } else {
1484        seconds as i32
1485    };
1486    setsockopt_i32(sock, IPPROTO_TCP, TCP_KEEPIDLE, val)
1487}
1488
1489/// Définit le délai `TCP_KEEPINTVL` (secondes entre deux keepalives).
1490pub fn set_tcp_keepintvl(sock: BorrowedFd<'_>, seconds: u32) -> Result<(), Errno> {
1491    #[allow(clippy::cast_possible_truncation)]
1492    let val = if seconds > i32::MAX as u32 {
1493        i32::MAX
1494    } else {
1495        seconds as i32
1496    };
1497    setsockopt_i32(sock, IPPROTO_TCP, TCP_KEEPINTVL, val)
1498}
1499
1500/// Définit le nombre max de keepalives sans réponse (`TCP_KEEPCNT`).
1501pub fn set_tcp_keepcnt(sock: BorrowedFd<'_>, count: u32) -> Result<(), Errno> {
1502    #[allow(clippy::cast_possible_truncation)]
1503    let val = if count > i32::MAX as u32 {
1504        i32::MAX
1505    } else {
1506        count as i32
1507    };
1508    setsockopt_i32(sock, IPPROTO_TCP, TCP_KEEPCNT, val)
1509}
1510
1511/// Définit le TTL IP (`IP_TTL`).
1512pub fn set_ip_ttl(sock: BorrowedFd<'_>, ttl: u8) -> Result<(), Errno> {
1513    setsockopt_i32(sock, IPPROTO_IP, IP_TTL, i32::from(ttl))
1514}
1515
1516/// Restreint les sockets IPv6 aux connexions IPv6-only (`IPV6_V6ONLY`).
1517pub fn set_ipv6_v6only(sock: BorrowedFd<'_>, v6only: bool) -> Result<(), Errno> {
1518    setsockopt_i32(sock, IPPROTO_IPV6, IPV6_V6ONLY, i32::from(v6only))
1519}
1520
1521// ─────────────────────────────────────────────────────────────────────────
1522// Options à structure `timeval` — délais de réception / d'envoi
1523// (`SO_RCVTIMEO` / `SO_SNDTIMEO`). Options **structurées** (16 octets) : la
1524// couche 0 pose/lit un `Timeval` typé (jamais un `setsockopt` générique).
1525// ─────────────────────────────────────────────────────────────────────────
1526
1527/// Décode un `timeval` **rendu par `getsockopt`** en délai optionnel.
1528///
1529/// Fonction **pure** (aucun syscall) : le kernel écrit une structure `timeval`
1530/// que NOUS relisons ensuite. Un `timeval` nul (`{0, 0}`) signifie « aucun délai
1531/// armé » (le socket bloque indéfiniment) → `None` ; toute autre valeur →
1532/// `Some(timeval)`. Isolé pour être **property-testé** (cf. exigence fuzz du
1533/// décodage `getsockopt`).
1534fn decode_timeout(tv: Timeval) -> Option<Timeval> {
1535    if tv.tv_sec == 0 && tv.tv_usec == 0 {
1536        None
1537    } else {
1538        Some(tv)
1539    }
1540}
1541
1542/// Pose un délai (`SO_RCVTIMEO` / `SO_SNDTIMEO`) via un `timeval` de 16 octets.
1543///
1544/// `None` ⇒ `timeval` nul = « pas de délai » (sémantique kernel : blocage sans
1545/// limite). Helper **interne** paramétré par `optname` (les points d'entrée
1546/// publics restent dédiés, ADR-021 conv. 3).
1547fn set_socket_timeout(
1548    sock: BorrowedFd<'_>,
1549    optname: i32,
1550    timeout: Option<Timeval>,
1551) -> Result<(), Errno> {
1552    let tv = timeout.unwrap_or(Timeval {
1553        tv_sec: 0,
1554        tv_usec: 0,
1555    });
1556    let tv_ptr: *const Timeval = &tv;
1557    // SAFETY:
1558    // - setsockopt(2) lit `TIMEVAL_LEN` (16) octets depuis tv_ptr (struct timeval).
1559    // - tv est local, valide pour la durée du syscall ; layout #[repr(C)] fidèle
1560    //   (assertion const `size_of::<Timeval>() == 16`).
1561    // - sock est garanti ouvert par BorrowedFd.
1562    let ret = unsafe {
1563        raw_syscall_setsockopt(
1564            sock.as_raw_fd(),
1565            SOL_SOCKET,
1566            optname,
1567            tv_ptr as u64,
1568            TIMEVAL_LEN,
1569        )
1570    };
1571    if ret < 0 {
1572        return Err(errno_from_negative_syscall_ret(ret));
1573    }
1574    Ok(())
1575}
1576
1577/// Lit un délai (`SO_RCVTIMEO` / `SO_SNDTIMEO`). Helper **interne** paramétré par
1578/// `optname`.
1579fn get_socket_timeout(sock: BorrowedFd<'_>, optname: i32) -> Result<Option<Timeval>, Errno> {
1580    let mut tv = Timeval {
1581        tv_sec: 0,
1582        tv_usec: 0,
1583    };
1584    let mut length: u32 = TIMEVAL_LEN;
1585    let tv_ptr: *mut Timeval = &mut tv;
1586    let len_ptr: *mut u32 = &mut length;
1587    // SAFETY:
1588    // - getsockopt(2) écrit au plus `length` (16) octets dans tv (struct timeval).
1589    // - tv et length sont locaux, valides pour la durée du syscall ; layout
1590    //   #[repr(C)] fidèle (assertion const `size_of::<Timeval>() == 16`).
1591    // - sock est garanti ouvert par BorrowedFd.
1592    let ret = unsafe {
1593        raw_syscall_getsockopt(
1594            sock.as_raw_fd(),
1595            SOL_SOCKET,
1596            optname,
1597            tv_ptr as u64,
1598            len_ptr as u64,
1599        )
1600    };
1601    if ret < 0 {
1602        return Err(errno_from_negative_syscall_ret(ret));
1603    }
1604    Ok(decode_timeout(tv))
1605}
1606
1607/// Définit le délai de **réception** (`SO_RCVTIMEO`). `None` désarme le délai.
1608pub fn set_receive_timeout(sock: BorrowedFd<'_>, timeout: Option<Timeval>) -> Result<(), Errno> {
1609    set_socket_timeout(sock, SO_RCVTIMEO, timeout)
1610}
1611
1612/// Lit le délai de **réception** (`SO_RCVTIMEO`). `None` = aucun délai armé.
1613pub fn receive_timeout(sock: BorrowedFd<'_>) -> Result<Option<Timeval>, Errno> {
1614    get_socket_timeout(sock, SO_RCVTIMEO)
1615}
1616
1617/// Définit le délai d'**envoi** (`SO_SNDTIMEO`). `None` désarme le délai.
1618pub fn set_send_timeout(sock: BorrowedFd<'_>, timeout: Option<Timeval>) -> Result<(), Errno> {
1619    set_socket_timeout(sock, SO_SNDTIMEO, timeout)
1620}
1621
1622/// Lit le délai d'**envoi** (`SO_SNDTIMEO`). `None` = aucun délai armé.
1623pub fn send_timeout(sock: BorrowedFd<'_>) -> Result<Option<Timeval>, Errno> {
1624    get_socket_timeout(sock, SO_SNDTIMEO)
1625}
1626
1627// ─────────────────────────────────────────────────────────────────────────
1628// Appartenance à un groupe multicast — options **structurées**
1629// `ip_mreq` (v4) / `ipv6_mreq` (v6). Fonctions dédiées typées (ADR-021).
1630// ─────────────────────────────────────────────────────────────────────────
1631
1632/// Pose une option d'appartenance multicast **IPv4** (`ip_mreq`, 8 octets).
1633/// Helper **interne** paramétré par `optname` (`IP_ADD`/`IP_DROP_MEMBERSHIP`).
1634fn set_ip_membership_v4(
1635    sock: BorrowedFd<'_>,
1636    optname: i32,
1637    membership: &IpMembershipV4,
1638) -> Result<(), Errno> {
1639    let ptr: *const IpMembershipV4 = membership;
1640    // SAFETY:
1641    // - setsockopt(2) lit `IP_MREQ_LEN` (8) octets depuis ptr (struct ip_mreq).
1642    // - `membership` est valide pour la durée de l'appel (référence empruntée) ;
1643    //   layout #[repr(C)] fidèle (assertion const `size_of == 8`).
1644    // - sock est garanti ouvert par BorrowedFd.
1645    let ret = unsafe {
1646        raw_syscall_setsockopt(
1647            sock.as_raw_fd(),
1648            IPPROTO_IP,
1649            optname,
1650            ptr as u64,
1651            IP_MREQ_LEN,
1652        )
1653    };
1654    if ret < 0 {
1655        return Err(errno_from_negative_syscall_ret(ret));
1656    }
1657    Ok(())
1658}
1659
1660/// Pose une option d'appartenance multicast **IPv6** (`ipv6_mreq`, 20 octets).
1661/// Helper **interne** paramétré par `optname` (`IPV6_ADD`/`IPV6_DROP_MEMBERSHIP`).
1662fn set_ip_membership_v6(
1663    sock: BorrowedFd<'_>,
1664    optname: i32,
1665    membership: &IpMembershipV6,
1666) -> Result<(), Errno> {
1667    let ptr: *const IpMembershipV6 = membership;
1668    // SAFETY:
1669    // - setsockopt(2) lit `IPV6_MREQ_LEN` (20) octets depuis ptr (struct ipv6_mreq).
1670    // - `membership` est valide pour la durée de l'appel (référence empruntée) ;
1671    //   layout #[repr(C)] fidèle (assertion const `size_of == 20`).
1672    // - sock est garanti ouvert par BorrowedFd.
1673    let ret = unsafe {
1674        raw_syscall_setsockopt(
1675            sock.as_raw_fd(),
1676            IPPROTO_IPV6,
1677            optname,
1678            ptr as u64,
1679            IPV6_MREQ_LEN,
1680        )
1681    };
1682    if ret < 0 {
1683        return Err(errno_from_negative_syscall_ret(ret));
1684    }
1685    Ok(())
1686}
1687
1688/// **Rejoint** un groupe multicast IPv4 (`IP_ADD_MEMBERSHIP`, `struct ip_mreq`).
1689pub fn join_multicast_v4(sock: BorrowedFd<'_>, membership: &IpMembershipV4) -> Result<(), Errno> {
1690    set_ip_membership_v4(sock, IP_ADD_MEMBERSHIP, membership)
1691}
1692
1693/// **Quitte** un groupe multicast IPv4 (`IP_DROP_MEMBERSHIP`, `struct ip_mreq`).
1694pub fn leave_multicast_v4(sock: BorrowedFd<'_>, membership: &IpMembershipV4) -> Result<(), Errno> {
1695    set_ip_membership_v4(sock, IP_DROP_MEMBERSHIP, membership)
1696}
1697
1698/// **Rejoint** un groupe multicast IPv6 (`IPV6_ADD_MEMBERSHIP`, `struct ipv6_mreq`).
1699pub fn join_multicast_v6(sock: BorrowedFd<'_>, membership: &IpMembershipV6) -> Result<(), Errno> {
1700    set_ip_membership_v6(sock, IPV6_ADD_MEMBERSHIP, membership)
1701}
1702
1703/// **Quitte** un groupe multicast IPv6 (`IPV6_DROP_MEMBERSHIP`, `struct ipv6_mreq`).
1704pub fn leave_multicast_v6(sock: BorrowedFd<'_>, membership: &IpMembershipV6) -> Result<(), Errno> {
1705    set_ip_membership_v6(sock, IPV6_DROP_MEMBERSHIP, membership)
1706}
1707
1708// ─────────────────────────────────────────────────────────────────────────
1709// Helpers syscall bruts — x86_64
1710// ─────────────────────────────────────────────────────────────────────────
1711
1712#[cfg(target_arch = "x86_64")]
1713#[inline]
1714unsafe fn raw_syscall_socket(domain: i32, ty: i32, protocol: i32) -> i64 {
1715    let ret: i64;
1716    // SAFETY: SYS_socket (x86_64 = 41). Pas d'accès mémoire utilisateur.
1717    unsafe {
1718        core::arch::asm!(
1719            "syscall",
1720            in("rax") 41_i64,
1721            in("rdi") i64::from(domain),
1722            in("rsi") i64::from(ty),
1723            in("rdx") i64::from(protocol),
1724            lateout("rax") ret,
1725            lateout("rcx") _,
1726            lateout("r11") _,
1727            options(nostack, preserves_flags),
1728        );
1729    }
1730    ret
1731}
1732
1733#[cfg(target_arch = "x86_64")]
1734#[inline]
1735unsafe fn raw_syscall_bind(fd: i32, address: u64, addrlen: u32) -> i64 {
1736    let ret: i64;
1737    // SAFETY: SYS_bind (x86_64 = 49). Le kernel lit `addrlen` octets depuis `address`.
1738    unsafe {
1739        core::arch::asm!(
1740            "syscall",
1741            in("rax") 49_i64,
1742            in("rdi") i64::from(fd),
1743            in("rsi") address,
1744            in("rdx") i64::from(addrlen),
1745            lateout("rax") ret,
1746            lateout("rcx") _,
1747            lateout("r11") _,
1748            options(nostack, preserves_flags),
1749        );
1750    }
1751    ret
1752}
1753
1754#[cfg(target_arch = "x86_64")]
1755#[inline]
1756unsafe fn raw_syscall_listen(fd: i32, backlog: i32) -> i64 {
1757    let ret: i64;
1758    // SAFETY: SYS_listen (x86_64 = 50). Pas d'accès mémoire utilisateur.
1759    unsafe {
1760        core::arch::asm!(
1761            "syscall",
1762            in("rax") 50_i64,
1763            in("rdi") i64::from(fd),
1764            in("rsi") i64::from(backlog),
1765            lateout("rax") ret,
1766            lateout("rcx") _,
1767            lateout("r11") _,
1768            options(nostack, preserves_flags),
1769        );
1770    }
1771    ret
1772}
1773
1774#[cfg(target_arch = "x86_64")]
1775#[inline]
1776unsafe fn raw_syscall_connect(fd: i32, address: u64, addrlen: u32) -> i64 {
1777    let ret: i64;
1778    // SAFETY: SYS_connect (x86_64 = 42). Le kernel lit `addrlen` octets depuis `address`.
1779    unsafe {
1780        core::arch::asm!(
1781            "syscall",
1782            in("rax") 42_i64,
1783            in("rdi") i64::from(fd),
1784            in("rsi") address,
1785            in("rdx") i64::from(addrlen),
1786            lateout("rax") ret,
1787            lateout("rcx") _,
1788            lateout("r11") _,
1789            options(nostack, preserves_flags),
1790        );
1791    }
1792    ret
1793}
1794
1795#[cfg(target_arch = "x86_64")]
1796#[inline]
1797unsafe fn raw_syscall_accept4(fd: i32, address: u64, addrlen: u64, flags: i32) -> i64 {
1798    let ret: i64;
1799    // SAFETY: SYS_accept4 (x86_64 = 288). Le kernel écrit l'adresse du pair dans address.
1800    unsafe {
1801        core::arch::asm!(
1802            "syscall",
1803            in("rax") 288_i64,
1804            in("rdi") i64::from(fd),
1805            in("rsi") address,
1806            in("rdx") addrlen,
1807            in("r10") i64::from(flags),
1808            lateout("rax") ret,
1809            lateout("rcx") _,
1810            lateout("r11") _,
1811            options(nostack, preserves_flags),
1812        );
1813    }
1814    ret
1815}
1816
1817#[cfg(target_arch = "x86_64")]
1818#[inline]
1819unsafe fn raw_syscall_sendto(
1820    fd: i32,
1821    buffer: u64,
1822    length: usize,
1823    flags: u64,
1824    address: u64,
1825    addrlen: u32,
1826) -> i64 {
1827    let ret: i64;
1828    // SAFETY: SYS_sendto (x86_64 = 44). Le kernel lit `length` octets depuis `buffer`.
1829    unsafe {
1830        core::arch::asm!(
1831            "syscall",
1832            in("rax") 44_i64,
1833            in("rdi") i64::from(fd),
1834            in("rsi") buffer,
1835            in("rdx") length,
1836            in("r10") flags,
1837            in("r8") address,
1838            in("r9") i64::from(addrlen),
1839            lateout("rax") ret,
1840            lateout("rcx") _,
1841            lateout("r11") _,
1842            options(nostack, preserves_flags),
1843        );
1844    }
1845    ret
1846}
1847
1848#[cfg(target_arch = "x86_64")]
1849#[inline]
1850unsafe fn raw_syscall_recvfrom(
1851    fd: i32,
1852    buffer: u64,
1853    length: usize,
1854    flags: u64,
1855    address: u64,
1856    addrlen: u64,
1857) -> i64 {
1858    let ret: i64;
1859    // SAFETY: SYS_recvfrom (x86_64 = 45). Le kernel écrit au plus `length` octets dans `buffer`.
1860    unsafe {
1861        core::arch::asm!(
1862            "syscall",
1863            in("rax") 45_i64,
1864            in("rdi") i64::from(fd),
1865            in("rsi") buffer,
1866            in("rdx") length,
1867            in("r10") flags,
1868            in("r8") address,
1869            in("r9") addrlen,
1870            lateout("rax") ret,
1871            lateout("rcx") _,
1872            lateout("r11") _,
1873            options(nostack, preserves_flags),
1874        );
1875    }
1876    ret
1877}
1878
1879#[cfg(target_arch = "x86_64")]
1880#[inline]
1881unsafe fn raw_syscall_sendmsg(fd: i32, msghdr: u64, flags: u64) -> i64 {
1882    let ret: i64;
1883    // SAFETY: SYS_sendmsg (x86_64 = 46). Le kernel lit la msghdr et les buffers associés.
1884    unsafe {
1885        core::arch::asm!(
1886            "syscall",
1887            in("rax") 46_i64,
1888            in("rdi") i64::from(fd),
1889            in("rsi") msghdr,
1890            in("rdx") flags,
1891            lateout("rax") ret,
1892            lateout("rcx") _,
1893            lateout("r11") _,
1894            options(nostack, preserves_flags),
1895        );
1896    }
1897    ret
1898}
1899
1900#[cfg(target_arch = "x86_64")]
1901#[inline]
1902unsafe fn raw_syscall_recvmsg(fd: i32, msghdr: u64, flags: u64) -> i64 {
1903    let ret: i64;
1904    // SAFETY: SYS_recvmsg (x86_64 = 47). Le kernel écrit dans les buffers iov et cmsg.
1905    unsafe {
1906        core::arch::asm!(
1907            "syscall",
1908            in("rax") 47_i64,
1909            in("rdi") i64::from(fd),
1910            in("rsi") msghdr,
1911            in("rdx") flags,
1912            lateout("rax") ret,
1913            lateout("rcx") _,
1914            lateout("r11") _,
1915            options(nostack, preserves_flags),
1916        );
1917    }
1918    ret
1919}
1920
1921#[cfg(target_arch = "x86_64")]
1922#[inline]
1923unsafe fn raw_syscall_shutdown(fd: i32, how: i32) -> i64 {
1924    let ret: i64;
1925    // SAFETY: SYS_shutdown (x86_64 = 48). Pas d'accès mémoire utilisateur.
1926    unsafe {
1927        core::arch::asm!(
1928            "syscall",
1929            in("rax") 48_i64,
1930            in("rdi") i64::from(fd),
1931            in("rsi") i64::from(how),
1932            lateout("rax") ret,
1933            lateout("rcx") _,
1934            lateout("r11") _,
1935            options(nostack, preserves_flags),
1936        );
1937    }
1938    ret
1939}
1940
1941#[cfg(target_arch = "x86_64")]
1942#[inline]
1943unsafe fn raw_syscall_getsockname(fd: i32, address: u64, addrlen: u64) -> i64 {
1944    let ret: i64;
1945    // SAFETY: SYS_getsockname (x86_64 = 51). Le kernel écrit l'adresse et met à jour addrlen.
1946    unsafe {
1947        core::arch::asm!(
1948            "syscall",
1949            in("rax") 51_i64,
1950            in("rdi") i64::from(fd),
1951            in("rsi") address,
1952            in("rdx") addrlen,
1953            lateout("rax") ret,
1954            lateout("rcx") _,
1955            lateout("r11") _,
1956            options(nostack, preserves_flags),
1957        );
1958    }
1959    ret
1960}
1961
1962#[cfg(target_arch = "x86_64")]
1963#[inline]
1964unsafe fn raw_syscall_getpeername(fd: i32, address: u64, addrlen: u64) -> i64 {
1965    let ret: i64;
1966    // SAFETY: SYS_getpeername (x86_64 = 52). Le kernel écrit l'adresse et met à jour addrlen.
1967    unsafe {
1968        core::arch::asm!(
1969            "syscall",
1970            in("rax") 52_i64,
1971            in("rdi") i64::from(fd),
1972            in("rsi") address,
1973            in("rdx") addrlen,
1974            lateout("rax") ret,
1975            lateout("rcx") _,
1976            lateout("r11") _,
1977            options(nostack, preserves_flags),
1978        );
1979    }
1980    ret
1981}
1982
1983#[cfg(target_arch = "x86_64")]
1984#[inline]
1985unsafe fn raw_syscall_socketpair(domain: i32, ty: i32, protocol: i32, sv: u64) -> i64 {
1986    let ret: i64;
1987    // SAFETY: SYS_socketpair (x86_64 = 53). Le kernel écrit deux FDs dans sv[0..2].
1988    unsafe {
1989        core::arch::asm!(
1990            "syscall",
1991            in("rax") 53_i64,
1992            in("rdi") i64::from(domain),
1993            in("rsi") i64::from(ty),
1994            in("rdx") i64::from(protocol),
1995            in("r10") sv,
1996            lateout("rax") ret,
1997            lateout("rcx") _,
1998            lateout("r11") _,
1999            options(nostack, preserves_flags),
2000        );
2001    }
2002    ret
2003}
2004
2005#[cfg(target_arch = "x86_64")]
2006#[inline]
2007unsafe fn raw_syscall_getsockopt(
2008    fd: i32,
2009    level: i32,
2010    optname: i32,
2011    optval: u64,
2012    optlen: u64,
2013) -> i64 {
2014    let ret: i64;
2015    // SAFETY: SYS_getsockopt (x86_64 = 55). Le kernel écrit *optlen octets dans optval.
2016    unsafe {
2017        core::arch::asm!(
2018            "syscall",
2019            in("rax") 55_i64,
2020            in("rdi") i64::from(fd),
2021            in("rsi") i64::from(level),
2022            in("rdx") i64::from(optname),
2023            in("r10") optval,
2024            in("r8") optlen,
2025            lateout("rax") ret,
2026            lateout("rcx") _,
2027            lateout("r11") _,
2028            options(nostack, preserves_flags),
2029        );
2030    }
2031    ret
2032}
2033
2034#[cfg(target_arch = "x86_64")]
2035#[inline]
2036unsafe fn raw_syscall_setsockopt(
2037    fd: i32,
2038    level: i32,
2039    optname: i32,
2040    optval: u64,
2041    optlen: u32,
2042) -> i64 {
2043    let ret: i64;
2044    // SAFETY: SYS_setsockopt (x86_64 = 54). Le kernel lit optlen octets depuis optval.
2045    unsafe {
2046        core::arch::asm!(
2047            "syscall",
2048            in("rax") 54_i64,
2049            in("rdi") i64::from(fd),
2050            in("rsi") i64::from(level),
2051            in("rdx") i64::from(optname),
2052            in("r10") optval,
2053            in("r8") i64::from(optlen),
2054            lateout("rax") ret,
2055            lateout("rcx") _,
2056            lateout("r11") _,
2057            options(nostack, preserves_flags),
2058        );
2059    }
2060    ret
2061}
2062
2063// ─────────────────────────────────────────────────────────────────────────
2064// Helpers syscall bruts — aarch64
2065// ─────────────────────────────────────────────────────────────────────────
2066
2067#[cfg(target_arch = "aarch64")]
2068#[inline]
2069unsafe fn raw_syscall_socket(domain: i32, ty: i32, protocol: i32) -> i64 {
2070    let ret: i64;
2071    // SAFETY: SYS_socket (aarch64 = 198). Pas d'accès mémoire utilisateur.
2072    unsafe {
2073        core::arch::asm!(
2074            "svc 0",
2075            in("x8") 198_i64,
2076            inout("x0") i64::from(domain) => ret,
2077            in("x1") i64::from(ty),
2078            in("x2") i64::from(protocol),
2079            options(nostack, preserves_flags),
2080        );
2081    }
2082    ret
2083}
2084
2085#[cfg(target_arch = "aarch64")]
2086#[inline]
2087unsafe fn raw_syscall_bind(fd: i32, address: u64, addrlen: u32) -> i64 {
2088    let ret: i64;
2089    // SAFETY: SYS_bind (aarch64 = 200). Le kernel lit `addrlen` octets depuis `address`.
2090    unsafe {
2091        core::arch::asm!(
2092            "svc 0",
2093            in("x8") 200_i64,
2094            inout("x0") i64::from(fd) => ret,
2095            in("x1") address,
2096            in("x2") i64::from(addrlen),
2097            options(nostack, preserves_flags),
2098        );
2099    }
2100    ret
2101}
2102
2103#[cfg(target_arch = "aarch64")]
2104#[inline]
2105unsafe fn raw_syscall_listen(fd: i32, backlog: i32) -> i64 {
2106    let ret: i64;
2107    // SAFETY: SYS_listen (aarch64 = 201). Pas d'accès mémoire utilisateur.
2108    unsafe {
2109        core::arch::asm!(
2110            "svc 0",
2111            in("x8") 201_i64,
2112            inout("x0") i64::from(fd) => ret,
2113            in("x1") i64::from(backlog),
2114            options(nostack, preserves_flags),
2115        );
2116    }
2117    ret
2118}
2119
2120#[cfg(target_arch = "aarch64")]
2121#[inline]
2122unsafe fn raw_syscall_connect(fd: i32, address: u64, addrlen: u32) -> i64 {
2123    let ret: i64;
2124    // SAFETY: SYS_connect (aarch64 = 203). Le kernel lit `addrlen` octets depuis `address`.
2125    unsafe {
2126        core::arch::asm!(
2127            "svc 0",
2128            in("x8") 203_i64,
2129            inout("x0") i64::from(fd) => ret,
2130            in("x1") address,
2131            in("x2") i64::from(addrlen),
2132            options(nostack, preserves_flags),
2133        );
2134    }
2135    ret
2136}
2137
2138#[cfg(target_arch = "aarch64")]
2139#[inline]
2140unsafe fn raw_syscall_accept4(fd: i32, address: u64, addrlen: u64, flags: i32) -> i64 {
2141    let ret: i64;
2142    // SAFETY: SYS_accept4 (aarch64 = 242). Le kernel écrit l'adresse du pair dans address.
2143    unsafe {
2144        core::arch::asm!(
2145            "svc 0",
2146            in("x8") 242_i64,
2147            inout("x0") i64::from(fd) => ret,
2148            in("x1") address,
2149            in("x2") addrlen,
2150            in("x3") i64::from(flags),
2151            options(nostack, preserves_flags),
2152        );
2153    }
2154    ret
2155}
2156
2157#[cfg(target_arch = "aarch64")]
2158#[inline]
2159unsafe fn raw_syscall_sendto(
2160    fd: i32,
2161    buffer: u64,
2162    length: usize,
2163    flags: u64,
2164    address: u64,
2165    addrlen: u32,
2166) -> i64 {
2167    let ret: i64;
2168    // SAFETY: SYS_sendto (aarch64 = 206). Le kernel lit `length` octets depuis `buffer`.
2169    unsafe {
2170        core::arch::asm!(
2171            "svc 0",
2172            in("x8") 206_i64,
2173            inout("x0") i64::from(fd) => ret,
2174            in("x1") buffer,
2175            in("x2") length,
2176            in("x3") flags,
2177            in("x4") address,
2178            in("x5") i64::from(addrlen),
2179            options(nostack, preserves_flags),
2180        );
2181    }
2182    ret
2183}
2184
2185#[cfg(target_arch = "aarch64")]
2186#[inline]
2187unsafe fn raw_syscall_recvfrom(
2188    fd: i32,
2189    buffer: u64,
2190    length: usize,
2191    flags: u64,
2192    address: u64,
2193    addrlen: u64,
2194) -> i64 {
2195    let ret: i64;
2196    // SAFETY: SYS_recvfrom (aarch64 = 207). Le kernel écrit au plus `length` octets dans `buffer`.
2197    unsafe {
2198        core::arch::asm!(
2199            "svc 0",
2200            in("x8") 207_i64,
2201            inout("x0") i64::from(fd) => ret,
2202            in("x1") buffer,
2203            in("x2") length,
2204            in("x3") flags,
2205            in("x4") address,
2206            in("x5") addrlen,
2207            options(nostack, preserves_flags),
2208        );
2209    }
2210    ret
2211}
2212
2213#[cfg(target_arch = "aarch64")]
2214#[inline]
2215unsafe fn raw_syscall_sendmsg(fd: i32, msghdr: u64, flags: u64) -> i64 {
2216    let ret: i64;
2217    // SAFETY: SYS_sendmsg (aarch64 = 211). Le kernel lit la msghdr et les buffers associés.
2218    unsafe {
2219        core::arch::asm!(
2220            "svc 0",
2221            in("x8") 211_i64,
2222            inout("x0") i64::from(fd) => ret,
2223            in("x1") msghdr,
2224            in("x2") flags,
2225            options(nostack, preserves_flags),
2226        );
2227    }
2228    ret
2229}
2230
2231#[cfg(target_arch = "aarch64")]
2232#[inline]
2233unsafe fn raw_syscall_recvmsg(fd: i32, msghdr: u64, flags: u64) -> i64 {
2234    let ret: i64;
2235    // SAFETY: SYS_recvmsg (aarch64 = 212). Le kernel écrit dans les buffers iov et cmsg.
2236    unsafe {
2237        core::arch::asm!(
2238            "svc 0",
2239            in("x8") 212_i64,
2240            inout("x0") i64::from(fd) => ret,
2241            in("x1") msghdr,
2242            in("x2") flags,
2243            options(nostack, preserves_flags),
2244        );
2245    }
2246    ret
2247}
2248
2249#[cfg(target_arch = "aarch64")]
2250#[inline]
2251unsafe fn raw_syscall_shutdown(fd: i32, how: i32) -> i64 {
2252    let ret: i64;
2253    // SAFETY: SYS_shutdown (aarch64 = 210). Pas d'accès mémoire utilisateur.
2254    unsafe {
2255        core::arch::asm!(
2256            "svc 0",
2257            in("x8") 210_i64,
2258            inout("x0") i64::from(fd) => ret,
2259            in("x1") i64::from(how),
2260            options(nostack, preserves_flags),
2261        );
2262    }
2263    ret
2264}
2265
2266#[cfg(target_arch = "aarch64")]
2267#[inline]
2268unsafe fn raw_syscall_getsockname(fd: i32, address: u64, addrlen: u64) -> i64 {
2269    let ret: i64;
2270    // SAFETY: SYS_getsockname (aarch64 = 204). Le kernel écrit l'adresse et met à jour addrlen.
2271    unsafe {
2272        core::arch::asm!(
2273            "svc 0",
2274            in("x8") 204_i64,
2275            inout("x0") i64::from(fd) => ret,
2276            in("x1") address,
2277            in("x2") addrlen,
2278            options(nostack, preserves_flags),
2279        );
2280    }
2281    ret
2282}
2283
2284#[cfg(target_arch = "aarch64")]
2285#[inline]
2286unsafe fn raw_syscall_getpeername(fd: i32, address: u64, addrlen: u64) -> i64 {
2287    let ret: i64;
2288    // SAFETY: SYS_getpeername (aarch64 = 205). Le kernel écrit l'adresse et met à jour addrlen.
2289    unsafe {
2290        core::arch::asm!(
2291            "svc 0",
2292            in("x8") 205_i64,
2293            inout("x0") i64::from(fd) => ret,
2294            in("x1") address,
2295            in("x2") addrlen,
2296            options(nostack, preserves_flags),
2297        );
2298    }
2299    ret
2300}
2301
2302#[cfg(target_arch = "aarch64")]
2303#[inline]
2304unsafe fn raw_syscall_socketpair(domain: i32, ty: i32, protocol: i32, sv: u64) -> i64 {
2305    let ret: i64;
2306    // SAFETY: SYS_socketpair (aarch64 = 199). Le kernel écrit deux FDs dans sv[0..2].
2307    unsafe {
2308        core::arch::asm!(
2309            "svc 0",
2310            in("x8") 199_i64,
2311            inout("x0") i64::from(domain) => ret,
2312            in("x1") i64::from(ty),
2313            in("x2") i64::from(protocol),
2314            in("x3") sv,
2315            options(nostack, preserves_flags),
2316        );
2317    }
2318    ret
2319}
2320
2321#[cfg(target_arch = "aarch64")]
2322#[inline]
2323unsafe fn raw_syscall_getsockopt(
2324    fd: i32,
2325    level: i32,
2326    optname: i32,
2327    optval: u64,
2328    optlen: u64,
2329) -> i64 {
2330    let ret: i64;
2331    // SAFETY: SYS_getsockopt (aarch64 = 209). Le kernel écrit *optlen octets dans optval.
2332    unsafe {
2333        core::arch::asm!(
2334            "svc 0",
2335            in("x8") 209_i64,
2336            inout("x0") i64::from(fd) => ret,
2337            in("x1") i64::from(level),
2338            in("x2") i64::from(optname),
2339            in("x3") optval,
2340            in("x4") optlen,
2341            options(nostack, preserves_flags),
2342        );
2343    }
2344    ret
2345}
2346
2347#[cfg(target_arch = "aarch64")]
2348#[inline]
2349unsafe fn raw_syscall_setsockopt(
2350    fd: i32,
2351    level: i32,
2352    optname: i32,
2353    optval: u64,
2354    optlen: u32,
2355) -> i64 {
2356    let ret: i64;
2357    // SAFETY: SYS_setsockopt (aarch64 = 208). Le kernel lit optlen octets depuis optval.
2358    unsafe {
2359        core::arch::asm!(
2360            "svc 0",
2361            in("x8") 208_i64,
2362            inout("x0") i64::from(fd) => ret,
2363            in("x1") i64::from(level),
2364            in("x2") i64::from(optname),
2365            in("x3") optval,
2366            in("x4") i64::from(optlen),
2367            options(nostack, preserves_flags),
2368        );
2369    }
2370    ret
2371}
2372
2373// ─────────────────────────────────────────────────────────────────────────
2374// Tests
2375// ─────────────────────────────────────────────────────────────────────────
2376
2377#[cfg(all(test, target_os = "linux"))]
2378mod tests {
2379    use super::*;
2380    use air_sys_types::fd::AsFd;
2381
2382    // errno standard non exposés par l'API publique (gelée). Valeurs
2383    // asm-generic/errno.h, identiques sur x86_64 et aarch64.
2384    const EADDRINUSE: Errno = Errno::try_from_raw(98).unwrap();
2385    const ECONNREFUSED: Errno = Errno::try_from_raw(111).unwrap();
2386    const ENOTCONN: Errno = Errno::try_from_raw(107).unwrap();
2387    const ESOCKTNOSUPPORT: Errno = Errno::try_from_raw(94).unwrap();
2388
2389    /// Un `BorrowedFd` jamais valide, pour provoquer `EBADF` de façon
2390    /// déterministe. `i32::MAX` dépasse toute limite de fd ouvrable : le
2391    /// kernel rejette systématiquement, sans course de réutilisation
2392    /// (contrairement à un fd réel fermé). `-1` est exclu car c'est la
2393    /// valeur-niche de `BorrowedFd` (`borrow_raw(-1)` panique `fd != -1`).
2394    fn invalid_fd() -> BorrowedFd<'static> {
2395        // SAFETY: aucune ressource n'est lue/écrite via ce fd ; il n'est
2396        // transmis au kernel que pour être rejeté avec `EBADF`.
2397        unsafe { BorrowedFd::borrow_raw(i32::MAX) }
2398    }
2399
2400    /// `socket` crée un FD valide.
2401    #[test]
2402    fn socket_creates_valid_fd() {
2403        let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket Unix stream");
2404        // On vérifie que le FD est valide en récupérant une vue empruntée.
2405        let _borrowed = fd.as_fd();
2406    }
2407
2408    /// `socketpair` crée deux FDs distincts et symétriques.
2409    #[test]
2410    fn socketpair_creates_two_valid_fds() {
2411        let (fd_a, fd_b) =
2412            socketpair(SocketDomain::Unix, SocketType::Stream, 0).expect("socketpair");
2413        let raw_a = fd_a.as_raw_fd();
2414        let raw_b = fd_b.as_raw_fd();
2415        assert_ne!(raw_a, raw_b, "les deux FDs doivent être distincts");
2416    }
2417
2418    /// `send` + `recv` via socketpair : roundtrip simple.
2419    #[test]
2420    fn socketpair_send_recv_roundtrip() {
2421        let (fd_a, fd_b) =
2422            socketpair(SocketDomain::Unix, SocketType::Stream, 0).expect("socketpair");
2423
2424        let data = b"bonjour";
2425        let n = send(fd_a.as_fd(), data, MessageFlags::empty()).expect("send");
2426        assert_eq!(n, data.len(), "send doit envoyer tous les octets");
2427
2428        let mut buffer = [0u8; 16];
2429        let m = recv(fd_b.as_fd(), &mut buffer, MessageFlags::empty()).expect("recv");
2430        assert_eq!(
2431            m,
2432            data.len(),
2433            "recv doit recevoir autant d'octets qu'envoyés"
2434        );
2435        assert_eq!(
2436            &buffer[..m],
2437            data,
2438            "les données reçues doivent être identiques"
2439        );
2440    }
2441
2442    /// `sendmsg` + `recvmsg` : roundtrip vectoriel.
2443    #[test]
2444    fn socketpair_sendmsg_recvmsg_roundtrip() {
2445        let (fd_a, fd_b) =
2446            socketpair(SocketDomain::Unix, SocketType::Stream, 0).expect("socketpair");
2447
2448        let data1 = b"hello";
2449        let data2 = b" world";
2450        let iov_send = [IoSlice::new(data1), IoSlice::new(data2)];
2451        let req_send = SendMessageRequest {
2452            iov: &iov_send,
2453            address: None,
2454            fds: &[],
2455            flags: MessageFlags::empty(),
2456        };
2457        let result = sendmsg(fd_a.as_fd(), &req_send).expect("sendmsg");
2458        assert_eq!(
2459            result.bytes_sent,
2460            data1.len() + data2.len(),
2461            "sendmsg doit envoyer 11 octets"
2462        );
2463
2464        let mut buffer = [0u8; 32];
2465        let mut iov_recv = [IoSliceMut::new(&mut buffer)];
2466        let mut req_recv = ReceiveMessageRequest {
2467            iov: &mut iov_recv,
2468            flags: MessageFlags::empty(),
2469        };
2470        let result_recv = recvmsg(fd_b.as_fd(), &mut req_recv).expect("recvmsg");
2471        assert_eq!(
2472            result_recv.bytes_received,
2473            data1.len() + data2.len(),
2474            "recvmsg doit recevoir 11 octets"
2475        );
2476        assert_eq!(&buffer[..result_recv.bytes_received], b"hello world");
2477    }
2478
2479    /// `bind` + `listen` + `connect` + `accept4` sur socket Unix path-based.
2480    #[test]
2481    fn unix_bind_listen_connect_accept4() {
2482        // Utiliser un chemin temporaire unique
2483        let path = format!("/tmp/air-net-test-{}.sock", std::process::id());
2484        let cpath = CString::new(path.as_str()).expect("CString");
2485
2486        // Supprimer un éventuel fichier résiduel
2487        let _ = std::fs::remove_file(&path);
2488
2489        let server = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket serveur");
2490        bind(
2491            server.as_fd(),
2492            &SocketAddr::Unix(UnixSocketAddr::Path(cpath.clone())),
2493        )
2494        .expect("bind");
2495        listen(server.as_fd(), 4).expect("listen");
2496
2497        let client = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket client");
2498        connect(
2499            client.as_fd(),
2500            &SocketAddr::Unix(UnixSocketAddr::Path(cpath.clone())),
2501        )
2502        .expect("connect");
2503
2504        let accepted = accept4(server.as_fd(), AcceptFlags::CLOEXEC).expect("accept4");
2505        // Le FD accepté doit être valide
2506        let _borrowed = accepted.fd.as_fd();
2507
2508        // Nettoyage
2509        let _ = std::fs::remove_file(&path);
2510    }
2511
2512    /// `getsockname` sur socket Unix lié retourne l'adresse correcte.
2513    #[test]
2514    fn getsockname_unix_path() {
2515        let path = format!("/tmp/air-net-gsn-{}.sock", std::process::id());
2516        let cpath = CString::new(path.as_str()).expect("CString");
2517        let _ = std::fs::remove_file(&path);
2518
2519        let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket");
2520        bind(
2521            fd.as_fd(),
2522            &SocketAddr::Unix(UnixSocketAddr::Path(cpath.clone())),
2523        )
2524        .expect("bind");
2525
2526        let address = getsockname(fd.as_fd()).expect("getsockname");
2527        match address {
2528            SocketAddr::Unix(UnixSocketAddr::Path(returned)) => {
2529                assert_eq!(returned, cpath, "getsockname doit retourner le chemin lié");
2530            }
2531            other => panic!("adresse inattendue : {other:?}"),
2532        }
2533
2534        let _ = std::fs::remove_file(&path);
2535    }
2536
2537    /// `getpeername` sur une paire de sockets connectés.
2538    #[test]
2539    fn getpeername_unix_abstract() {
2540        // Un socket abstract permet de tester getpeername sans fichier temporaire.
2541        let path = format!("/tmp/air-net-gpn-{}.sock", std::process::id());
2542        let cpath = CString::new(path.as_str()).expect("CString");
2543        let _ = std::fs::remove_file(&path);
2544
2545        let server = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket serveur");
2546        bind(
2547            server.as_fd(),
2548            &SocketAddr::Unix(UnixSocketAddr::Path(cpath.clone())),
2549        )
2550        .expect("bind");
2551        listen(server.as_fd(), 4).expect("listen");
2552
2553        let client = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket client");
2554        connect(
2555            client.as_fd(),
2556            &SocketAddr::Unix(UnixSocketAddr::Path(cpath.clone())),
2557        )
2558        .expect("connect");
2559
2560        let accepted = accept4(server.as_fd(), AcceptFlags::CLOEXEC).expect("accept4");
2561
2562        // getpeername sur le FD accepté doit retourner l'adresse du serveur
2563        let peer = getpeername(client.as_fd()).expect("getpeername");
2564        match peer {
2565            SocketAddr::Unix(_) => { /* OK */ }
2566            other => panic!("adresse inattendue : {other:?}"),
2567        }
2568
2569        drop(accepted);
2570        let _ = std::fs::remove_file(&path);
2571    }
2572
2573    /// `shutdown` ne retourne pas d'erreur sur un socket connecté.
2574    #[test]
2575    fn shutdown_on_connected_socket() {
2576        let (fd_a, _fd_b) =
2577            socketpair(SocketDomain::Unix, SocketType::Stream, 0).expect("socketpair");
2578        shutdown(fd_a.as_fd(), ShutdownMode::Write).expect("shutdown write");
2579    }
2580
2581    /// `set_so_reuseaddr` / `get_so_reuseaddr` : roundtrip.
2582    #[test]
2583    fn sockopt_reuseaddr_roundtrip() {
2584        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2585        set_so_reuseaddr(fd.as_fd(), true).expect("set_so_reuseaddr(true)");
2586        let val = get_so_reuseaddr(fd.as_fd()).expect("get_so_reuseaddr");
2587        assert!(val, "SO_REUSEADDR doit être activé");
2588
2589        set_so_reuseaddr(fd.as_fd(), false).expect("set_so_reuseaddr(false)");
2590        let val2 = get_so_reuseaddr(fd.as_fd()).expect("get_so_reuseaddr");
2591        assert!(!val2, "SO_REUSEADDR doit être désactivé");
2592    }
2593
2594    /// `set_so_keepalive` / `get_so_keepalive` : roundtrip.
2595    #[test]
2596    fn sockopt_keepalive_roundtrip() {
2597        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2598        set_so_keepalive(fd.as_fd(), true).expect("set_so_keepalive");
2599        let val = get_so_keepalive(fd.as_fd()).expect("get_so_keepalive");
2600        assert!(val, "SO_KEEPALIVE doit être activé");
2601    }
2602
2603    /// `get_so_error` retourne `None` sur un socket sain.
2604    #[test]
2605    fn get_so_error_none_on_healthy_socket() {
2606        let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket");
2607        let err = get_so_error(fd.as_fd()).expect("get_so_error");
2608        assert!(err.is_none(), "SO_ERROR doit être None sur un socket sain");
2609    }
2610
2611    /// Erreur socket **asynchrone réelle** : un `connect` non bloquant vers un
2612    /// port loopback **fermé** (réservé puis libéré, donc sans listener) dépose
2613    /// `ECONNREFUSED` dans `SO_ERROR` → `get_so_error` rend `Some(...)` (couvre le
2614    /// bras `val != 0`, jadis exempté faute d'outillage `poll`). Déterministe :
2615    /// boucle **bornée** sur la disponibilité de l'erreur (RST loopback quasi
2616    /// immédiat), sans `sleep`.
2617    #[test]
2618    #[cfg_attr(miri, ignore = "sockets non supportés par Miri")]
2619    fn get_so_error_some_on_refused_nonblocking_connect() {
2620        use air_sys_types::net::Ipv4SocketAddr;
2621
2622        // Réserve un port loopback puis le **ferme** → port garanti sans listener.
2623        let probe = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket probe");
2624        bind(
2625            probe.as_fd(),
2626            &SocketAddr::Ipv4(Ipv4SocketAddr {
2627                address: [127, 0, 0, 1],
2628                port: 0,
2629            }),
2630        )
2631        .expect("bind probe");
2632        let port = match getsockname(probe.as_fd()).expect("getsockname") {
2633            SocketAddr::Ipv4(v4) => v4.port,
2634            other => panic!("adresse IPv4 attendue : {other:?}"),
2635        };
2636        drop(probe); // libère/ferme le port
2637
2638        let sock = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
2639        // Bascule en non-bloquant (fcntl F_SETFL + O_NONBLOCK = 0x800).
2640        // SAFETY: `fcntl(F_SETFL, O_NONBLOCK)` sur un fd valide ; aucune mémoire
2641        // utilisateur lue/écrite.
2642        let ret = unsafe { raw_fcntl_setfl(sock.as_raw_fd(), 4, 0x800) };
2643        assert!(ret >= 0, "fcntl F_SETFL O_NONBLOCK");
2644        let target = SocketAddr::Ipv4(Ipv4SocketAddr {
2645            address: [127, 0, 0, 1],
2646            port,
2647        });
2648        // `connect` non bloquant : `EINPROGRESS` (ou `ECONNREFUSED` synchrone) —
2649        // dans les deux cas l'erreur se matérialise dans `SO_ERROR`.
2650        let _ = connect(sock.as_fd(), &target);
2651
2652        // ── CIBLE : `get_so_error` rend `Some(erreur)` (bras `val != 0`). ──
2653        let mut observed = None;
2654        for _ in 0..2_000_000 {
2655            if let Some(error) = get_so_error(sock.as_fd()).expect("get_so_error") {
2656                observed = Some(error);
2657                break;
2658            }
2659        }
2660        let error = observed.expect("SO_ERROR doit devenir disponible (connexion refusée)");
2661        assert_eq!(
2662            error, ECONNREFUSED,
2663            "port fermé → ECONNREFUSED via SO_ERROR"
2664        );
2665        // Idempotence : `SO_ERROR` est effacé après lecture → `None` ensuite.
2666        assert!(
2667            get_so_error(sock.as_fd())
2668                .expect("get_so_error 2")
2669                .is_none(),
2670            "SO_ERROR effacé après lecture"
2671        );
2672    }
2673
2674    /// `set_so_linger` / `get_so_linger` : roundtrip Disabled.
2675    #[test]
2676    fn sockopt_linger_disabled_roundtrip() {
2677        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2678        set_so_linger(fd.as_fd(), LingerOption::Disabled).expect("set_so_linger(Disabled)");
2679        let val = get_so_linger(fd.as_fd()).expect("get_so_linger");
2680        assert_eq!(val, LingerOption::Disabled);
2681    }
2682
2683    /// `set_so_linger` / `get_so_linger` : roundtrip Enabled.
2684    #[test]
2685    fn sockopt_linger_enabled_roundtrip() {
2686        use core::time::Duration;
2687        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2688        let linger = LingerOption::Enabled(Duration::from_secs(5));
2689        set_so_linger(fd.as_fd(), linger).expect("set_so_linger(Enabled(5s))");
2690        let val = get_so_linger(fd.as_fd()).expect("get_so_linger");
2691        assert_eq!(val, LingerOption::Enabled(Duration::from_secs(5)));
2692    }
2693
2694    /// `get_so_peercred` sur une `socketpair` Unix : le pair est le processus
2695    /// courant, donc les credentials retournés sont nos propres PID/UID/GID.
2696    /// (Ce n'était PAS une vraie « non-testabilité » : socketpair suffit.)
2697    #[test]
2698    fn sockopt_peercred_on_socketpair_is_self() {
2699        let (a, _b) = socketpair(SocketDomain::Unix, SocketType::Stream, 0).expect("socketpair");
2700        let creds = get_so_peercred(a.as_fd()).expect("get_so_peercred");
2701        // Le pair (l'autre extrémité) appartient au même processus.
2702        assert_eq!(creds.pid.as_raw(), crate::process::getpid().as_raw());
2703    }
2704
2705    /// `get_so_peercred` sur un socket Unix **non connecté** : le kernel renvoie
2706    /// `SO_PEERCRED` avec `pid == 0` (aucun pair établi). `NonZeroI32::new(0)`
2707    /// vaut alors `None` → la closure d'erreur `EINVAL` (`net.rs` l.1251) est
2708    /// prise. Couvre ce bras défensif par un chemin déterministe et sans
2709    /// privilège (création de socket Unix seule, aucune connexion).
2710    #[test]
2711    fn sockopt_peercred_einval_on_unconnected_unix_socket() {
2712        let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket Unix");
2713        assert_eq!(
2714            get_so_peercred(fd.as_fd()).unwrap_err(),
2715            Errno::EINVAL,
2716            "un socket Unix non connecté rapporte pid=0 → EINVAL"
2717        );
2718    }
2719
2720    /// `set_ip_ttl` sur un socket IPv4 réel (UDP) : aucune privilège requis.
2721    #[test]
2722    fn sockopt_ip_ttl_set_on_ipv4_socket() {
2723        let fd = socket(SocketDomain::Ipv4, SocketType::Datagram, 0).expect("socket IPv4 UDP");
2724        set_ip_ttl(fd.as_fd(), 64).expect("set_ip_ttl(64)");
2725    }
2726
2727    /// `set_ipv6_v6only` sur un socket IPv6 réel : aucune privilège requis.
2728    /// Skippe proprement si IPv6 est désactivé sur l'hôte (`EAFNOSUPPORT`).
2729    #[test]
2730    fn sockopt_ipv6_v6only_set_on_ipv6_socket() {
2731        // IPv6 indisponible sur l'hôte (`Err`) : pas un bug du wrapper → skip.
2732        if let Ok(fd) = socket(SocketDomain::Ipv6, SocketType::Stream, 0) {
2733            set_ipv6_v6only(fd.as_fd(), true).expect("set_ipv6_v6only(true)");
2734        }
2735    }
2736
2737    // ── Délais SO_RCVTIMEO / SO_SNDTIMEO (options `timeval` — couche-0-v1.13) ──
2738
2739    /// `set_receive_timeout(Some)` puis `receive_timeout()` : roundtrip exact
2740    /// (couvre le bras `Some` de `decode_timeout` et le `unwrap_or` côté `Some`).
2741    #[test]
2742    fn sockopt_receive_timeout_some_roundtrip() {
2743        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2744        let tv = Timeval {
2745            tv_sec: 0,
2746            tv_usec: 200_000,
2747        };
2748        set_receive_timeout(fd.as_fd(), Some(tv)).expect("set_receive_timeout(Some)");
2749        assert_eq!(
2750            receive_timeout(fd.as_fd()).expect("receive_timeout"),
2751            Some(tv)
2752        );
2753    }
2754
2755    /// `set_receive_timeout(None)` désarme le délai → `receive_timeout()` rend
2756    /// `None` (couvre le bras `None` de `decode_timeout` et le `unwrap_or` défaut).
2757    #[test]
2758    fn sockopt_receive_timeout_none_clears() {
2759        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2760        set_receive_timeout(
2761            fd.as_fd(),
2762            Some(Timeval {
2763                tv_sec: 1,
2764                tv_usec: 0,
2765            }),
2766        )
2767        .expect("armer d'abord");
2768        set_receive_timeout(fd.as_fd(), None).expect("set_receive_timeout(None)");
2769        assert_eq!(receive_timeout(fd.as_fd()).expect("receive_timeout"), None);
2770    }
2771
2772    /// `set_send_timeout` / `send_timeout` : roundtrip (exerce l'`optname`
2773    /// `SO_SNDTIMEO` du helper partagé).
2774    #[test]
2775    fn sockopt_send_timeout_roundtrip() {
2776        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2777        let tv = Timeval {
2778            tv_sec: 2,
2779            tv_usec: 500_000,
2780        };
2781        set_send_timeout(fd.as_fd(), Some(tv)).expect("set_send_timeout");
2782        assert_eq!(send_timeout(fd.as_fd()).expect("send_timeout"), Some(tv));
2783    }
2784
2785    /// Bras d'erreur `if ret < 0` des délais via fd invalide → `EBADF`.
2786    #[test]
2787    fn sockopt_timeout_ebadf_on_invalid_fd() {
2788        let tv = Timeval {
2789            tv_sec: 0,
2790            tv_usec: 1,
2791        };
2792        assert_eq!(
2793            set_receive_timeout(invalid_fd(), Some(tv)).unwrap_err(),
2794            Errno::EBADF
2795        );
2796        assert_eq!(receive_timeout(invalid_fd()).unwrap_err(), Errno::EBADF);
2797        assert_eq!(
2798            set_send_timeout(invalid_fd(), Some(tv)).unwrap_err(),
2799            Errno::EBADF
2800        );
2801        assert_eq!(send_timeout(invalid_fd()).unwrap_err(), Errno::EBADF);
2802    }
2803
2804    // ── Appartenance multicast v4/v6 (options `ip_mreq`/`ipv6_mreq`) ──
2805    //
2806    // Tests **déterministes sur loopback** (aucune dépendance réseau externe) :
2807    // rejoindre `224.0.0.1` via l'interface `127.0.0.1`, et `ff02::1` via l'index
2808    // de `lo` (toujours `1` sur Linux), réussissent sans privilège — vérifié.
2809
2810    /// `join_multicast_v4` puis `leave_multicast_v4` sur loopback : succès
2811    /// (couvre le bras `Ok` de `set_ip_membership_v4` pour ADD **et** DROP).
2812    #[test]
2813    fn sockopt_multicast_v4_join_leave_on_loopback() {
2814        let fd = socket(SocketDomain::Ipv4, SocketType::Datagram, 0).expect("socket IPv4 UDP");
2815        let membership = IpMembershipV4 {
2816            multiaddr: [224, 0, 0, 1],
2817            interface: [127, 0, 0, 1],
2818        };
2819        join_multicast_v4(fd.as_fd(), &membership).expect("join_multicast_v4(224.0.0.1@lo)");
2820        leave_multicast_v4(fd.as_fd(), &membership).expect("leave_multicast_v4");
2821    }
2822
2823    /// `join_multicast_v6` puis `leave_multicast_v6` sur loopback (`ff02::1`,
2824    /// ifindex 1) : succès (couvre `set_ip_membership_v6` ADD **et** DROP).
2825    #[test]
2826    fn sockopt_multicast_v6_join_leave_on_loopback() {
2827        let fd = socket(SocketDomain::Ipv6, SocketType::Datagram, 0).expect("socket IPv6 UDP");
2828        let membership = IpMembershipV6 {
2829            multiaddr: [0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
2830            interface: 1, // `lo` — index invariant sur Linux
2831        };
2832        join_multicast_v6(fd.as_fd(), &membership).expect("join_multicast_v6(ff02::1@lo)");
2833        leave_multicast_v6(fd.as_fd(), &membership).expect("leave_multicast_v6");
2834    }
2835
2836    /// Bras d'erreur `if ret < 0` du multicast via fd invalide → `EBADF`.
2837    #[test]
2838    fn sockopt_multicast_ebadf_on_invalid_fd() {
2839        let v4 = IpMembershipV4 {
2840            multiaddr: [224, 0, 0, 1],
2841            interface: [0, 0, 0, 0],
2842        };
2843        let v6 = IpMembershipV6 {
2844            multiaddr: [0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
2845            interface: 0,
2846        };
2847        assert_eq!(
2848            join_multicast_v4(invalid_fd(), &v4).unwrap_err(),
2849            Errno::EBADF
2850        );
2851        assert_eq!(
2852            leave_multicast_v4(invalid_fd(), &v4).unwrap_err(),
2853            Errno::EBADF
2854        );
2855        assert_eq!(
2856            join_multicast_v6(invalid_fd(), &v6).unwrap_err(),
2857            Errno::EBADF
2858        );
2859        assert_eq!(
2860            leave_multicast_v6(invalid_fd(), &v6).unwrap_err(),
2861            Errno::EBADF
2862        );
2863    }
2864
2865    proptest::proptest! {
2866        /// Property (décodage `getsockopt` — structure **rendue par le kernel**) :
2867        /// `decode_timeout` rend `None` **si et seulement si** le `timeval` est nul,
2868        /// et **préserve** exactement la valeur sinon. Couvre le décodage de la
2869        /// structure lue depuis le kernel (exigence fuzz/property de la mission).
2870        #[test]
2871        fn decode_timeout_is_none_iff_zero(sec in i64::MIN..=i64::MAX, usec in i64::MIN..=i64::MAX) {
2872            let tv = Timeval { tv_sec: sec, tv_usec: usec };
2873            let decoded = decode_timeout(tv);
2874            if sec == 0 && usec == 0 {
2875                proptest::prop_assert_eq!(decoded, None);
2876            } else {
2877                proptest::prop_assert_eq!(decoded, Some(tv));
2878            }
2879        }
2880    }
2881
2882    /// `set_so_reuseport` / `get_so_reuseport` : roundtrip.
2883    #[test]
2884    fn sockopt_reuseport_roundtrip() {
2885        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2886        set_so_reuseport(fd.as_fd(), true).expect("set_so_reuseport(true)");
2887        assert!(get_so_reuseport(fd.as_fd()).expect("get_so_reuseport"));
2888    }
2889
2890    /// `set_tcp_nodelay` / `get_tcp_nodelay` : roundtrip sur un socket TCP.
2891    #[test]
2892    fn sockopt_tcp_nodelay_roundtrip() {
2893        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket TCP");
2894        set_tcp_nodelay(fd.as_fd(), true).expect("set_tcp_nodelay(true)");
2895        assert!(get_tcp_nodelay(fd.as_fd()).expect("get_tcp_nodelay"));
2896    }
2897
2898    /// Buffers `SO_RCVBUF` / `SO_SNDBUF` : set puis get (le kernel double la
2899    /// valeur demandée, on vérifie seulement qu'elle est non nulle).
2900    #[test]
2901    fn sockopt_rcvbuf_sndbuf_roundtrip() {
2902        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2903        set_so_rcvbuf(fd.as_fd(), 8192).expect("set_so_rcvbuf");
2904        assert!(get_so_rcvbuf(fd.as_fd()).expect("get_so_rcvbuf") > 0);
2905        set_so_sndbuf(fd.as_fd(), 8192).expect("set_so_sndbuf");
2906        assert!(get_so_sndbuf(fd.as_fd()).expect("get_so_sndbuf") > 0);
2907    }
2908
2909    /// `set_tcp_keepidle`/`keepintvl`/`keepcnt` sur un socket TCP réel.
2910    #[test]
2911    fn sockopt_tcp_keepalive_params() {
2912        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket TCP");
2913        set_tcp_keepidle(fd.as_fd(), 60).expect("keepidle");
2914        set_tcp_keepintvl(fd.as_fd(), 10).expect("keepintvl");
2915        set_tcp_keepcnt(fd.as_fd(), 5).expect("keepcnt");
2916    }
2917
2918    // ── bind/getsockname avec IPv4 (branche Ipv4 de socket_addr_to_raw) ──
2919
2920    #[test]
2921    fn bind_ipv4_loopback_and_getsockname() {
2922        use air_sys_types::net::Ipv4SocketAddr;
2923        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket IPv4");
2924        set_so_reuseaddr(fd.as_fd(), true).expect("reuseaddr");
2925        let address = SocketAddr::Ipv4(Ipv4SocketAddr {
2926            address: [127, 0, 0, 1],
2927            port: 0, // port 0 = attribution automatique
2928        });
2929        bind(fd.as_fd(), &address).expect("bind IPv4 loopback");
2930        let name = getsockname(fd.as_fd()).expect("getsockname IPv4");
2931        if let SocketAddr::Ipv4(v4) = name {
2932            assert_eq!(v4.address, [127, 0, 0, 1]);
2933            assert!(v4.port > 0, "port assigné doit être > 0");
2934        } else {
2935            panic!("getsockname doit retourner IPv4, got {name:?}");
2936        }
2937    }
2938
2939    // ── bind/getsockname avec IPv6 (branche Ipv6 de socket_addr_to_raw) ──
2940
2941    #[test]
2942    fn bind_ipv6_loopback_and_getsockname() {
2943        use air_sys_types::net::Ipv6SocketAddr;
2944        let fd = socket(SocketDomain::Ipv6, SocketType::Stream, 0).expect("socket IPv6");
2945        set_so_reuseaddr(fd.as_fd(), true).expect("reuseaddr");
2946        let address = SocketAddr::Ipv6(Ipv6SocketAddr {
2947            address: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], // ::1
2948            port: 0,
2949            flowinfo: 0,
2950            scope_id: 0,
2951        });
2952        bind(fd.as_fd(), &address).expect("bind IPv6 loopback");
2953        let name = getsockname(fd.as_fd()).expect("getsockname IPv6");
2954        if let SocketAddr::Ipv6(v6) = name {
2955            assert_eq!(v6.address, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
2956        } else {
2957            panic!("getsockname doit retourner IPv6, got {name:?}");
2958        }
2959    }
2960
2961    // ── Unix socket abstrait (branche Abstract dans parse_sockaddr_from_raw) ─
2962
2963    #[test]
2964    fn bind_unix_abstract_and_getsockname() {
2965        use air_sys_types::net::UnixSocketAddr;
2966        let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket Unix");
2967        // UnixSocketAddr::Abstract prend le nom sans le '\0' initial.
2968        let address = SocketAddr::Unix(UnixSocketAddr::Abstract(b"air_test_abstract_42".to_vec()));
2969        bind(fd.as_fd(), &address).expect("bind Unix abstrait");
2970        let name = getsockname(fd.as_fd()).expect("getsockname abstrait");
2971        if let SocketAddr::Unix(UnixSocketAddr::Abstract(_)) = name {
2972            // OK : l'adresse abstraite a été retournée.
2973        } else {
2974            panic!("getsockname doit retourner Unix abstrait, got {name:?}");
2975        }
2976    }
2977
2978    // ── socket sans nom → Unnamed dans getsockname ────────────────────────
2979
2980    #[test]
2981    fn getsockname_unbound_unix_returns_unnamed_or_empty() {
2982        let fd = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket Unix");
2983        // Un socket Unix non bindé retourne une adresse sans nom.
2984        match getsockname(fd.as_fd()) {
2985            Ok(SocketAddr::Unix(_)) => {}
2986            Ok(other) => panic!("attendu Unix, got {other:?}"),
2987            Err(_) => {} // certains kernels retournent une erreur pour un socket non bindé
2988        }
2989    }
2990
2991    // ── bind EADDRINUSE ────────────────────────────────────────────────────
2992
2993    #[test]
2994    fn bind_ipv4_eaddrinuse() {
2995        use air_sys_types::net::Ipv4SocketAddr;
2996        let fd1 = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket 1");
2997        let fd2 = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket 2");
2998        set_so_reuseaddr(fd1.as_fd(), true).expect("reuseaddr 1");
2999        let address = SocketAddr::Ipv4(Ipv4SocketAddr {
3000            address: [127, 0, 0, 1],
3001            port: 0,
3002        });
3003        bind(fd1.as_fd(), &address).expect("bind 1");
3004        // Obtient le port assigné.
3005        let name = getsockname(fd1.as_fd()).expect("getsockname 1");
3006        let port = if let SocketAddr::Ipv4(v4) = name {
3007            v4.port
3008        } else {
3009            panic!()
3010        };
3011        // Tente de binder le même port depuis fd2 : EADDRINUSE attendu.
3012        let addr2 = SocketAddr::Ipv4(Ipv4SocketAddr {
3013            address: [127, 0, 0, 1],
3014            port,
3015        });
3016        let err = bind(fd2.as_fd(), &addr2).unwrap_err();
3017        assert_eq!(err, EADDRINUSE);
3018    }
3019
3020    // ── connect ECONNREFUSED ───────────────────────────────────────────────
3021
3022    #[test]
3023    fn connect_ipv4_econnrefused() {
3024        use air_sys_types::net::Ipv4SocketAddr;
3025        // On se connecte sur un port probablement fermé (1).
3026        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket TCP");
3027        let address = SocketAddr::Ipv4(Ipv4SocketAddr {
3028            address: [127, 0, 0, 1],
3029            port: 1, // port 1 est rarement en écoute
3030        });
3031        let err = connect(fd.as_fd(), &address).unwrap_err();
3032        assert!(
3033            err == ECONNREFUSED || err == Errno::EACCES,
3034            "attendu ECONNREFUSED ou EACCES, got {err:?}"
3035        );
3036    }
3037
3038    // ── send / recv error paths ────────────────────────────────────────────
3039
3040    #[test]
3041    fn send_ebadf() {
3042        let invalid = invalid_fd();
3043        let err = send(invalid, b"data", MessageFlags::empty()).unwrap_err();
3044        assert_eq!(err, Errno::EBADF);
3045    }
3046
3047    #[test]
3048    fn recv_ebadf() {
3049        let invalid = invalid_fd();
3050        let mut buffer = [0u8; 4];
3051        let err = recv(invalid, &mut buffer, MessageFlags::empty()).unwrap_err();
3052        assert_eq!(err, Errno::EBADF);
3053    }
3054
3055    // ── get_so_bindtodevice / set_so_bindtodevice ─────────────────────────
3056
3057    #[test]
3058    fn get_so_bindtodevice_unbound_returns_empty() {
3059        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
3060        let device = get_so_bindtodevice(fd.as_fd()).expect("get_so_bindtodevice");
3061        assert!(
3062            device.to_bytes().is_empty(),
3063            "socket non bindé à un device doit retourner vide"
3064        );
3065    }
3066
3067    #[test]
3068    fn set_so_bindtodevice_lo_or_eperm() {
3069        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
3070        // SO_BINDTODEVICE requiert CAP_NET_RAW ; on accepte EPERM.
3071        match set_so_bindtodevice(fd.as_fd(), c"lo") {
3072            Ok(()) => {
3073                // Succès : on peut vérifier que get renvoie "lo".
3074                let dev = get_so_bindtodevice(fd.as_fd()).expect("get après set");
3075                assert_eq!(dev.to_bytes(), b"lo");
3076            }
3077            Err(Errno::EPERM) | Err(Errno::ENODEV) => {
3078                // Pas root ou "lo" non existant : acceptable.
3079            }
3080            Err(e) => panic!("set_so_bindtodevice erreur inattendue : {e:?}"),
3081        }
3082    }
3083
3084    // ── socket : chemin d'erreur ────────────────────────────────────────────
3085
3086    #[test]
3087    fn socket_esocktnosupport_on_unsupported_type() {
3088        // AF_INET ne supporte pas SOCK_SEQPACKET : le kernel rejette la
3089        // combinaison avec ESOCKTNOSUPPORT. Couvre la branche d'erreur de
3090        // `socket` via une combinaison déterministe (pas de privilège requis).
3091        let err = socket(SocketDomain::Ipv4, SocketType::SeqPacket, 0).unwrap_err();
3092        assert_eq!(err, ESOCKTNOSUPPORT);
3093    }
3094
3095    // ── accept4 EAGAIN sur socket non-bloquant ──────────────────────────
3096
3097    #[test]
3098    fn accept4_eagain_on_nonblocking_without_connections() {
3099        use air_sys_types::net::UnixSocketAddr;
3100        let listener = socket(SocketDomain::Unix, SocketType::Stream, 0).expect("socket");
3101        let path = c"/tmp/air_net_accept4_eagain_42.sock";
3102        let _ = std::fs::remove_file("/tmp/air_net_accept4_eagain_42.sock");
3103        bind(
3104            listener.as_fd(),
3105            &SocketAddr::Unix(UnixSocketAddr::Path(path.to_owned())),
3106        )
3107        .expect("bind");
3108        listen(listener.as_fd(), 1).expect("listen");
3109        // Bascule le socket en mode non-bloquant via fcntl F_SETFL + O_NONBLOCK.
3110        // O_NONBLOCK = 0x800 sur Linux x86_64/aarch64.
3111        let o_nonblock: u64 = 0x800;
3112        // SAFETY: `fcntl(F_SETFL)` ne prend que des scalaires (fd, cmd, flags) et ne
3113        // déréférence aucun pointeur utilisateur — appel sûr sur un FD valide.
3114        let ret = unsafe { raw_fcntl_setfl(listener.as_raw_fd(), 4, o_nonblock) };
3115        if ret < 0 {
3116            // fcntl non disponible : skip
3117            let _ = std::fs::remove_file("/tmp/air_net_accept4_eagain_42.sock");
3118            return;
3119        }
3120        let err = accept4(listener.as_fd(), AcceptFlags::CLOEXEC).unwrap_err();
3121        // Sur Linux, EWOULDBLOCK == EAGAIN == 11.
3122        assert_eq!(
3123            err,
3124            Errno::EAGAIN,
3125            "attendu EAGAIN sur socket non-bloquant vide"
3126        );
3127        let _ = std::fs::remove_file("/tmp/air_net_accept4_eagain_42.sock");
3128    }
3129
3130    #[cfg(target_arch = "x86_64")]
3131    unsafe fn raw_fcntl_setfl(fd: i32, cmd: i64, flags: u64) -> i64 {
3132        let ret: i64;
3133        // SAFETY: fcntl (x86_64 = 72). Ne prend que des scalaires (fd, cmd, flags)
3134        // et ne touche à aucune mémoire utilisateur.
3135        unsafe {
3136            core::arch::asm!(
3137                "syscall",
3138                in("rax") 72_i64, // fcntl
3139                in("rdi") i64::from(fd),
3140                in("rsi") cmd,
3141                in("rdx") flags,
3142                lateout("rax") ret,
3143                lateout("rcx") _,
3144                lateout("r11") _,
3145                options(nostack, preserves_flags),
3146            );
3147        }
3148        ret
3149    }
3150
3151    #[cfg(target_arch = "aarch64")]
3152    unsafe fn raw_fcntl_setfl(fd: i32, cmd: i64, flags: u64) -> i64 {
3153        let ret: i64;
3154        // SAFETY: fcntl (aarch64 = 25). Ne prend que des scalaires (fd, cmd, flags)
3155        // et ne touche à aucune mémoire utilisateur.
3156        unsafe {
3157            core::arch::asm!(
3158                "svc 0",
3159                in("x8") 25_i64, // fcntl
3160                inout("x0") i64::from(fd) => ret,
3161                in("x1") cmd,
3162                in("x2") flags,
3163                options(nostack, preserves_flags),
3164            );
3165        }
3166        ret
3167    }
3168
3169    // ── shutdown error path ────────────────────────────────────────────────
3170
3171    #[test]
3172    fn shutdown_enotconn_on_unconnected() {
3173        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
3174        let err = shutdown(fd.as_fd(), ShutdownMode::Both).unwrap_err();
3175        assert_eq!(err, ENOTCONN);
3176    }
3177
3178    // ───────── debug_assert errno helper ─────────
3179
3180    // Couvrent les deux opérandes du `debug_assert!(ret < 0 && ret > -4096)`.
3181    #[test]
3182    #[should_panic(expected = "ret < 0 && ret > -4096")]
3183    fn errno_from_negative_syscall_ret_panics_on_non_negative() {
3184        let _ = errno_from_negative_syscall_ret(0);
3185    }
3186
3187    #[test]
3188    #[should_panic(expected = "ret < 0 && ret > -4096")]
3189    fn errno_from_negative_syscall_ret_panics_below_errno_range() {
3190        let _ = errno_from_negative_syscall_ret(-5000);
3191    }
3192
3193    // ───────── branches d'erreur `if ret < 0` via fd invalide → EBADF ─────────
3194
3195    #[test]
3196    fn listen_ebadf_on_invalid_fd() {
3197        assert_eq!(listen(invalid_fd(), 1).unwrap_err(), Errno::EBADF);
3198    }
3199
3200    #[test]
3201    fn getsockname_ebadf_on_invalid_fd() {
3202        assert_eq!(getsockname(invalid_fd()).unwrap_err(), Errno::EBADF);
3203    }
3204
3205    #[test]
3206    fn getpeername_ebadf_on_invalid_fd() {
3207        assert_eq!(getpeername(invalid_fd()).unwrap_err(), Errno::EBADF);
3208    }
3209
3210    #[test]
3211    fn get_so_peercred_ebadf_on_invalid_fd() {
3212        assert_eq!(get_so_peercred(invalid_fd()).unwrap_err(), Errno::EBADF);
3213    }
3214
3215    #[test]
3216    fn get_so_linger_ebadf_on_invalid_fd() {
3217        assert_eq!(get_so_linger(invalid_fd()).unwrap_err(), Errno::EBADF);
3218    }
3219
3220    #[test]
3221    fn set_so_linger_ebadf_on_invalid_fd() {
3222        use core::time::Duration;
3223        let err =
3224            set_so_linger(invalid_fd(), LingerOption::Enabled(Duration::from_secs(1))).unwrap_err();
3225        assert_eq!(err, Errno::EBADF);
3226    }
3227
3228    #[test]
3229    fn set_so_bindtodevice_ebadf_on_invalid_fd() {
3230        assert_eq!(
3231            set_so_bindtodevice(invalid_fd(), c"lo").unwrap_err(),
3232            Errno::EBADF
3233        );
3234    }
3235
3236    #[test]
3237    fn get_so_bindtodevice_ebadf_on_invalid_fd() {
3238        assert_eq!(get_so_bindtodevice(invalid_fd()).unwrap_err(), Errno::EBADF);
3239    }
3240
3241    #[test]
3242    fn sendmsg_ebadf_on_invalid_fd() {
3243        let data = *b"x";
3244        let iov = [IoSlice::new(&data)];
3245        let req = SendMessageRequest {
3246            iov: &iov,
3247            address: None,
3248            fds: &[],
3249            flags: MessageFlags::empty(),
3250        };
3251        assert_eq!(sendmsg(invalid_fd(), &req).unwrap_err(), Errno::EBADF);
3252    }
3253
3254    #[test]
3255    fn recvmsg_ebadf_on_invalid_fd() {
3256        let mut buffer = [0u8; 8];
3257        let mut iov = [IoSliceMut::new(&mut buffer)];
3258        let mut req = ReceiveMessageRequest {
3259            iov: &mut iov,
3260            flags: MessageFlags::empty(),
3261        };
3262        assert_eq!(recvmsg(invalid_fd(), &mut req).unwrap_err(), Errno::EBADF);
3263    }
3264
3265    #[test]
3266    fn socketpair_eopnotsupp_on_inet_domain() {
3267        // `socketpair(2)` n'est supporté que pour `AF_UNIX` → EOPNOTSUPP sur
3268        // un domaine inet (couvre la branche `if ret < 0` de `socketpair`).
3269        let eopnotsupp = Errno::try_from_raw(95).unwrap();
3270        let err = socketpair(SocketDomain::Ipv4, SocketType::Stream, 0).unwrap_err();
3271        assert_eq!(err, eopnotsupp);
3272    }
3273
3274    // ───────── clamps `if val > i32::MAX` (sur socket valide) ─────────
3275
3276    #[test]
3277    fn set_so_rcvbuf_clamps_oversized() {
3278        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
3279        set_so_rcvbuf(fd.as_fd(), 0x8000_0000).expect("set_so_rcvbuf clamp");
3280    }
3281
3282    #[test]
3283    fn set_so_sndbuf_clamps_oversized() {
3284        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
3285        set_so_sndbuf(fd.as_fd(), 0x8000_0000).expect("set_so_sndbuf clamp");
3286    }
3287
3288    #[test]
3289    fn set_so_linger_clamps_oversized_secs() {
3290        use core::time::Duration;
3291        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
3292        // Durée en secondes > i32::MAX → clampée avant le syscall (le résultat
3293        // du syscall importe peu : la branche de clamp est exercée).
3294        let _ = set_so_linger(
3295            fd.as_fd(),
3296            LingerOption::Enabled(Duration::from_secs(u64::from(u32::MAX))),
3297        );
3298    }
3299
3300    #[test]
3301    fn set_tcp_keepalive_params_clamp_oversized() {
3302        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket TCP");
3303        // Valeurs > i32::MAX → clampées avant le syscall.
3304        let _ = set_tcp_keepidle(fd.as_fd(), 0x8000_0000);
3305        let _ = set_tcp_keepintvl(fd.as_fd(), 0x8000_0000);
3306        let _ = set_tcp_keepcnt(fd.as_fd(), 0x8000_0000);
3307    }
3308
3309    // ───────── parseur `raw_to_socket_addr` : buffers tronqués → None ─────────
3310
3311    #[test]
3312    fn raw_to_socket_addr_rejects_buffer_shorter_than_len() {
3313        // `buffer.len() < length` → None (couvre la garde l.154).
3314        assert!(raw_to_socket_addr(&[2, 0], 16).is_none());
3315    }
3316
3317    #[test]
3318    fn raw_to_socket_addr_rejects_truncated_ipv4() {
3319        // famille AF_INET (2) mais `length < 16` → None (l.184).
3320        let mut buffer = [0u8; 16];
3321        buffer[0..2].copy_from_slice(&2u16.to_ne_bytes());
3322        assert!(raw_to_socket_addr(&buffer, 10).is_none());
3323    }
3324
3325    #[test]
3326    fn raw_to_socket_addr_rejects_truncated_ipv6() {
3327        // famille AF_INET6 (10) mais `length < 28` → None (l.193).
3328        let mut buffer = [0u8; 28];
3329        buffer[0..2].copy_from_slice(&10u16.to_ne_bytes());
3330        assert!(raw_to_socket_addr(&buffer, 20).is_none());
3331    }
3332
3333    // ───────── codec sockaddr PUBLIC (ADR-070) : round-trip + gardes ─────────
3334
3335    #[test]
3336    fn socket_addr_bytes_round_trip_ipv4() {
3337        let address = SocketAddr::Ipv4(Ipv4SocketAddr {
3338            address: [127, 0, 0, 1],
3339            port: 443,
3340        });
3341        let mut buffer = [0u8; 128];
3342        let length = socket_addr_to_bytes(&address, &mut buffer).expect("encode IPv4");
3343        assert_eq!(length, 16);
3344        assert_eq!(socket_addr_from_bytes(&buffer, length), Some(address));
3345    }
3346
3347    #[test]
3348    fn socket_addr_bytes_round_trip_ipv6() {
3349        let address = SocketAddr::Ipv6(Ipv6SocketAddr {
3350            address: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
3351            port: 443,
3352            flowinfo: 0,
3353            scope_id: 0,
3354        });
3355        let mut buffer = [0u8; 128];
3356        let length = socket_addr_to_bytes(&address, &mut buffer).expect("encode IPv6");
3357        assert_eq!(length, 28);
3358        assert_eq!(socket_addr_from_bytes(&buffer, length), Some(address));
3359    }
3360
3361    #[test]
3362    fn socket_addr_to_bytes_reports_none_when_out_too_small() {
3363        let address = SocketAddr::Ipv4(Ipv4SocketAddr {
3364            address: [10, 0, 0, 1],
3365            port: 22,
3366        });
3367        // `out` plus court que les 16 octets d'un sockaddr_in ⇒ None (garde `out`).
3368        let mut tiny = [0u8; 8];
3369        assert_eq!(socket_addr_to_bytes(&address, &mut tiny), None);
3370    }
3371
3372    #[test]
3373    fn socket_option_int_public_wrappers_round_trip() {
3374        // Exposition publique (ADR-071) : set SO_REUSEADDR puis get SO_TYPE.
3375        let sock = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
3376        // SOL_SOCKET = 1, SO_REUSEADDR = 2, SO_TYPE = 3.
3377        set_socket_option_int(sock.as_fd(), 1, 2, 1).expect("set SO_REUSEADDR");
3378        let kind = get_socket_option_int(sock.as_fd(), 1, 3).expect("get SO_TYPE");
3379        assert_eq!(kind, 1); // SOCK_STREAM
3380    }
3381
3382    #[test]
3383    fn socket_option_int_reports_error_on_bad_option() {
3384        // optname invalide => ENOPROTOOPT propage (chemin d'erreur).
3385        let sock = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
3386        assert!(get_socket_option_int(sock.as_fd(), 1, 99_999).is_err());
3387    }
3388
3389    #[test]
3390    fn socket_addr_from_bytes_rejects_unknown_family() {
3391        // Délègue à `raw_to_socket_addr` : une famille inconnue rend `None`.
3392        assert!(socket_addr_from_bytes(&[99, 0, 0, 0, 0, 0, 0, 0], 8).is_none());
3393    }
3394
3395    // ───────── SCM_RIGHTS : passage de FD sur socketpair Unix ─────────
3396
3397    #[test]
3398    fn scm_rights_fd_passing_roundtrip() {
3399        // Couvre `build_scm_rights_cmsg` (ctrl non vide), le bras `fds` de
3400        // `sendmsg`, et `parse_scm_rights` (boucle cmsg + extraction des FDs).
3401        let (fd_a, fd_b) =
3402            socketpair(SocketDomain::Unix, SocketType::Stream, 0).expect("socketpair");
3403        let passed =
3404            crate::mem::memfd_create(c"air-scm-rights", air_sys_types::mem::MemfdFlags::empty())
3405                .expect("memfd");
3406        let data = *b"fd!";
3407        let iov = [IoSlice::new(&data)];
3408        let fds = [passed.as_fd()];
3409        let req = SendMessageRequest {
3410            iov: &iov,
3411            address: None,
3412            fds: &fds,
3413            flags: MessageFlags::empty(),
3414        };
3415        sendmsg(fd_a.as_fd(), &req).expect("sendmsg with fd");
3416
3417        let mut buffer = [0u8; 8];
3418        let mut iov_r = [IoSliceMut::new(&mut buffer)];
3419        let mut req_r = ReceiveMessageRequest {
3420            iov: &mut iov_r,
3421            flags: MessageFlags::empty(),
3422        };
3423        let res = recvmsg(fd_b.as_fd(), &mut req_r).expect("recvmsg with fd");
3424        assert_eq!(&buffer[..res.bytes_received], b"fd!");
3425        assert_eq!(res.fds.len(), 1, "un FD doit être reçu via SCM_RIGHTS");
3426    }
3427
3428    #[test]
3429    fn socket_eprotonosupport_on_mismatched_protocol() {
3430        // Type STREAM avec le protocole UDP (17) → EPROTONOSUPPORT (couvre la
3431        // branche `if ret < 0` de `socket`).
3432        let eprotonosupport = Errno::try_from_raw(93).unwrap();
3433        let err = socket(SocketDomain::Ipv4, SocketType::Stream, 17).unwrap_err();
3434        assert_eq!(err, eprotonosupport);
3435    }
3436
3437    #[test]
3438    fn listen_clamps_oversized_backlog() {
3439        // `backlog > i32::MAX` → clampé avant le syscall (couvre la branche de
3440        // clamp l.489). Le résultat du syscall importe peu.
3441        let fd = socket(SocketDomain::Ipv4, SocketType::Stream, 0).expect("socket");
3442        let _ = listen(fd.as_fd(), 0x8000_0000);
3443    }
3444
3445    // ───────── parse_scm_rights : buffers cmsg dégénérés ─────────
3446
3447    #[test]
3448    fn parse_scm_rights_ignores_non_scm_rights_cmsg() {
3449        // Un cmsg de niveau/type ≠ SOL_SOCKET/SCM_RIGHTS ne produit aucun FD
3450        // (couvre les côtés faux de `level == 1 && ty == 1 && length >= 16`).
3451        let mut buffer = vec![0u8; 16];
3452        buffer[0..8].copy_from_slice(&16u64.to_ne_bytes()); // cmsg_len = 16
3453        buffer[8..12].copy_from_slice(&0i32.to_ne_bytes()); // level ≠ SOL_SOCKET
3454        buffer[12..16].copy_from_slice(&0i32.to_ne_bytes()); // type ≠ SCM_RIGHTS
3455        let fds = parse_scm_rights(&buffer, 16);
3456        assert!(fds.is_empty());
3457    }
3458
3459    #[test]
3460    fn parse_scm_rights_ignores_too_short_payload() {
3461        // level/type = SCM_RIGHTS mais `length < 16` → pas de FD (couvre le côté
3462        // faux de `length >= 16`).
3463        let mut buffer = vec![0u8; 16];
3464        buffer[0..8].copy_from_slice(&8u64.to_ne_bytes()); // cmsg interne length = 8 (< 16)
3465        buffer[8..12].copy_from_slice(&1i32.to_ne_bytes()); // SOL_SOCKET
3466        buffer[12..16].copy_from_slice(&1i32.to_ne_bytes()); // SCM_RIGHTS
3467        let fds = parse_scm_rights(&buffer, 16);
3468        assert!(fds.is_empty());
3469    }
3470
3471    #[test]
3472    fn parse_scm_rights_stops_when_cmsg_len_exceeds_buffer() {
3473        // `cmsg_len` annoncé plus grand que le buffer réel → la 2ᵉ borne du
3474        // `while` (`<= cmsg_buf.len()`) arrête la boucle (couvre l.251).
3475        let buffer = vec![0u8; 8]; // trop court pour un header de 16 octets
3476        let fds = parse_scm_rights(&buffer, 1000);
3477        assert!(fds.is_empty());
3478    }
3479
3480    #[test]
3481    fn parse_scm_rights_ignores_wrong_cmsg_type() {
3482        // level = SOL_SOCKET mais type ≠ SCM_RIGHTS → côté faux de `ty == 1`.
3483        let mut buffer = vec![0u8; 16];
3484        buffer[0..8].copy_from_slice(&16u64.to_ne_bytes());
3485        buffer[8..12].copy_from_slice(&1i32.to_ne_bytes()); // SOL_SOCKET
3486        buffer[12..16].copy_from_slice(&2i32.to_ne_bytes()); // type ≠ SCM_RIGHTS
3487        assert!(parse_scm_rights(&buffer, 16).is_empty());
3488    }
3489
3490    #[test]
3491    fn parse_scm_rights_breaks_on_zero_len_entry() {
3492        // Un cmsg de longueur interne 0 → `aligned == 0` → `break` (l.292),
3493        // évite une boucle infinie.
3494        let mut buffer = vec![0u8; 16];
3495        buffer[0..8].copy_from_slice(&0u64.to_ne_bytes()); // length interne = 0
3496        buffer[8..12].copy_from_slice(&1i32.to_ne_bytes());
3497        buffer[12..16].copy_from_slice(&1i32.to_ne_bytes());
3498        assert!(parse_scm_rights(&buffer, 16).is_empty());
3499    }
3500
3501    #[test]
3502    fn get_so_rcvbuf_ebadf_on_invalid_fd() {
3503        // Couvre la branche d'erreur du helper partagé `getsockopt_i32` (l.344).
3504        assert_eq!(get_so_rcvbuf(invalid_fd()).unwrap_err(), Errno::EBADF);
3505    }
3506
3507    #[test]
3508    fn sendmsg_with_explicit_addr_on_udp() {
3509        // `sendmsg` avec `address = Some` (sendto-style) couvre les branches
3510        // `address`/`name_ptr` (l.756, 768).
3511        let fd = socket(SocketDomain::Ipv4, SocketType::Datagram, 0).expect("socket UDP");
3512        let dest = SocketAddr::Ipv4(Ipv4SocketAddr {
3513            address: [127, 0, 0, 1],
3514            port: 9,
3515        });
3516        let data = *b"udp";
3517        let iov = [IoSlice::new(&data)];
3518        let req = SendMessageRequest {
3519            iov: &iov,
3520            address: Some(&dest),
3521            fds: &[],
3522            flags: MessageFlags::empty(),
3523        };
3524        let _ = sendmsg(fd.as_fd(), &req);
3525    }
3526}