Skip to main content

air_sys_syscall/
device.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 `device`.
6//!
7//! Cf. `docs/specs/layer-0/family-device.md`.
8//!
9//! Trois sous-systèmes :
10//! - **uevent** (netlink) : [`UEventSocket`] (RAII) + [`uevent_socket_open`] ;
11//!   [`UEventSocket::read`] reçoit via `recvmsg` et **vérifie l'authenticité**
12//!   (`nl_pid == 0`, et `uid == 0` du `SCM_CREDENTIALS` en mode `USERSPACE`)
13//!   avant de décoder le message via [`air_sys_types::UEventMessage`].
14//! - **evdev** : [`evdev_read_events`] (lecture typée) et les ioctls dédiés
15//!   `EVIOC*` (jamais d'ioctl générique, ADR-021 conv. 3).
16//! - **sysfs** : aucun wrapper (cf. spec sous-section 3 — c'est la famille `fs`).
17
18use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
19use core::num::NonZeroI32;
20
21use air_sys_types::{
22    AbsAxis, Errno, EventClock, EventType, InputAbsInfo, InputEvent, InputId, UEventGroups,
23    UEventMessage, UEventSocketFlags,
24};
25
26#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
27compile_error!("air-sys-syscall::device supporte uniquement x86_64 et aarch64 (ADR-014).");
28
29// ─────────────────────────────────────────────────────────────────────────
30// Constantes kernel (identiques x86_64 / aarch64 — asm-generic).
31// ─────────────────────────────────────────────────────────────────────────
32
33const AF_NETLINK: i32 = 16;
34const SOCK_RAW: i32 = 3;
35const SOCK_CLOEXEC: i32 = 0x8_0000;
36const NETLINK_KOBJECT_UEVENT: i32 = 15;
37const SOL_SOCKET: i32 = 1;
38const SO_PASSCRED: i32 = 16;
39const SCM_CREDENTIALS: i32 = 2;
40
41/// `'E'` — octet de type des ioctls evdev.
42const EVIOC_TYPE: u8 = b'E';
43/// `_IOC_NONE` / `_IOC_WRITE` / `_IOC_READ` (asm-generic ioctl.h).
44const IOC_WRITE: u32 = 1;
45const IOC_READ: u32 = 2;
46/// Masque des 14 bits de taille d'une requête ioctl.
47const IOC_SIZE_MASK: usize = 0x3fff;
48
49// ─────────────────────────────────────────────────────────────────────────
50// uevent — socket RAII.
51// ─────────────────────────────────────────────────────────────────────────
52
53/// Socket `NETLINK_KOBJECT_UEVENT` recevant les notifications de hotplug.
54///
55/// Drop ferme le FD via [`OwnedFd`]. `CLOEXEC` est toujours activé.
56#[derive(Debug)]
57pub struct UEventSocket {
58    fd: OwnedFd,
59    /// Vrai si le groupe `USERSPACE` est écouté : on exige alors un
60    /// `SCM_CREDENTIALS` à `uid == 0` à chaque lecture.
61    check_credentials: bool,
62}
63
64impl UEventSocket {
65    /// Vue empruntée du FD sous-jacent.
66    #[must_use]
67    pub fn as_fd(&self) -> BorrowedFd<'_> {
68        use air_sys_types::fd::AsFd;
69        self.fd.as_fd()
70    }
71
72    /// Consomme et restitue le `OwnedFd`.
73    #[must_use]
74    pub fn into_fd(self) -> OwnedFd {
75        self.fd
76    }
77
78    /// Lit un message uevent et le décode en place dans `buffer`.
79    ///
80    /// Le [`UEventMessage`] retourné **emprunte** `buffer` : il reste valide
81    /// tant que le buffer n'est pas réutilisé. Aucune allocation.
82    ///
83    /// **Authenticité.** Le message est rejeté ([`Errno::EPERM`]) si l'adresse
84    /// source n'est pas le kernel (`nl_pid != 0`) ou si, en mode `USERSPACE`,
85    /// le crédential `SCM_CREDENTIALS` est absent ou n'a pas `uid == 0`.
86    ///
87    /// # Errors
88    ///
89    /// - [`Errno::EPERM`] : message rejeté (source non authentique).
90    /// - [`Errno::EAGAIN`] : socket `NONBLOCK`, aucun message disponible.
91    /// - [`Errno::EINTR`] : interrompu (remonté tel quel, ADR-021 conv. 2).
92    /// - [`Errno::EBADMSG`] : message indécodable (en-tête `libudev` corrompu).
93    /// - [`Errno::EMSGSIZE`] : message tronqué (buffer trop petit).
94    pub fn read<'b>(&self, buffer: &'b mut [u8]) -> Result<UEventMessage<'b>, Errno> {
95        let mut source = SockAddrNl::zeroed();
96        let mut cmsg_buf = [0_u8; CMSG_BUFFER_SIZE];
97
98        let mut iov = Iovec {
99            iov_base: buffer.as_mut_ptr(),
100            iov_len: buffer.len(),
101        };
102        let mut msg = MsgHdr {
103            msg_name: (&raw mut source).cast::<u8>(),
104            msg_namelen: SOCKADDR_NL_SIZE,
105            msg_iov: &raw mut iov,
106            msg_iovlen: 1,
107            msg_control: cmsg_buf.as_mut_ptr(),
108            msg_controllen: cmsg_buf.len(),
109            msg_flags: 0,
110        };
111
112        // SAFETY: `recvmsg` lit le `MsgHdr` (pointeurs valides vers `source`,
113        // `iov` couvrant `buffer`, `cmsg_buf`) et écrit le message reçu dans
114        // `buffer`, l'adresse source dans `source`, les cmsg dans `cmsg_buf`.
115        // Toutes les zones sont locales et vivantes pour la durée du syscall.
116        let ret = unsafe { raw_syscall_recvmsg(self.fd.as_raw_fd(), &raw mut msg, 0) };
117        if ret < 0 {
118            return Err(errno_from_negative_syscall_ret(ret));
119        }
120        let received = usize::try_from(ret).map_err(|_| Errno::EBADMSG)?;
121        let controllen = msg.msg_controllen.min(cmsg_buf.len());
122        let control = cmsg_buf.get(..controllen).unwrap_or(&[]);
123
124        authenticate_and_decode(
125            msg.msg_flags,
126            msg.msg_namelen,
127            source.nl_pid,
128            self.check_credentials,
129            control,
130            received,
131            buffer,
132        )
133    }
134}
135
136/// Valide l'authenticité d'un message uevent reçu puis le décode.
137///
138/// Extrait du chemin `recvmsg` pour être testable au niveau frontière
139/// (« simulateur de syscall ») sans privilège ni vrai hotplug.
140fn authenticate_and_decode<'b>(
141    msg_flags: i32,
142    namelen: u32,
143    nl_pid: u32,
144    check_credentials: bool,
145    control: &[u8],
146    received: usize,
147    buffer: &'b mut [u8],
148) -> Result<UEventMessage<'b>, Errno> {
149    if (msg_flags & MSG_TRUNC) != 0 {
150        return Err(Errno::EMSGSIZE);
151    }
152    // Authenticité 1 : l'adresse source doit être le kernel (nl_pid == 0).
153    if namelen < SOCKADDR_NL_SIZE || nl_pid != 0 {
154        return Err(Errno::EPERM);
155    }
156    // Authenticité 2 : en mode USERSPACE, exiger SCM_CREDENTIALS uid == 0.
157    if check_credentials && credential_uid(control) != Some(0) {
158        return Err(Errno::EPERM);
159    }
160    let received = received.min(buffer.len());
161    let payload = buffer.get(..received).unwrap_or(buffer);
162    UEventMessage::parse(payload)
163}
164
165/// Ouvre un socket uevent lié aux `groups` demandés.
166///
167/// `CLOEXEC` est **toujours** activé. Si `groups` contient `USERSPACE`,
168/// `SO_PASSCRED` est posé afin de vérifier l'identité des émetteurs userspace.
169///
170/// # Errors
171///
172/// - [`Errno::EINVAL`] : `groups` vide (rien à écouter).
173/// - `EMFILE` / `ENFILE` / `ENOMEM` : limites de ressources.
174pub fn uevent_socket_open(
175    groups: UEventGroups,
176    flags: UEventSocketFlags,
177) -> Result<UEventSocket, Errno> {
178    if groups.is_empty() {
179        return Err(Errno::EINVAL);
180    }
181
182    let socket_type = SOCK_RAW | SOCK_CLOEXEC | (flags.bits() & UEventSocketFlags::NONBLOCK.bits());
183    // SAFETY: socket(AF_NETLINK, SOCK_RAW|CLOEXEC[|NONBLOCK], proto) ne touche
184    // aucune mémoire utilisateur ; retourne un fd ou un errno négatif.
185    let ret = unsafe { raw_syscall_socket(AF_NETLINK, socket_type, NETLINK_KOBJECT_UEVENT) };
186    if ret < 0 {
187        return Err(errno_from_negative_syscall_ret(ret));
188    }
189    #[allow(clippy::cast_possible_truncation)]
190    let raw_fd = ret as i32;
191    // SAFETY: le kernel vient de transférer la propriété d'un fd valide ;
192    // l'envelopper dans un OwnedFd garantit la fermeture (RAII) même en cas
193    // d'erreur ultérieure (bind/setsockopt).
194    let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
195
196    let check_credentials = groups.contains(UEventGroups::USERSPACE);
197    if check_credentials {
198        let enable: i32 = 1;
199        // SAFETY: setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &enable, 4) lit 4
200        // octets à `&enable`. Pas d'écriture utilisateur.
201        let ret = unsafe {
202            raw_syscall_setsockopt(
203                fd.as_raw_fd(),
204                SOL_SOCKET,
205                SO_PASSCRED,
206                (&raw const enable).cast::<u8>(),
207                4,
208            )
209        };
210        if ret < 0 {
211            return Err(errno_from_negative_syscall_ret(ret));
212        }
213    }
214
215    let addr = SockAddrNl {
216        nl_family: u16::try_from(AF_NETLINK).expect("AF_NETLINK tient en u16"),
217        nl_pad: 0,
218        nl_pid: 0, // le kernel assigne le port id.
219        nl_groups: groups.bits(),
220    };
221    // SAFETY: bind(fd, &addr, sizeof(sockaddr_nl)) lit 12 octets à `&addr`.
222    let ret = unsafe {
223        raw_syscall_bind(
224            fd.as_raw_fd(),
225            (&raw const addr).cast::<u8>(),
226            SOCKADDR_NL_SIZE,
227        )
228    };
229    if ret < 0 {
230        return Err(errno_from_negative_syscall_ret(ret));
231    }
232
233    Ok(UEventSocket {
234        fd,
235        check_credentials,
236    })
237}
238
239// ─────────────────────────────────────────────────────────────────────────
240// uevent — structures kernel internes et parsing des credentials.
241// ─────────────────────────────────────────────────────────────────────────
242
243const SOCKADDR_NL_SIZE: u32 = 12;
244const MSG_TRUNC: i32 = 0x20;
245/// Place pour un `cmsghdr` (16 o) + `struct ucred` (12 o), aligné.
246const CMSG_BUFFER_SIZE: usize = 64;
247
248#[repr(C)]
249#[derive(Clone, Copy)]
250struct SockAddrNl {
251    nl_family: u16,
252    nl_pad: u16,
253    nl_pid: u32,
254    nl_groups: u32,
255}
256
257impl SockAddrNl {
258    const fn zeroed() -> Self {
259        Self {
260            nl_family: 0,
261            nl_pad: 0,
262            nl_pid: 0,
263            nl_groups: 0,
264        }
265    }
266}
267
268#[repr(C)]
269struct Iovec {
270    iov_base: *mut u8,
271    iov_len: usize,
272}
273
274#[repr(C)]
275struct MsgHdr {
276    msg_name: *mut u8,
277    msg_namelen: u32,
278    msg_iov: *mut Iovec,
279    msg_iovlen: usize,
280    msg_control: *mut u8,
281    msg_controllen: usize,
282    msg_flags: i32,
283}
284
285/// Extrait l'`uid` du premier `SCM_CREDENTIALS` d'un tampon de contrôle.
286///
287/// Retourne `None` si aucun cmsg `SOL_SOCKET`/`SCM_CREDENTIALS` valide n'est
288/// présent. Parsing emprunté, sans allocation, slicing via `get`.
289fn credential_uid(control: &[u8]) -> Option<u32> {
290    // struct cmsghdr { size_t cmsg_len; int cmsg_level; int cmsg_type; }
291    const CMSG_HDR_SIZE: usize = 16;
292    // CMSG_ALIGN(sizeof(cmsghdr)) = 16 sur LP64.
293    const CMSG_DATA_OFFSET: usize = 16;
294    // struct ucred { pid_t pid; uid_t uid; gid_t gid; } — uid à l'offset 4.
295    const UCRED_UID_OFFSET: usize = 4;
296
297    let mut offset = 0_usize;
298    loop {
299        let header_end = offset.checked_add(CMSG_HDR_SIZE)?;
300        let header = control.get(offset..header_end)?;
301        let len_bytes: [u8; 8] = header.get(0..8)?.try_into().ok()?;
302        let level_bytes: [u8; 4] = header.get(8..12)?.try_into().ok()?;
303        let type_bytes: [u8; 4] = header.get(12..16)?.try_into().ok()?;
304        let cmsg_len = usize::try_from(u64::from_ne_bytes(len_bytes)).ok()?;
305        let cmsg_level = i32::from_ne_bytes(level_bytes);
306        let cmsg_type = i32::from_ne_bytes(type_bytes);
307
308        if cmsg_len < CMSG_HDR_SIZE {
309            return None;
310        }
311        if cmsg_level == SOL_SOCKET && cmsg_type == SCM_CREDENTIALS {
312            let uid_start = offset
313                .checked_add(CMSG_DATA_OFFSET)?
314                .checked_add(UCRED_UID_OFFSET)?;
315            let uid_end = uid_start.checked_add(4)?;
316            let uid_bytes: [u8; 4] = control.get(uid_start..uid_end)?.try_into().ok()?;
317            return Some(u32::from_ne_bytes(uid_bytes));
318        }
319        // Avance au prochain cmsg (CMSG_ALIGN(cmsg_len), align 8).
320        let aligned = cmsg_len.checked_add(7)? & !7_usize;
321        offset = offset.checked_add(aligned)?;
322    }
323}
324
325// ─────────────────────────────────────────────────────────────────────────
326// evdev — lecture typée.
327// ─────────────────────────────────────────────────────────────────────────
328
329/// Lit un lot d'événements dans `events`, sans allocation.
330///
331/// Retourne le nombre d'événements **complets** lus. Sur un FD `NONBLOCK`
332/// sans donnée : [`Errno::EAGAIN`].
333///
334/// # Errors
335///
336/// - [`Errno::EAGAIN`] : mode non bloquant, rien à lire.
337/// - [`Errno::EPROTO`] : le kernel a renvoyé un nombre d'octets non multiple
338///   de `size_of::<InputEvent>()` (ne devrait jamais arriver).
339/// - [`Errno::EBADF`] : `device` n'est pas un FD valide.
340pub fn evdev_read_events(
341    device: BorrowedFd<'_>,
342    events: &mut [InputEvent],
343) -> Result<usize, Errno> {
344    let size = core::mem::size_of::<InputEvent>();
345    let byte_len = events.len().checked_mul(size).ok_or(Errno::EINVAL)?;
346    let ptr = events.as_mut_ptr().cast::<u8>();
347    // SAFETY: `read` écrit au plus `byte_len` octets dans `events`,
348    // réinterprété en octets. `InputEvent` est `#[repr(C)]` et accepte tout
349    // motif binaire. Le buffer est vivant pour la durée du syscall.
350    let ret = unsafe { raw_syscall_read(device.as_raw_fd(), ptr as u64, byte_len as u64) };
351    if ret < 0 {
352        return Err(errno_from_negative_syscall_ret(ret));
353    }
354    let bytes = usize::try_from(ret).unwrap_or(0);
355    let rem = bytes.checked_rem(size).unwrap_or(0);
356    if rem != 0 {
357        return Err(Errno::EPROTO);
358    }
359    Ok(bytes.checked_div(size).unwrap_or(0))
360}
361
362// ─────────────────────────────────────────────────────────────────────────
363// evdev — encodage des requêtes ioctl (_IOC).
364// ─────────────────────────────────────────────────────────────────────────
365
366/// Encode une requête ioctl `_IOC(dir, type, nr, size)` (asm-generic).
367const fn ioc(dir: u32, ty: u8, nr: u8, size: u32) -> u32 {
368    (dir << 30) | (size << 16) | ((ty as u32) << 8) | (nr as u32)
369}
370
371/// ioctl evdev à taille fixe écrivant dans `out` (`_IOR('E', nr, T)`).
372fn evdev_ioctl_read_struct(
373    device: BorrowedFd<'_>,
374    nr: u8,
375    out: *mut u8,
376    size: u32,
377) -> Result<(), Errno> {
378    let request = ioc(IOC_READ, EVIOC_TYPE, nr, size);
379    // SAFETY: ioctl(fd, _IOR('E',nr,T), out) écrit `size` octets à `out`
380    // (buffer fourni par l'appelant, dimensionné pour T) ou retourne un
381    // errno. Aucune lecture de mémoire non initialisée côté Air.
382    let ret = unsafe { raw_syscall_ioctl(device.as_raw_fd(), u64::from(request), out as u64) };
383    if ret < 0 {
384        return Err(errno_from_negative_syscall_ret(ret));
385    }
386    Ok(())
387}
388
389/// ioctl evdev remplissant un buffer de taille variable (`EVIOCG{NAME,…}`).
390///
391/// Retourne la tranche effectivement remplie. La taille encodée est plafonnée
392/// au masque 14 bits des requêtes ioctl.
393fn evdev_ioctl_read_buffer<'b>(
394    device: BorrowedFd<'_>,
395    nr: u8,
396    buffer: &'b mut [u8],
397) -> Result<&'b [u8], Errno> {
398    let capped = buffer.len().min(IOC_SIZE_MASK);
399    let size = u32::try_from(capped).expect("capped ≤ 0x3fff tient en u32");
400    let request = ioc(IOC_READ, EVIOC_TYPE, nr, size);
401    let ptr = buffer.as_mut_ptr();
402    // SAFETY: ioctl écrit au plus `size` octets à `ptr` (buffer appelant) et
403    // retourne le nombre d'octets écrits, ou un errno.
404    let ret = unsafe { raw_syscall_ioctl(device.as_raw_fd(), u64::from(request), ptr as u64) };
405    if ret < 0 {
406        return Err(errno_from_negative_syscall_ret(ret));
407    }
408    let written = usize::try_from(ret).unwrap_or(0).min(buffer.len());
409    Ok(buffer.get(..written).unwrap_or(buffer))
410}
411
412/// ioctl evdev écrivant une valeur scalaire vers le kernel (`_IOW`).
413fn evdev_ioctl_write(device: BorrowedFd<'_>, nr: u8, size: u32, arg: u64) -> Result<(), Errno> {
414    let request = ioc(IOC_WRITE, EVIOC_TYPE, nr, size);
415    // SAFETY: ioctl(fd, _IOW('E',nr,T), arg). Selon `nr`, `arg` est soit une
416    // valeur entière (grab/release/revoke), soit un pointeur vers une zone
417    // lisible de `size` octets (set_clock). Dans les deux cas l'appelant a
418    // garanti la validité ; aucune écriture côté Air.
419    let ret = unsafe { raw_syscall_ioctl(device.as_raw_fd(), u64::from(request), arg) };
420    if ret < 0 {
421        return Err(errno_from_negative_syscall_ret(ret));
422    }
423    Ok(())
424}
425
426// ─────────────────────────────────────────────────────────────────────────
427// evdev — identité et description.
428// ─────────────────────────────────────────────────────────────────────────
429
430/// `EVIOCGVERSION` — version du protocole evdev du driver.
431///
432/// # Errors
433///
434/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
435pub fn evdev_driver_version(device: BorrowedFd<'_>) -> Result<u32, Errno> {
436    let mut version: i32 = 0;
437    evdev_ioctl_read_struct(device, 0x01, (&raw mut version).cast::<u8>(), 4)?;
438    #[allow(clippy::cast_sign_loss)]
439    Ok(version as u32)
440}
441
442/// `EVIOCGID` — identifiant bus/vendor/product/version.
443///
444/// # Errors
445///
446/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
447pub fn evdev_device_id(device: BorrowedFd<'_>) -> Result<InputId, Errno> {
448    let mut id = InputId::default();
449    let size = u32::try_from(core::mem::size_of::<InputId>()).expect("input_id tient en u32");
450    evdev_ioctl_read_struct(device, 0x02, (&raw mut id).cast::<u8>(), size)?;
451    Ok(id)
452}
453
454/// `EVIOCGNAME(len)` — nom du périphérique (octets, possiblement non-UTF-8).
455///
456/// # Errors
457///
458/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
459pub fn evdev_name<'b>(device: BorrowedFd<'_>, buffer: &'b mut [u8]) -> Result<&'b [u8], Errno> {
460    evdev_ioctl_read_buffer(device, 0x06, buffer)
461}
462
463/// `EVIOCGPHYS(len)` — emplacement physique (topologie).
464///
465/// # Errors
466///
467/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
468pub fn evdev_physical_location<'b>(
469    device: BorrowedFd<'_>,
470    buffer: &'b mut [u8],
471) -> Result<&'b [u8], Errno> {
472    evdev_ioctl_read_buffer(device, 0x07, buffer)
473}
474
475/// `EVIOCGUNIQ(len)` — identifiant unique (souvent vide).
476///
477/// # Errors
478///
479/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
480pub fn evdev_unique_id<'b>(
481    device: BorrowedFd<'_>,
482    buffer: &'b mut [u8],
483) -> Result<&'b [u8], Errno> {
484    evdev_ioctl_read_buffer(device, 0x08, buffer)
485}
486
487// ─────────────────────────────────────────────────────────────────────────
488// evdev — capacités.
489// ─────────────────────────────────────────────────────────────────────────
490
491/// `EVIOCGBIT(event_type, len)` — bitmap des codes supportés. `event_type =
492/// None` interroge les **types** supportés (`EVIOCGBIT(0, len)`).
493///
494/// # Errors
495///
496/// - [`Errno::EINVAL`] : `event_type` hors de la plage `EV_*` adressable.
497/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
498pub fn evdev_supported_codes<'b>(
499    device: BorrowedFd<'_>,
500    event_type: Option<EventType>,
501    bitmap: &'b mut [u8],
502) -> Result<&'b [u8], Errno> {
503    let ev = match event_type {
504        Some(ty) => u8::try_from(ty.to_raw()).map_err(|_| Errno::EINVAL)?,
505        None => 0,
506    };
507    // EVIOCGBIT(ev, len) → nr = 0x20 + ev (ev ≤ EV_MAX = 0x1f).
508    let nr = 0x20_u8.checked_add(ev).ok_or(Errno::EINVAL)?;
509    evdev_ioctl_read_buffer(device, nr, bitmap)
510}
511
512/// `EVIOCGPROP(len)` — propriétés du périphérique (`INPUT_PROP_*`).
513///
514/// # Errors
515///
516/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
517pub fn evdev_properties<'b>(
518    device: BorrowedFd<'_>,
519    bitmap: &'b mut [u8],
520) -> Result<&'b [u8], Errno> {
521    evdev_ioctl_read_buffer(device, 0x09, bitmap)
522}
523
524/// `EVIOCGABS(axis)` — plage et état d'un axe absolu.
525///
526/// # Errors
527///
528/// - [`Errno::EINVAL`] : `axis` hors de la plage `ABS_*` adressable.
529/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
530pub fn evdev_abs_info(device: BorrowedFd<'_>, axis: AbsAxis) -> Result<InputAbsInfo, Errno> {
531    let abs = u8::try_from(axis.to_raw()).map_err(|_| Errno::EINVAL)?;
532    // EVIOCGABS(abs) → nr = 0x40 + abs (abs ≤ ABS_MAX = 0x3f).
533    let nr = 0x40_u8.checked_add(abs).ok_or(Errno::EINVAL)?;
534    let mut info = InputAbsInfo::default();
535    let size =
536        u32::try_from(core::mem::size_of::<InputAbsInfo>()).expect("input_absinfo tient en u32");
537    evdev_ioctl_read_struct(device, nr, (&raw mut info).cast::<u8>(), size)?;
538    Ok(info)
539}
540
541// ─────────────────────────────────────────────────────────────────────────
542// evdev — état courant.
543// ─────────────────────────────────────────────────────────────────────────
544
545/// `EVIOCGKEY(len)` — état actuel (enfoncé/relâché) de toutes les touches.
546///
547/// # Errors
548///
549/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
550pub fn evdev_key_state<'b>(d: BorrowedFd<'_>, bitmap: &'b mut [u8]) -> Result<&'b [u8], Errno> {
551    evdev_ioctl_read_buffer(d, 0x18, bitmap)
552}
553
554/// `EVIOCGLED(len)` — état des LED.
555///
556/// # Errors
557///
558/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
559pub fn evdev_led_state<'b>(d: BorrowedFd<'_>, bitmap: &'b mut [u8]) -> Result<&'b [u8], Errno> {
560    evdev_ioctl_read_buffer(d, 0x19, bitmap)
561}
562
563/// `EVIOCGSND(len)` — état des sorties son.
564///
565/// # Errors
566///
567/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
568pub fn evdev_sound_state<'b>(d: BorrowedFd<'_>, bitmap: &'b mut [u8]) -> Result<&'b [u8], Errno> {
569    evdev_ioctl_read_buffer(d, 0x1a, bitmap)
570}
571
572/// `EVIOCGSW(len)` — état des interrupteurs (capot, jack...).
573///
574/// # Errors
575///
576/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
577pub fn evdev_switch_state<'b>(d: BorrowedFd<'_>, bitmap: &'b mut [u8]) -> Result<&'b [u8], Errno> {
578    evdev_ioctl_read_buffer(d, 0x1b, bitmap)
579}
580
581// ─────────────────────────────────────────────────────────────────────────
582// evdev — contrôle exclusif et horloge.
583// ─────────────────────────────────────────────────────────────────────────
584
585/// `EVIOCGRAB` (argument `1`) — capture exclusive du périphérique.
586///
587/// # Errors
588///
589/// - [`Errno::EBUSY`] : un autre client détient déjà la capture.
590pub fn evdev_grab(device: BorrowedFd<'_>) -> Result<(), Errno> {
591    evdev_ioctl_write(device, 0x90, 4, 1)
592}
593
594/// `EVIOCGRAB` (pointeur nul) — relâche la capture exclusive.
595///
596/// # Errors
597///
598/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
599pub fn evdev_release(device: BorrowedFd<'_>) -> Result<(), Errno> {
600    evdev_ioctl_write(device, 0x90, 4, 0)
601}
602
603/// `EVIOCREVOKE` — révoque définitivement l'accès à ce FD (irréversible).
604///
605/// # Errors
606///
607/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
608pub fn evdev_revoke(device: BorrowedFd<'_>) -> Result<(), Errno> {
609    evdev_ioctl_write(device, 0x91, 4, 0)
610}
611
612/// `EVIOCSCLOCKID` — choisit l'horloge des horodatages des événements.
613///
614/// # Errors
615///
616/// - [`Errno::ENOTTY`] : `device` n'est pas un periphérique evdev.
617pub fn evdev_set_clock(device: BorrowedFd<'_>, clock: EventClock) -> Result<(), Errno> {
618    let clockid: i32 = clock.to_clockid();
619    evdev_ioctl_write(device, 0xa0, 4, (&raw const clockid) as u64)
620}
621
622// ─────────────────────────────────────────────────────────────────────────
623// asm! wrappers x86_64 / aarch64.
624// ─────────────────────────────────────────────────────────────────────────
625
626#[cfg(target_arch = "x86_64")]
627#[inline]
628unsafe fn raw_syscall_socket(domain: i32, ty: i32, protocol: i32) -> i64 {
629    let ret: i64;
630    // SAFETY: SYS_socket (x86_64 = 41). Pas d'accès mémoire utilisateur.
631    unsafe {
632        core::arch::asm!(
633            "syscall",
634            in("rax") 41_i64,
635            in("rdi") i64::from(domain),
636            in("rsi") i64::from(ty),
637            in("rdx") i64::from(protocol),
638            lateout("rax") ret,
639            lateout("rcx") _,
640            lateout("r11") _,
641            options(nostack, preserves_flags, readonly),
642        );
643    }
644    ret
645}
646
647#[cfg(target_arch = "aarch64")]
648#[inline]
649unsafe fn raw_syscall_socket(domain: i32, ty: i32, protocol: i32) -> i64 {
650    let ret: i64;
651    // SAFETY: SYS_socket (aarch64 = 198).
652    unsafe {
653        core::arch::asm!(
654            "svc 0",
655            in("x8") 198_i64,
656            inout("x0") i64::from(domain) => ret,
657            in("x1") i64::from(ty),
658            in("x2") i64::from(protocol),
659            options(nostack, preserves_flags, readonly),
660        );
661    }
662    ret
663}
664
665#[cfg(target_arch = "x86_64")]
666#[inline]
667unsafe fn raw_syscall_bind(fd: i32, address: *const u8, addrlen: u32) -> i64 {
668    let ret: i64;
669    // SAFETY: SYS_bind (x86_64 = 49). Le kernel lit `addrlen` octets à
670    // `address` ; pas d'écriture utilisateur.
671    unsafe {
672        core::arch::asm!(
673            "syscall",
674            in("rax") 49_i64,
675            in("rdi") i64::from(fd),
676            in("rsi") address as u64,
677            in("rdx") u64::from(addrlen),
678            lateout("rax") ret,
679            lateout("rcx") _,
680            lateout("r11") _,
681            options(nostack, preserves_flags, readonly),
682        );
683    }
684    ret
685}
686
687#[cfg(target_arch = "aarch64")]
688#[inline]
689unsafe fn raw_syscall_bind(fd: i32, address: *const u8, addrlen: u32) -> i64 {
690    let ret: i64;
691    // SAFETY: SYS_bind (aarch64 = 200).
692    unsafe {
693        core::arch::asm!(
694            "svc 0",
695            in("x8") 200_i64,
696            inout("x0") i64::from(fd) => ret,
697            in("x1") address as u64,
698            in("x2") u64::from(addrlen),
699            options(nostack, preserves_flags, readonly),
700        );
701    }
702    ret
703}
704
705#[cfg(target_arch = "x86_64")]
706#[inline]
707unsafe fn raw_syscall_setsockopt(
708    fd: i32,
709    level: i32,
710    optname: i32,
711    optval: *const u8,
712    optlen: u32,
713) -> i64 {
714    let ret: i64;
715    // SAFETY: SYS_setsockopt (x86_64 = 54). Le kernel lit `optlen` octets à
716    // `optval`. Pas d'écriture utilisateur.
717    unsafe {
718        core::arch::asm!(
719            "syscall",
720            in("rax") 54_i64,
721            in("rdi") i64::from(fd),
722            in("rsi") i64::from(level),
723            in("rdx") i64::from(optname),
724            in("r10") optval as u64,
725            in("r8") u64::from(optlen),
726            lateout("rax") ret,
727            lateout("rcx") _,
728            lateout("r11") _,
729            options(nostack, preserves_flags, readonly),
730        );
731    }
732    ret
733}
734
735#[cfg(target_arch = "aarch64")]
736#[inline]
737unsafe fn raw_syscall_setsockopt(
738    fd: i32,
739    level: i32,
740    optname: i32,
741    optval: *const u8,
742    optlen: u32,
743) -> i64 {
744    let ret: i64;
745    // SAFETY: SYS_setsockopt (aarch64 = 208).
746    unsafe {
747        core::arch::asm!(
748            "svc 0",
749            in("x8") 208_i64,
750            inout("x0") i64::from(fd) => ret,
751            in("x1") i64::from(level),
752            in("x2") i64::from(optname),
753            in("x3") optval as u64,
754            in("x4") u64::from(optlen),
755            options(nostack, preserves_flags, readonly),
756        );
757    }
758    ret
759}
760
761#[cfg(target_arch = "x86_64")]
762#[inline]
763unsafe fn raw_syscall_recvmsg(fd: i32, msg: *mut MsgHdr, flags: i32) -> i64 {
764    let ret: i64;
765    // SAFETY: SYS_recvmsg (x86_64 = 47). Le kernel lit/écrit le `msghdr` à
766    // `msg` et écrit dans les buffers qu'il référence (iov, control, name).
767    unsafe {
768        core::arch::asm!(
769            "syscall",
770            in("rax") 47_i64,
771            in("rdi") i64::from(fd),
772            in("rsi") msg as u64,
773            in("rdx") i64::from(flags),
774            lateout("rax") ret,
775            lateout("rcx") _,
776            lateout("r11") _,
777            options(nostack, preserves_flags),
778        );
779    }
780    ret
781}
782
783#[cfg(target_arch = "aarch64")]
784#[inline]
785unsafe fn raw_syscall_recvmsg(fd: i32, msg: *mut MsgHdr, flags: i32) -> i64 {
786    let ret: i64;
787    // SAFETY: SYS_recvmsg (aarch64 = 212).
788    unsafe {
789        core::arch::asm!(
790            "svc 0",
791            in("x8") 212_i64,
792            inout("x0") i64::from(fd) => ret,
793            in("x1") msg as u64,
794            in("x2") i64::from(flags),
795            options(nostack, preserves_flags),
796        );
797    }
798    ret
799}
800
801#[cfg(target_arch = "x86_64")]
802#[inline]
803unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: u64) -> i64 {
804    let ret: i64;
805    // SAFETY: SYS_read (x86_64 = 0). Le kernel écrit au plus `count` octets à
806    // `buffer` (zone valide garantie par l'appelant).
807    unsafe {
808        core::arch::asm!(
809            "syscall",
810            in("rax") 0_i64,
811            in("rdi") i64::from(fd),
812            in("rsi") buffer,
813            in("rdx") count,
814            lateout("rax") ret,
815            lateout("rcx") _,
816            lateout("r11") _,
817            options(nostack, preserves_flags),
818        );
819    }
820    ret
821}
822
823#[cfg(target_arch = "aarch64")]
824#[inline]
825unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: u64) -> i64 {
826    let ret: i64;
827    // SAFETY: SYS_read (aarch64 = 63).
828    unsafe {
829        core::arch::asm!(
830            "svc 0",
831            in("x8") 63_i64,
832            inout("x0") i64::from(fd) => ret,
833            in("x1") buffer,
834            in("x2") count,
835            options(nostack, preserves_flags),
836        );
837    }
838    ret
839}
840
841#[cfg(target_arch = "x86_64")]
842#[inline]
843unsafe fn raw_syscall_ioctl(fd: i32, request: u64, arg: u64) -> i64 {
844    let ret: i64;
845    // SAFETY: SYS_ioctl (x86_64 = 16). L'effet mémoire dépend de `request` ;
846    // chaque appelant a justifié la validité de `arg` pour sa requête.
847    unsafe {
848        core::arch::asm!(
849            "syscall",
850            in("rax") 16_i64,
851            in("rdi") i64::from(fd),
852            in("rsi") request,
853            in("rdx") arg,
854            lateout("rax") ret,
855            lateout("rcx") _,
856            lateout("r11") _,
857            options(nostack, preserves_flags),
858        );
859    }
860    ret
861}
862
863#[cfg(target_arch = "aarch64")]
864#[inline]
865unsafe fn raw_syscall_ioctl(fd: i32, request: u64, arg: u64) -> i64 {
866    let ret: i64;
867    // SAFETY: SYS_ioctl (aarch64 = 29).
868    unsafe {
869        core::arch::asm!(
870            "svc 0",
871            in("x8") 29_i64,
872            inout("x0") i64::from(fd) => ret,
873            in("x1") request,
874            in("x2") arg,
875            options(nostack, preserves_flags),
876        );
877    }
878    ret
879}
880
881// ─────────────────────────────────────────────────────────────────────────
882// errno helper.
883// ─────────────────────────────────────────────────────────────────────────
884
885fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
886    debug_assert!(ret < 0 && ret > -4096);
887    #[allow(clippy::cast_possible_truncation)]
888    let raw = ret.wrapping_neg() as i32;
889    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
890    Errno::from_nonzero(nz)
891}
892
893#[cfg(test)]
894mod tests;