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;