//! The device registry: parse `/system/configuration/devices.csv` into match rules and bind a //! reported device to a driver. This is the data-driven replacement for the //! device manager's three hand-written `switch` tables (`pciDriverForIdentity`, //! `hidDriverFor`, `usbDriverForIdentity`); the registry is now **authoritative** //! — a device that no row matches goes unbound (logged), never guessed. //! //! Pure logic: no hardware access, no syscalls, no allocator. `parse` fills a //! caller-provided `[]Rule` whose string fields (`hid`, `driver`) are slices //! *into the CSV source*, so the source buffer must outlive the rules (the //! manager holds it in a static buffer for the life of the process — zero-copy). //! That keeps this module freestanding and unit-testable with plain `zig test`. //! //! The file format (docs/device-driver-development/device-manager.md, and the //! `/system/configuration/devices.csv` header itself): one rule per line, nine comma-separated //! fields, `#` starts a comment (whole-line or trailing), blank lines ignored. //! //! bus, base, class, prog_if, vendor, device, subsystem, hid, driver //! //! `bus` is `pci`/`usb`/`acpi`; the numeric fields are hex (with or without a //! `0x` prefix); `*` or an empty field is a wildcard (matches anything). For PCI //! the class triple is base/subclass/prog-IF; for USB it is class/subclass/ //! protocol with vendor/device the idVendor/idProduct; ACPI matches on `hid` //! (e.g. "PNP0303") with the triple left blank. `driver` is a full ramdisk path. const std = @import("std"); const csv = @import("csv"); /// Which bus a rule or a reported device belongs to. `unknown` is what an /// unrecognised `bus` token parses to — such a rule never matches (its bus /// equals no real device's), so a typo fails safe rather than binding wrongly. pub const Bus = enum { pci, usb, acpi, unknown, pub fn fromToken(token: []const u8) Bus { if (std.mem.eql(u8, token, "pci")) return .pci; if (std.mem.eql(u8, token, "usb")) return .usb; if (std.mem.eql(u8, token, "acpi")) return .acpi; return .unknown; } }; /// A reported device's full identity, as the manager assembles it from a /// `child_added`: the bus-native class triple plus the numeric ids the widened /// ABI now carries, or the ACPI `_HID` string. Fields a given bus does not have /// are zero / empty (a PCI function has no `hid`; an ACPI device has no vendor). pub const Identity = struct { bus: Bus, base: u8 = 0, subclass: u8 = 0, prog_if: u8 = 0, vendor: u16 = 0, device: u16 = 0, subsystem: u32 = 0, hid: []const u8 = "", }; /// One parsed registry row. A `null` field is a wildcard — it matches any value /// and contributes nothing to specificity. String fields point into the CSV /// source that was parsed (see the module doc). pub const Rule = struct { bus: Bus, base: ?u8 = null, subclass: ?u8 = null, prog_if: ?u8 = null, vendor: ?u16 = null, device: ?u16 = null, subsystem: ?u32 = null, hid: ?[]const u8 = null, driver: []const u8, }; /// Specificity weights: how much each pinned field counts toward "most specific /// wins". Doubling from the coarsest (`base`) so that each level outweighs *all* /// coarser levels combined (1+2+4+8+16 = 31 < 32) — a rule that pins `device` /// always beats any rule that does not, no matter how many coarse fields the /// latter pins. `hid` and `device` share the top tier (the user's "hid and /// device weigh heaviest"); they never co-occur, since `hid` is ACPI-only and /// `device` is a PCI/USB numeric id. const weight_base: u32 = 1; const weight_subclass: u32 = 2; const weight_prog_if: u32 = 4; const weight_vendor: u32 = 8; const weight_subsystem: u32 = 16; const weight_device: u32 = 32; const weight_hid: u32 = 32; /// The outcome of `matchDriver`: the winning rule's driver path, its specificity, /// and whether another rule tied it at that specificity. `ambiguous` is a /// registry authoring error (two equally-specific rules claiming one device); the /// manager logs it loudly and binds the first, so a shadowed rule is visible /// rather than silently dropped. pub const Match = struct { driver: []const u8, specificity: u32, ambiguous: bool, }; /// Whether `rule` matches `id`: same bus, and every pinned (non-wildcard) field /// equal. `hid` compares as a string; the rest as integers. fn matches(rule: Rule, id: Identity) bool { if (rule.bus != id.bus) return false; if (rule.base) |b| if (b != id.base) return false; if (rule.subclass) |s| if (s != id.subclass) return false; if (rule.prog_if) |p| if (p != id.prog_if) return false; if (rule.vendor) |v| if (v != id.vendor) return false; if (rule.device) |d| if (d != id.device) return false; if (rule.subsystem) |s| if (s != id.subsystem) return false; if (rule.hid) |h| if (!std.mem.eql(u8, h, id.hid)) return false; return true; } /// The specificity score of a rule — the sum of the weights of its pinned fields. fn specificity(rule: Rule) u32 { var score: u32 = 0; if (rule.base != null) score += weight_base; if (rule.subclass != null) score += weight_subclass; if (rule.prog_if != null) score += weight_prog_if; if (rule.vendor != null) score += weight_vendor; if (rule.device != null) score += weight_device; if (rule.subsystem != null) score += weight_subsystem; if (rule.hid != null) score += weight_hid; return score; } /// Bind a reported device to a driver: of every rule that matches `id`, return /// the most specific. `null` when nothing matches (the device goes unbound — /// the authoritative registry does not guess). On an exact specificity tie the /// first such rule in file order wins and `ambiguous` is set. pub fn matchDriver(rules: []const Rule, id: Identity) ?Match { var best: ?Match = null; for (rules) |rule| { if (!matches(rule, id)) continue; const score = specificity(rule); if (best) |current| { if (score > current.specificity) { best = .{ .driver = rule.driver, .specificity = score, .ambiguous = false }; } else if (score == current.specificity) { // Two equally-specific rules claim this device — keep the first, // flag the ambiguity for the manager to log. best.?.ambiguous = true; } } else { best = .{ .driver = rule.driver, .specificity = score, .ambiguous = false }; } } return best; } // --- parsing ----------------------------------------------------------------- /// What one CSV line parsed to. `malformed` is a non-comment, non-blank line the /// parser could not read (wrong field count, unparsable number, empty driver) — /// the manager counts these and logs, so a broken registry is loud, not silent. const Line = union(enum) { rule: Rule, ignorable, // blank or comment malformed, }; /// The result of `parse`: how many rules landed in the caller's buffer, and how /// many non-ignorable lines were malformed (for the manager to log). `truncated` /// is set if there were more valid rules than the buffer could hold. pub const ParseResult = struct { count: usize, malformed: usize, truncated: bool, }; /// Parse one hex field into `T`, honouring `*`/empty as a wildcard (`null`) and /// an optional `0x` prefix. Returns an error only for a genuinely unparsable /// non-wildcard token, so the caller can mark the whole line malformed. fn parseHexField(comptime T: type, field: []const u8) !?T { const token = std.mem.trim(u8, field, " \t"); if (token.len == 0 or std.mem.eql(u8, token, "*")) return null; const digits = if (std.mem.startsWith(u8, token, "0x") or std.mem.startsWith(u8, token, "0X")) token[2..] else token; return try std.fmt.parseInt(T, digits, 16); } /// Parse a wildcard-or-string field (the `hid` column): `*`/empty → wildcard. fn parseStringField(field: []const u8) ?[]const u8 { const token = std.mem.trim(u8, field, " \t"); if (token.len == 0 or std.mem.eql(u8, token, "*")) return null; return token; } /// Classify and (if a rule) parse one line. Split out from `parse` so it can be /// unit-tested directly. `line` is the raw line including no newline. fn parseLine(line: []const u8) Line { const body = csv.stripComment(line); if (body.len == 0) return .ignorable; // Nine comma-separated fields (csv.fields trims each): bus, base, class, // prog_if, vendor, device, subsystem, hid, driver. var cols: [9][]const u8 = undefined; var count: usize = 0; var it = csv.fields(body); while (it.next()) |field| { if (count >= cols.len) return .malformed; // too many columns cols[count] = field; count += 1; } if (count != cols.len) return .malformed; // too few columns const bus = Bus.fromToken(cols[0]); if (bus == .unknown) return .malformed; const driver = cols[8]; if (driver.len == 0) return .malformed; return .{ .rule = .{ .bus = bus, .base = parseHexField(u8, cols[1]) catch return .malformed, .subclass = parseHexField(u8, cols[2]) catch return .malformed, .prog_if = parseHexField(u8, cols[3]) catch return .malformed, .vendor = parseHexField(u16, cols[4]) catch return .malformed, .device = parseHexField(u16, cols[5]) catch return .malformed, .subsystem = parseHexField(u32, cols[6]) catch return .malformed, .hid = parseStringField(cols[7]), .driver = driver, } }; } /// Parse a whole `/system/configuration/devices.csv` into `out_rules`. The string fields of the /// returned rules point into `source`, which must outlive them. pub fn parse(source: []const u8, out_rules: []Rule) ParseResult { var result: ParseResult = .{ .count = 0, .malformed = 0, .truncated = false }; var lines = std.mem.splitScalar(u8, source, '\n'); while (lines.next()) |line| { switch (parseLine(line)) { .ignorable => {}, .malformed => result.malformed += 1, .rule => |rule| { if (result.count >= out_rules.len) { result.truncated = true; continue; } out_rules[result.count] = rule; result.count += 1; }, } } return result; } // --- tests ------------------------------------------------------------------- const testing = std.testing; // The worked example from the design: a specific virtio-gpu rule (pins vendor + // device) and a generic display rule (class only) both match the virtio card; // the specific one must win. And a plain VGA adapter still falls to the generic // rule. This is the whole point of widening the ABI to carry vendor/device. test "virtio device rule beats the generic display rule" { const text = \\# bus, base, class, prog_if, vendor, device, subsystem, hid, driver \\pci, 03, 00, 00, *, *, *, *, /system/drivers/display \\pci, 03, 80, *, 1AF4, 1050, *, *, /system/drivers/virtio-gpu ; var rules: [8]Rule = undefined; const parsed = parse(text, &rules); try testing.expectEqual(@as(usize, 2), parsed.count); try testing.expectEqual(@as(usize, 0), parsed.malformed); // The virtio-gpu function: display / other, vendor 1AF4 device 1050. const virtio = matchDriver(rules[0..parsed.count], .{ .bus = .pci, .base = 0x03, .subclass = 0x80, .prog_if = 0x00, .vendor = 0x1AF4, .device = 0x1050, }).?; try testing.expect(!virtio.ambiguous); try testing.expectEqualStrings("/system/drivers/virtio-gpu", virtio.driver); // A plain VGA adapter (display / VGA) still binds the generic display driver. const vga = matchDriver(rules[0..parsed.count], .{ .bus = .pci, .base = 0x03, .subclass = 0x00, .prog_if = 0x00, .vendor = 0x1234, .device = 0x1111, }).?; try testing.expectEqualStrings("/system/drivers/display", vga.driver); } test "no matching row leaves the device unbound" { const text = "pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus\n"; var rules: [8]Rule = undefined; const parsed = parse(text, &rules); try testing.expectEqual(@as(usize, 1), parsed.count); // An AHCI controller (mass storage / SATA / AHCI) has no row — unbound. const unmatched = matchDriver(rules[0..parsed.count], .{ .bus = .pci, .base = 0x01, .subclass = 0x06, .prog_if = 0x01, }); try testing.expect(unmatched == null); } test "acpi rows match on hid" { const text = \\acpi, *, *, *, *, *, *, PNP0303, /system/drivers/ps2-bus \\acpi, *, *, *, *, *, *, PNP0F13, /system/drivers/ps2-bus ; var rules: [8]Rule = undefined; const parsed = parse(text, &rules); try testing.expectEqual(@as(usize, 2), parsed.count); const keyboard = matchDriver(rules[0..parsed.count], .{ .bus = .acpi, .hid = "PNP0303" }).?; try testing.expectEqualStrings("/system/drivers/ps2-bus", keyboard.driver); const nothing = matchDriver(rules[0..parsed.count], .{ .bus = .acpi, .hid = "PNP0A03" }); try testing.expect(nothing == null); } test "equally specific rules flag ambiguity" { const text = \\pci, 03, 00, 00, *, *, *, *, /system/drivers/display-a \\pci, 03, 00, 00, *, *, *, *, /system/drivers/display-b ; var rules: [8]Rule = undefined; const parsed = parse(text, &rules); const hit = matchDriver(rules[0..parsed.count], .{ .bus = .pci, .base = 0x03, .subclass = 0x00, .prog_if = 0x00, }).?; try testing.expect(hit.ambiguous); try testing.expectEqualStrings("/system/drivers/display-a", hit.driver); // first wins } test "comments, blanks, and malformed lines" { const text = \\# a header comment \\ \\pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus # trailing comment \\pci, ZZ, 03, 30, *, *, *, *, /system/drivers/broken \\pci, 03, 00, 00, *, *, *, *, \\bogus-bus, *, *, *, *, *, *, *, /system/drivers/x ; var rules: [8]Rule = undefined; const parsed = parse(text, &rules); try testing.expectEqual(@as(usize, 1), parsed.count); // only the xhci row is valid try testing.expectEqual(@as(usize, 3), parsed.malformed); // bad hex, empty driver, bad bus try testing.expectEqualStrings("/system/drivers/usb-xhci-bus", rules[0].driver); try testing.expect(rules[0].hid == null); // trailing comment stripped, hid still wildcard }