Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec couche 1 — air-device (énumération, surveillance, identité des périphériques)

Spécification technique — Version 1.0. Couche 1 « Primitives système ». Dernière crate couche 1.

Position et méthode

air-device construit, au-dessus de la famille device couche 0 (uevent netlink + evdev + sysfs via fs), le modèle de périphériques d’Air : énumération du matériel, surveillance du hotplug, accès typé aux propriétés et à l’identité. C’est l’équivalent fonctionnel de libudev, réécrit nativement en Rust, sans aucune dépendance C.

Décision de socle (gravée, 2026-06-12). air-device est natif : zéro libudev, zéro dépendance à systemd-udevd. Il énumère en parcourant /sys lui-même (via air-filesystem) et reçoit le hotplug en lisant le socket uevent kernel (couche 0). Conséquence assumée : Air ne dépend pas, pour la découverte de matériel, d’un démon systemd qui tourne — c’est le premier maillon de l’indépendance progressive vis-à-vis de systemd (cf. stratégie de remplacement : udevd est le candidat précoce, peu risqué). À réconcilier : la ligne « libudev en couche 1 » d’ADR-005 doit être amendée (pré-public → édition directe ou RFC, ADR-015). Même nature que les réconciliations passées : AirLog→socket journald natif, DNS maison→pas de getaddrinfo.

Règle du joint (anti-contrat-à-vie). La surface publique d’air-device s’exprime exclusivement dans le vocabulaire d’AirAirDevice, AirSubsystem, propriétés typées — jamais un miroir d’udev (udev_device, noms ID_* bruts). C’est ce qui rend l’implémentation dessous (sysfs natif aujourd’hui, autre chose demain) remplaçable sans douleur. À vérifier en CI : aucune couche > 1 ne dépend d’un type systemd/udev-spécifique.

API Rust d’abord ; ABI C (libair-base/dédié) différée. Méthode doc-d’abord : ce contrat est validé avant implémentation.

Conventions de couche 1 (rappel)

Rust idiomatique, aucun unsafe exposé ; couverture 100 % (couche fondatrice) ; nommage explicite (ADR-029) ; arithmétique défensive (Principe 2) ; zéro présomption d’encodage (Principe 3 — les noms/attributs sysfs sont des octets, pas forcément UTF-8 → AirString faillible vs &[u8]/AirPath) ; validation en amont (Principe 4).

Dépendances assumées (règle des 80 %, ADR-024)

  • Aucune dépendance externe. Pur Rust sur la couche 0 (air-sys-syscall::device, ::fs) et la couche 1 (air-base-lib, air-filesystem). Pas de libudev.
  • Pas de logique politique (macro-archi) : air-device énumère, identifie, notifie. Les décisions (autoriser/monter/appliquer une règle) sont en couche 5.

Carte de consommation couche 0 (pour le journal de dette doc) :

  • énumération/attributs → air-filesystem (/sys : openat, read, getdents64, lecture de symlinks pour la résolution DEVPATH/parent) ;
  • hotplug → device::UEventSocket + UEventMessage (décodeur emprunté couche 0, déjà fuzzé) ;
  • identité d’entrée → device::evdev_* (§5).

Section 1 — Le modèle : AirDevice

Un AirDevice est un nœud de l’arbre /sys/devices, identifié par son chemin sysfs. Construction paresseuse : les attributs sont lus à la demande (un device est bon marché à créer, on ne lit /sys que si on interroge).

