344 lines
14 KiB
Zig
344 lines
14 KiB
Zig
//! 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
|
|
}
|