Skip to main content

air_sys_types/
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//! Types purs de la famille `device` (couche 0).
6//!
7//! Cf. `docs/specs/layer-0/family-device.md`. Ce module ne fait **aucun**
8//! syscall : il décrit les miroirs `#[repr(C)]` des structures kernel
9//! (`input_event`, `input_id`, `input_absinfo`), les énumérations typées
10//! (`EventType`, `AbsAxis`, `EventClock`, `UEventAction`), les bitflags
11//! (`UEventGroups`, `UEventSocketFlags`) et le **décodeur emprunté, zéro
12//! allocation** des messages `uevent` ([`UEventMessage`] /
13//! [`UEventProperties`]).
14//!
15//! Le décodage d'un format de fil kernel stable est du **miroir** légitime
16//! en couche 0 (même catégorie que `SignalFdInfo`). Construire un modèle riche
17//! de périphérique relève de la couche 1.
18
19use bitflags::bitflags;
20
21use crate::Errno;
22
23// ─────────────────────────────────────────────────────────────────────────
24// uevent — bitflags d'ouverture du socket.
25// ─────────────────────────────────────────────────────────────────────────
26
27bitflags! {
28    /// Groupes multicast netlink `NETLINK_KOBJECT_UEVENT` à écouter.
29    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
30    pub struct UEventGroups: u32 {
31        /// Messages bruts générés par le kernel (groupe netlink 1).
32        const KERNEL = 1 << 0;
33        /// Messages re-diffusés par le gestionnaire de périphériques
34        /// userspace (groupe netlink 2, dit « libudev monitor »).
35        const USERSPACE = 1 << 1;
36    }
37}
38
39bitflags! {
40    /// Drapeaux d'ouverture du socket uevent (sous-ensemble de `SOCK_*`).
41    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
42    pub struct UEventSocketFlags: i32 {
43        /// `SOCK_NONBLOCK` — lecture non bloquante.
44        const NONBLOCK = 0x800;
45        /// `SOCK_CLOEXEC` — toujours activé par le wrapper.
46        const CLOEXEC = 0x8_0000;
47    }
48}
49
50/// Taille de buffer recommandée pour [`UEventMessage`] (8 Kio).
51///
52/// Un message uevent kernel tient quasi toujours sous 2 Kio mais peut
53/// atteindre ~16 Kio dans des cas extrêmes ; 8192 est un compromis sûr.
54pub const UEVENT_RECOMMENDED_BUFFER_SIZE: usize = 8192;
55
56// ─────────────────────────────────────────────────────────────────────────
57// uevent — action.
58// ─────────────────────────────────────────────────────────────────────────
59
60/// Action portée par un message uevent (sous-ensemble typé + repli).
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum UEventAction {
63    /// `add` — apparition d'un périphérique.
64    Add,
65    /// `remove` — disparition.
66    Remove,
67    /// `change` — changement d'état.
68    Change,
69    /// `move` — déplacement dans l'arbre `sysfs`.
70    Move,
71    /// `online` — mise en ligne.
72    Online,
73    /// `offline` — mise hors ligne.
74    Offline,
75    /// `bind` — pilote lié.
76    Bind,
77    /// `unbind` — pilote délié.
78    Unbind,
79    /// Action non reconnue.
80    Other,
81}
82
83impl UEventAction {
84    /// Mappe les octets bruts d'une action vers la variante typée.
85    #[must_use]
86    pub fn from_bytes(bytes: &[u8]) -> Self {
87        match bytes {
88            b"add" => Self::Add,
89            b"remove" => Self::Remove,
90            b"change" => Self::Change,
91            b"move" => Self::Move,
92            b"online" => Self::Online,
93            b"offline" => Self::Offline,
94            b"bind" => Self::Bind,
95            b"unbind" => Self::Unbind,
96            _ => Self::Other,
97        }
98    }
99}
100
101// ─────────────────────────────────────────────────────────────────────────
102// uevent — décodeur emprunté, zéro allocation.
103// ─────────────────────────────────────────────────────────────────────────
104
105/// En-tête binaire `libudev` préfixant les messages du groupe `USERSPACE`.
106///
107/// Disposition (offsets en octets) : `prefix[8]` (`"libudev\0"`), puis sept
108/// `u32` natifs : `magic`, `header_size`, `properties_off`, `properties_len`,
109/// et trois hachages de filtre ignorés ici. `magic` vaut `0xfeed_cafe`.
110const LIBUDEV_PREFIX: &[u8; 8] = b"libudev\0";
111const LIBUDEV_MAGIC: u32 = 0xfeed_cafe;
112/// Octets minimaux pour lire `prefix` + `magic` + `header_size` +
113/// `properties_off` + `properties_len` (8 + 4×4 = 24).
114const LIBUDEV_MIN_HEADER: usize = 24;
115
116/// Message uevent décodé, empruntant le buffer de l'appelant.
117///
118/// Aucune copie : [`UEventMessage::action`], [`UEventMessage::subsystem`],
119/// [`UEventMessage::property`] et l'itérateur [`UEventMessage::properties`]
120/// rendent des tranches d'octets (`&[u8]`) pointant dans le buffer source.
121/// Les clés/valeurs sont des **octets**, jamais présumés UTF-8 (Principe 3).
122#[derive(Debug, Clone, Copy)]
123pub struct UEventMessage<'b> {
124    /// Octets bruts du message complet.
125    raw: &'b [u8],
126    /// En-tête `action@devpath` (format kernel) ; `None` pour le format
127    /// `libudev` (l'action vient alors de la propriété `ACTION`).
128    header: Option<&'b [u8]>,
129    /// Région des paires `CLÉ=VALEUR\0`.
130    properties: &'b [u8],
131}
132
133impl<'b> UEventMessage<'b> {
134    /// Décode un message uevent reçu dans `raw`.
135    ///
136    /// Détecte automatiquement le format `libudev` (groupe `USERSPACE`) via
137    /// le préfixe magique et expose les propriétés de façon uniforme.
138    ///
139    /// # Errors
140    ///
141    /// - [`Errno::EBADMSG`] : buffer vide, ou en-tête `libudev` tronqué /
142    ///   au magic absent ou incohérent (offsets hors borne).
143    pub fn parse(raw: &'b [u8]) -> Result<Self, Errno> {
144        if raw.is_empty() {
145            return Err(Errno::EBADMSG);
146        }
147
148        if raw.len() >= LIBUDEV_PREFIX.len()
149            && raw.get(..LIBUDEV_PREFIX.len()) == Some(&LIBUDEV_PREFIX[..])
150        {
151            return Self::parse_libudev(raw);
152        }
153
154        // Format kernel : `action@devpath\0` suivi des propriétés.
155        let nul = raw.iter().position(|&b| b == 0);
156        let (header, properties) = match nul {
157            Some(index) => {
158                let header = raw.get(..index).unwrap_or(&[]);
159                let start = index.saturating_add(1);
160                let properties = raw.get(start..).unwrap_or(&[]);
161                (header, properties)
162            }
163            // Pas de NUL : tout est en-tête, pas de propriétés.
164            None => (raw, &raw[raw.len()..]),
165        };
166        Ok(Self {
167            raw,
168            header: Some(header),
169            properties,
170        })
171    }
172
173    fn parse_libudev(raw: &'b [u8]) -> Result<Self, Errno> {
174        if raw.len() < LIBUDEV_MIN_HEADER {
175            return Err(Errno::EBADMSG);
176        }
177        let magic = read_u32_native(raw, 8).ok_or(Errno::EBADMSG)?;
178        if magic != LIBUDEV_MAGIC {
179            return Err(Errno::EBADMSG);
180        }
181        let properties_off = read_u32_native(raw, 16).ok_or(Errno::EBADMSG)?;
182        let properties_len = read_u32_native(raw, 20).ok_or(Errno::EBADMSG)?;
183        let off = usize::try_from(properties_off).map_err(|_| Errno::EBADMSG)?;
184        let len = usize::try_from(properties_len).map_err(|_| Errno::EBADMSG)?;
185        let end = off.checked_add(len).ok_or(Errno::EBADMSG)?;
186        let properties = raw.get(off..end).ok_or(Errno::EBADMSG)?;
187        Ok(Self {
188            raw,
189            header: None,
190            properties,
191        })
192    }
193
194    /// L'action du message (en-tête kernel, ou propriété `ACTION` en
195    /// format `libudev`).
196    #[must_use]
197    pub fn action(&self) -> UEventAction {
198        if let Some(header) = self.header {
199            let action_bytes = match header.iter().position(|&b| b == b'@') {
200                Some(at) => header.get(..at).unwrap_or(header),
201                None => header,
202            };
203            UEventAction::from_bytes(action_bytes)
204        } else if let Some(value) = self.property(b"ACTION") {
205            UEventAction::from_bytes(value)
206        } else {
207            UEventAction::Other
208        }
209    }
210
211    /// Le `DEVPATH` relatif à `/sys` (après le `@` de l'en-tête, ou la
212    /// propriété `DEVPATH`).
213    #[must_use]
214    pub fn device_path(&self) -> Option<&'b [u8]> {
215        if let Some(header) = self.header
216            && let Some(at) = header.iter().position(|&b| b == b'@')
217        {
218            let start = at.saturating_add(1);
219            return header.get(start..);
220        }
221        self.property(b"DEVPATH")
222    }
223
224    /// Le sous-système (`SUBSYSTEM=...`).
225    #[must_use]
226    pub fn subsystem(&self) -> Option<&'b [u8]> {
227        self.property(b"SUBSYSTEM")
228    }
229
230    /// La valeur d'une propriété arbitraire par clé (première occurrence).
231    #[must_use]
232    pub fn property(&self, key: &[u8]) -> Option<&'b [u8]> {
233        self.properties().find_map(|(k, v)| (k == key).then_some(v))
234    }
235
236    /// Itère toutes les paires `(clé, valeur)` sans allouer.
237    #[must_use]
238    pub fn properties(&self) -> UEventProperties<'b> {
239        UEventProperties {
240            region: self.properties,
241            cursor: 0,
242        }
243    }
244
245    /// Les octets bruts du message (diagnostic / passthrough).
246    #[must_use]
247    pub fn as_bytes(&self) -> &'b [u8] {
248        self.raw
249    }
250}
251
252/// Itérateur emprunté sur les paires `(clé, valeur)` d'un [`UEventMessage`].
253#[derive(Debug, Clone)]
254pub struct UEventProperties<'b> {
255    region: &'b [u8],
256    cursor: usize,
257}
258
259impl<'b> Iterator for UEventProperties<'b> {
260    type Item = (&'b [u8], &'b [u8]);
261
262    fn next(&mut self) -> Option<Self::Item> {
263        loop {
264            let rest = self.region.get(self.cursor..)?;
265            if rest.is_empty() {
266                return None;
267            }
268            let (entry, consumed) = match rest.iter().position(|&b| b == 0) {
269                Some(end) => (rest.get(..end).unwrap_or(&[]), end.saturating_add(1)),
270                None => (rest, rest.len()),
271            };
272            self.cursor = self.cursor.saturating_add(consumed);
273            if entry.is_empty() {
274                continue;
275            }
276            match entry.iter().position(|&b| b == b'=') {
277                Some(eq) => {
278                    let key = entry.get(..eq).unwrap_or(&[]);
279                    let value = entry.get(eq.saturating_add(1)..).unwrap_or(&[]);
280                    return Some((key, value));
281                }
282                // Entrée sans `=` : ignorée (ni clé ni valeur).
283                None => continue,
284            }
285        }
286    }
287}
288
289/// Lit un `u32` natif à `offset` dans `bytes`, ou `None` si hors borne.
290fn read_u32_native(bytes: &[u8], offset: usize) -> Option<u32> {
291    let end = offset.checked_add(4)?;
292    let slice = bytes.get(offset..end)?;
293    let array: [u8; 4] = slice.try_into().ok()?;
294    Some(u32::from_ne_bytes(array))
295}
296
297// ─────────────────────────────────────────────────────────────────────────
298// evdev — miroirs de structures kernel.
299// ─────────────────────────────────────────────────────────────────────────
300
301/// Miroir `#[repr(C)]` de `struct input_event` (24 octets sur LP64).
302///
303/// Champs aux noms kernel conservés (ADR-029, nuance « type miroir »).
304///
305/// **Signedness des horodatages.** `struct input_event` embarque un
306/// `struct timeval { __kernel_time_t tv_sec; __kernel_suseconds_t tv_usec; }`.
307/// Sur LP64 (x86_64 / aarch64, les deux cibles d'Air), `__kernel_time_t` et
308/// `__kernel_suseconds_t` sont tous deux `long` **signés** : on type donc
309/// `sec` et `usec` en `i64` pour refléter fidèlement l'ABI kernel. La spec
310/// `family-device.md` les notait `u64` ; le miroir privilégie la fidélité de
311/// l'ABI (la taille — 8 octets — est identique, le `#[repr(C)]` inchangé).
312#[repr(C)]
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub struct InputEvent {
315    /// Secondes de l'horodatage (`struct timeval::tv_sec`, `time_t` signé).
316    pub sec: i64,
317    /// Microsecondes (`struct timeval::tv_usec`, `suseconds_t` signé).
318    pub usec: i64,
319    /// Type d'événement (`EV_KEY`, `EV_REL`, `EV_ABS`, `EV_SYN`...).
320    pub event_type: u16,
321    /// Code (touche, axe, bouton) dépendant du type.
322    pub code: u16,
323    /// Valeur (1/0 pour une touche, delta `EV_REL`, absolu `EV_ABS`).
324    pub value: i32,
325}
326
327impl InputEvent {
328    /// Réinterprète un buffer d'octets en tranche d'[`InputEvent`] (zéro copie).
329    ///
330    /// `None` si la longueur n'est pas un multiple de
331    /// `size_of::<InputEvent>()` ou si l'alignement (8 octets) n'est pas
332    /// respecté. Utile quand les octets viennent d'ailleurs (io_uring, mmap).
333    #[must_use]
334    pub fn slice_from_bytes(bytes: &[u8]) -> Option<&[InputEvent]> {
335        if bytes.is_empty() {
336            return Some(&[]);
337        }
338        let size = core::mem::size_of::<InputEvent>();
339        let align = core::mem::align_of::<InputEvent>();
340        let rem = bytes.len().checked_rem(size)?;
341        if rem != 0 {
342            return None;
343        }
344        let addr = bytes.as_ptr() as usize;
345        if addr.checked_rem(align)? != 0 {
346            return None;
347        }
348        let count = bytes.len().checked_div(size)?;
349        // SAFETY: `InputEvent` est `#[repr(C)]` et tous ses champs (i64,
350        // u16, i32) acceptent n'importe quel motif binaire. La longueur est
351        // un multiple exact de la taille et le pointeur est aligné (vérifs
352        // ci-dessus). La tranche retournée a la même durée de vie que
353        // `bytes`. Aucune écriture.
354        Some(unsafe { core::slice::from_raw_parts(bytes.as_ptr().cast::<InputEvent>(), count) })
355    }
356}
357
358/// Miroir `#[repr(C)]` de `struct input_id` (noms kernel conservés).
359#[repr(C)]
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
361pub struct InputId {
362    /// Type de bus (`BUS_USB`, `BUS_BLUETOOTH`...).
363    pub bustype: u16,
364    /// Identifiant constructeur.
365    pub vendor: u16,
366    /// Identifiant produit.
367    pub product: u16,
368    /// Version.
369    pub version: u16,
370}
371
372/// Miroir `#[repr(C)]` de `struct input_absinfo` (noms kernel conservés).
373#[repr(C)]
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
375pub struct InputAbsInfo {
376    /// Valeur courante de l'axe.
377    pub value: i32,
378    /// Borne minimale.
379    pub minimum: i32,
380    /// Borne maximale.
381    pub maximum: i32,
382    /// Bruit toléré (hystérésis).
383    pub fuzz: i32,
384    /// Zone plate autour du centre.
385    pub flat: i32,
386    /// Résolution (unités par mm ou par radian).
387    pub resolution: i32,
388}
389
390// ─────────────────────────────────────────────────────────────────────────
391// evdev — énumérations typées.
392// ─────────────────────────────────────────────────────────────────────────
393
394/// Type d'événement evdev (`EV_*`). Variante `Raw(u16)` de repli.
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub enum EventType {
397    /// `EV_SYN` — synchronisation / séparateur de paquets.
398    Synchronization,
399    /// `EV_KEY` — touches et boutons.
400    Key,
401    /// `EV_REL` — déplacements relatifs (souris).
402    Relative,
403    /// `EV_ABS` — positions absolues (tactile, manette).
404    Absolute,
405    /// `EV_MSC` — événements divers.
406    Miscellaneous,
407    /// `EV_SW` — interrupteurs binaires (capot, jack).
408    Switch,
409    /// `EV_LED` — LED.
410    Led,
411    /// `EV_SND` — sorties son.
412    Sound,
413    /// `EV_REP` — auto-répétition clavier.
414    Repeat,
415    /// `EV_FF` — retour de force.
416    ForceFeedback,
417    /// `EV_PWR` — alimentation.
418    Power,
419    /// `EV_FF_STATUS` — état du retour de force.
420    ForceFeedbackStatus,
421    /// Type non nommé.
422    Raw(u16),
423}
424
425impl EventType {
426    /// Valeur kernel `EV_*` correspondante.
427    #[must_use]
428    pub const fn to_raw(self) -> u16 {
429        match self {
430            Self::Synchronization => 0x00,
431            Self::Key => 0x01,
432            Self::Relative => 0x02,
433            Self::Absolute => 0x03,
434            Self::Miscellaneous => 0x04,
435            Self::Switch => 0x05,
436            Self::Led => 0x11,
437            Self::Sound => 0x12,
438            Self::Repeat => 0x14,
439            Self::ForceFeedback => 0x15,
440            Self::Power => 0x16,
441            Self::ForceFeedbackStatus => 0x17,
442            Self::Raw(value) => value,
443        }
444    }
445
446    /// Type typé depuis une valeur kernel `EV_*`.
447    #[must_use]
448    pub const fn from_raw(value: u16) -> Self {
449        match value {
450            0x00 => Self::Synchronization,
451            0x01 => Self::Key,
452            0x02 => Self::Relative,
453            0x03 => Self::Absolute,
454            0x04 => Self::Miscellaneous,
455            0x05 => Self::Switch,
456            0x11 => Self::Led,
457            0x12 => Self::Sound,
458            0x14 => Self::Repeat,
459            0x15 => Self::ForceFeedback,
460            0x16 => Self::Power,
461            0x17 => Self::ForceFeedbackStatus,
462            other => Self::Raw(other),
463        }
464    }
465}
466
467/// Axe absolu evdev (`ABS_*`). Variante `Raw(u16)` de repli.
468#[derive(Debug, Clone, Copy, PartialEq, Eq)]
469pub enum AbsAxis {
470    /// `ABS_X`.
471    X,
472    /// `ABS_Y`.
473    Y,
474    /// `ABS_Z`.
475    Z,
476    /// `ABS_RX`.
477    Rx,
478    /// `ABS_RY`.
479    Ry,
480    /// `ABS_RZ`.
481    Rz,
482    /// `ABS_THROTTLE`.
483    Throttle,
484    /// `ABS_RUDDER`.
485    Rudder,
486    /// `ABS_WHEEL`.
487    Wheel,
488    /// `ABS_GAS`.
489    Gas,
490    /// `ABS_BRAKE`.
491    Brake,
492    /// `ABS_HAT0X`.
493    Hat0X,
494    /// `ABS_HAT0Y`.
495    Hat0Y,
496    /// `ABS_PRESSURE`.
497    Pressure,
498    /// `ABS_DISTANCE`.
499    Distance,
500    /// `ABS_MT_SLOT` — slot multi-touch courant.
501    MtSlot,
502    /// `ABS_MT_POSITION_X`.
503    MtPositionX,
504    /// `ABS_MT_POSITION_Y`.
505    MtPositionY,
506    /// `ABS_MT_TRACKING_ID`.
507    MtTrackingId,
508    /// Axe non nommé.
509    Raw(u16),
510}
511
512impl AbsAxis {
513    /// Valeur kernel `ABS_*` correspondante.
514    #[must_use]
515    pub const fn to_raw(self) -> u16 {
516        match self {
517            Self::X => 0x00,
518            Self::Y => 0x01,
519            Self::Z => 0x02,
520            Self::Rx => 0x03,
521            Self::Ry => 0x04,
522            Self::Rz => 0x05,
523            Self::Throttle => 0x06,
524            Self::Rudder => 0x07,
525            Self::Wheel => 0x08,
526            Self::Gas => 0x09,
527            Self::Brake => 0x0a,
528            Self::Hat0X => 0x10,
529            Self::Hat0Y => 0x11,
530            Self::Pressure => 0x18,
531            Self::Distance => 0x19,
532            Self::MtSlot => 0x2f,
533            Self::MtPositionX => 0x35,
534            Self::MtPositionY => 0x36,
535            Self::MtTrackingId => 0x39,
536            Self::Raw(value) => value,
537        }
538    }
539
540    /// Axe typé depuis une valeur kernel `ABS_*`.
541    #[must_use]
542    pub const fn from_raw(value: u16) -> Self {
543        match value {
544            0x00 => Self::X,
545            0x01 => Self::Y,
546            0x02 => Self::Z,
547            0x03 => Self::Rx,
548            0x04 => Self::Ry,
549            0x05 => Self::Rz,
550            0x06 => Self::Throttle,
551            0x07 => Self::Rudder,
552            0x08 => Self::Wheel,
553            0x09 => Self::Gas,
554            0x0a => Self::Brake,
555            0x10 => Self::Hat0X,
556            0x11 => Self::Hat0Y,
557            0x18 => Self::Pressure,
558            0x19 => Self::Distance,
559            0x2f => Self::MtSlot,
560            0x35 => Self::MtPositionX,
561            0x36 => Self::MtPositionY,
562            0x39 => Self::MtTrackingId,
563            other => Self::Raw(other),
564        }
565    }
566}
567
568/// Horloge des horodatages d'événements evdev (`EVIOCSCLOCKID`).
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
570pub enum EventClock {
571    /// `CLOCK_REALTIME` (défaut kernel).
572    Realtime,
573    /// `CLOCK_MONOTONIC` (recommandé pour la corrélation d'entrées).
574    Monotonic,
575}
576
577impl EventClock {
578    /// `clockid_t` kernel correspondant (`CLOCK_REALTIME=0`,
579    /// `CLOCK_MONOTONIC=1`).
580    #[must_use]
581    pub const fn to_clockid(self) -> i32 {
582        match self {
583            Self::Realtime => 0,
584            Self::Monotonic => 1,
585        }
586    }
587}
588
589#[cfg(test)]
590mod tests;
591
592#[cfg(test)]
593// Sous `tests/`, et pas à côté : le filtre par défaut de `cargo-llvm-cov` écarte
594// `tests.rs`, `*_tests.rs` et les répertoires `tests/` — mais PAS un `proptests.rs`
595// posé en voisin, dont les lignes retomberaient dans la mesure de production.
596// `#[path]` déplace le FICHIER sans toucher l'arbre des modules : `super::` désigne
597// toujours le parent, et le contenu n'a pas à changer.
598#[path = "device/tests/proptests.rs"]
599mod proptests;