#![allow(unused)]
fn main() {
/// Un périphérique du système, ancré sur son entrée sysfs.
pub struct AirDevice { /* syspath possédé + cache d'attributs paresseux */ }

impl AirDevice {
    /// Construit depuis un chemin sysfs absolu (`/sys/devices/...`).
    /// # Errors `NotFound` si le chemin n'existe pas / n'est pas un device.
    pub fn from_sys_path(path: &AirPath) -> AirResult<Self>;

    /// Construit depuis un `DEVPATH` d'uevent (relatif à `/sys`).
    pub fn from_dev_path(devpath: &[u8]) -> AirResult<Self>;

    /// Nom court du device (dernier composant du syspath), p. ex. `event3`, `sda`.
    pub fn sys_name(&self) -> &AirPath;
    /// Chemin sysfs absolu.
    pub fn sys_path(&self) -> &AirPath;
    /// Sous-système (`/sys/.../subsystem` résolu), typé.
    pub fn subsystem(&self) -> AirResult<AirSubsystem>;
    /// Nœud `/dev` associé (`DEVNAME`), s'il existe (p. ex. `/dev/input/event3`).
    pub fn device_node(&self) -> AirResult<Option<AirPath>>;

    /// Lit un attribut sysfs brut (octets), p. ex. `name`, `uevent`, `dev`.
    /// # Errors `NotFound` si l'attribut n'existe pas.
    pub fn attribute(&self, name: &str) -> AirResult<AirByteBuffer>;
    /// Attribut interprété en UTF-8 trimé (commodité ; faillible, Principe 3).
    pub fn attribute_str(&self, name: &str) -> AirResult<AirString>;
    /// Attribut parsé en entier (base 10 ou `0x`). # Errors `InvalidData`.
    pub fn attribute_int(&self, name: &str) -> AirResult<i64>;
    /// Écrit un attribut (p. ex. déclencher `uevent`, régler `power/control`).
    /// **Privilégié** selon l'attribut. # Errors `PermissionDenied`.
    pub fn write_attribute(&self, name: &str, value: &[u8]) -> AirResult<()>;

    /// Propriétés `uevent` du device (lues depuis `/sys/.../uevent`), itérables.
    pub fn properties(&self) -> AirResult<AirDeviceProperties>;
    /// Valeur d'une propriété par clé.
    pub fn property(&self, key: &str) -> AirResult<Option<AirByteBuffer>>;

    /// Parent direct dans l'arbre sysfs (`..` jusqu'au prochain device réel).
    pub fn parent(&self) -> AirResult<Option<AirDevice>>;
    /// Premier ancêtre du sous-système (et `devtype`) donné — remplace
    /// `udev_device_get_parent_with_subsystem_devtype`.
    pub fn parent_with_subsystem(&self, subsystem: AirSubsystem)
        -> AirResult<Option<AirDevice>>;
}
}

AirSubsystem : enum typé des sous-systèmes courants (Input, Block, Net, Usb, Pci, Tty, Hwmon, Drm, Sound…) + Other(AirString) pour ceux non encore nommés (jamais de blocage — cohérent ADR-029 « variante Raw »).


Section 2 — Énumération : AirDeviceEnumerator

#![allow(unused)]
fn main() {
/// Parcourt `/sys` et liste les devices correspondant à un filtre.
pub struct AirDeviceEnumerator { /* filtres accumulés */ }

impl AirDeviceEnumerator {
    pub fn new() -> Self;
    /// Restreint à un sous-système (`/sys/class/<sub>` ou `/sys/bus/<sub>`).
    pub fn match_subsystem(self, subsystem: AirSubsystem) -> Self;
    /// Restreint par nom court (`sys_name`).
    pub fn match_sys_name(self, pattern: &str) -> Self;
    /// Restreint aux devices ayant une propriété `key=value`.
    pub fn match_property(self, key: &str, value: &str) -> Self;
    /// Restreint aux devices ayant l'attribut sysfs `name=value`.
    pub fn match_attribute(self, name: &str, value: &[u8]) -> Self;

    /// Exécute le parcours et retourne les devices (allocation autorisée
    /// couche 1 ; le parcours sysfs est intrinsèquement allouant).
    /// # Errors propage les erreurs `fs` non triviales (≠ `NotFound` toléré).
    pub fn enumerate(&self) -> AirResult<Vec<AirDevice>>;
}
}

Le parcours suit /sys/class/<subsystem>/ et /sys/bus/<subsystem>/devices/, résout les symlinks vers /sys/devices/..., déduplique par syspath canonique. Filtrage paresseux : un device n’est matérialisé que s’il passe le filtre (on ne lit pas tous les attributs de toute la machine).


Section 3 — Surveillance du hotplug : AirDeviceMonitor

