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;