#![allow(unused)]
fn main() {
/// Flux d'événements d'apparition/disparition de matériel.
pub struct AirDeviceMonitor { /* UEventSocket + filtres */ }

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AirDeviceAction { Added, Removed, Changed, Bound, Unbound, Moved, Online, Offline, Other }

/// Un événement hotplug : action + device concerné.
pub struct AirDeviceEvent { /* action + AirDevice + propriétés de l'événement */ }

impl AirDeviceEvent {
    pub fn action(&self) -> AirDeviceAction;
    pub fn device(&self) -> &AirDevice;
    pub fn property(&self, key: &str) -> Option<&[u8]>;
}

impl AirDeviceMonitor {
    /// Ouvre la surveillance sur le **groupe kernel** (brut, sans démon udev).
    /// # Errors si le socket netlink ne peut être ouvert/lié.
    pub fn open() -> AirResult<Self>;
    /// Filtre côté Air par sous-système (n'expose que les events pertinents).
    pub fn match_subsystem(self, subsystem: AirSubsystem) -> Self;

    /// FD sous-jacent, pour intégration dans un reactor (io_uring/poll).
    pub fn as_fd(&self) -> BorrowedFd<'_>;
    /// Lit le prochain événement (bloquant, ou `WouldBlock` si non-bloquant).
    /// Décodé via le décodeur uevent couche 0 (emprunté), puis matérialisé.
    pub fn next_event(&mut self) -> AirResult<AirDeviceEvent>;
}
}

Décision (groupe KERNEL, autonomie). On écoute le groupe netlink kernel (brut), pas le groupe USERSPACE re-diffusé par udevd — c’est ce qui rend la surveillance indépendante d’un démon systemd. Contrepartie honnête : un event kernel peut précéder la stabilisation complète de /sys (un attribut pas encore peuplé). Mitigations spécifiées : AirDevice lit les attributs à la demande (pas au moment de l’event), et un consommateur qui a besoin d’un attribut tardif re-lit (le device est ré-interrogeable). On ne ré-implémente pas le moteur de règles d’udev (hors périmètre couche 1 ; politique = couche 5).


Section 4 — Sous-systèmes typés (cœur v1)

Tout AirDevice expose ses propriétés en générique (clé/valeur). Au-dessus, quelques sous-systèmes à forte valeur reçoivent un accès typé (vues légères construites depuis sysfs), extensibles au besoin :

#![allow(unused)]
fn main() {
/// Vue typée d'un device réseau (`/sys/class/net/<iface>`).
pub struct AirNetDevice<'d>(&'d AirDevice);
impl AirNetDevice<'_> {
    pub fn interface_name(&self) -> AirResult<AirString>;     // sys_name
    pub fn mac_address(&self) -> AirResult<AirMacAddress>;    // address
    pub fn is_up(&self) -> AirResult<bool>;                   // operstate/flags
}

/// Vue typée d'un device bloc (`/sys/class/block/<name>`).
pub struct AirBlockDevice<'d>(&'d AirDevice);
impl AirBlockDevice<'_> {
    pub fn size_bytes(&self) -> AirResult<u64>;     // size × 512
    pub fn is_removable(&self) -> AirResult<bool>;  // removable
    pub fn model(&self) -> AirResult<Option<AirString>>;
}

/// Vue typée d'un device USB (`bus usb`).
pub struct AirUsbDevice<'d>(&'d AirDevice);
impl AirUsbDevice<'_> {
    pub fn vendor_id(&self) -> AirResult<u16>;   // idVendor (hex)
    pub fn product_id(&self) -> AirResult<u16>;  // idProduct (hex)
    pub fn serial(&self) -> AirResult<Option<AirString>>;
}
}

Décision (v1 : générique + 3 typés). net, block, usb sont typés (les plus utiles tôt) ; tout le reste reste générique (clé/valeur via §1), le typage s’ajoutant au besoin sans casser la surface. L’entrée typée (input) est traitée à part en §5.


Section 5 — Périphériques d’entrée : AirInputDevice

air-device couvre l’entrée au niveau périphérique : ouverture, identité, capacités, capture, et lecture typée d’événements (mince enrobage des evdev_* couche 0). L’interprétation de l’entrée (layouts clavier, gestes, focus, association siège) est hors périmètre — c’est de la politique compositeur (ADR-003), spécifiée ailleurs.

#![allow(unused)]
fn main() {
pub struct AirInputDevice { /* OwnedFd evdev + identité */ }

impl AirInputDevice {
    /// Ouvre `/dev/input/eventX` (via un `AirDevice` du sous-système `Input`).
    /// Horloge des horodatages réglée sur **monotone** (corrélation fiable).
    pub fn open(device: &AirDevice) -> AirResult<Self>;

    pub fn name(&self) -> AirResult<AirString>;          // EVIOCGNAME
    pub fn id(&self) -> AirResult<AirInputId>;           // EVIOCGID (bus/vendor/product)
    pub fn physical_location(&self) -> AirResult<AirString>; // EVIOCGPHYS
    /// Capacités : types d'événements et codes supportés, propriétés.
    pub fn capabilities(&self) -> AirResult<AirInputCapabilities>;

    /// Capture exclusive (`grab`) / relâche. Révocation irréversible d'un FD cédé.
    pub fn grab(&mut self) -> AirResult<()>;
    pub fn release(&mut self) -> AirResult<()>;

    pub fn as_fd(&self) -> BorrowedFd<'_>; // pour reactor
    /// Lit un lot d'événements typés (enrobe `evdev_read_events`).
    pub fn read_events<'b>(&self, out: &'b mut [AirInputEvent]) -> AirResult<&'b [AirInputEvent]>;
}
}

AirInputEvent enrichit l’InputEvent couche 0 d’un horodatage AirInstant (l’horloge étant connue = monotone) et d’un event_type/code typés.


Section 6 — Décisions de fond

  1. Natif, zéro libudev/udevd — autonomie de découverte, premier pas de l’indépendance systemd ; ADR-005 (ligne libudev) à amender.
  2. Vocabulaire Air, pas de fuite udev — le joint qui rend l’implémentation remplaçable ; vérifié en CI.
  3. Groupe uevent KERNEL (pas USERSPACE/udevd) — autonomie, au prix d’une stabilisation sysfs non garantie (attributs lus/relus à la demande).
  4. Pas de moteur de règles, pas de politique — énumération + identité + notification seulement ; règles/nommage persistant/décisions = couche 5.
  5. Générique partout + typage des sous-systèmes à forte valeur (net/block/usb,
    • input à part), extensible.
  6. Octets vs UTF-8 stricts (Principe 3) — noms/attributs sysfs faillibles en AirString.

Section 7 — Stratégie de tests

  • Énumération : contre le vrai /sys du runner (présent en CI) — lister net/block, vérifier syspath/sysname/subsystem ; filtres (subsystem, propriété, attribut) ; dédup ; parent/parent_with_subsystem.
  • Attributs : parsing attribute_int/_str (base 10/hex, trim, non-UTF-8 → erreur propre) — property-based + fuzzing sur les parseurs d’attributs (ils ingèrent des octets sysfs = entrée externe, Principe 3).
  • Hotplug : AirDeviceMonitor — harnais privilégié déclenchant un vrai uevent (modprobe/rmmod d’un module factice, ou écriture /sys/.../uevent) ; à défaut de privilège, skip propre (COVERAGE-EXCEPTIONS catégorie PRIVILEGE) et test du décodage sur trames synthétiques.
  • Input : périphérique virtuel uinput (comme la couche 0) — injecter des événements, relire typé, vérifier identité/capacités/grab (EBUSY au second grab). Skip propre hors privilège.
  • Couverture 100 % hors exceptions ; ensemble « couvrable » VIDE ; exceptions (branches privilégiées non atteignables) documentées et justifiées.

Section 8 — Travail à reprendre

  • Couche 1 complète côté specs après ce document (8/8 crates).
  • Manques couche 0 éventuels révélés à l’implémentation (parcours sysfs) → à consigner (dette doc). Aucun anticipé : fs (openat/getdents64/readlink) et device (uevent/evdev) couvrent le besoin.
  • Hors périmètre, à spécifier ailleurs : interprétation de l’entrée (compositeur, ADR-003) ; nommage persistant / règles (couche 5) ; amendement ADR-005 (ligne libudev).

Licence du document : MPL 2.0 Statut : Spécification technique de air-device (couche 1), construite nativement sur la famille device couche 0. Dernière crate couche 1 spécifiée.