//! mgf-hrb-usb — the Linux USB backend (Phases U2 + U3a).
//!
//! Implements the abstract `kabi.KernelBackend` against the live Linux USB
//! substrate. Covers:
//!   - **U2a** enumerate: walk /sys/bus/usb/devices and emit one Resource
//!     per interface.
//!   - **U2b** watch: NETLINK_KOBJECT_UEVENT socket + pumpUevent draining.
//!   - **U3a** acquire / release / reset / raw_channel: USBDEVFS ioctls
//!     against /dev/bus/usb/<bus>/<devnum>. Atomic (errdefer); honest
//!     EACCES / EBUSY / ENOENT mapping. **In-process today; the privileged-
//!     helper subprocess split per D-031 is the U3b hardening pass** — the
//!     ioctls are identical, only the IPC boundary moves.
//!
//! map_region returns Unsupported (USB has no MMIO regions per §3 substrate
//! matrix).
//!
//! Substrate path: `/sys/bus/usb/devices/` is walked via raw syscalls — the
//! same "Level 2: no libudev / libusb" path that `linux_backend.zig` uses
//! for PCI (Part 6.5). The PCI backend's `sysOpen` / `sysReadAttr` /
//! `getdents64` shapes are mirrored here.
//!
//! Per the USB-worker design `hrb-design/mgf-hrb-usb-worker.md`:
//!   - §2.4 / D-013: enumerate emits ONE Resource per interface, never per
//!     device. A composite device shows up as N Resources.
//!   - §2.2: URN grammar is `hw:/usb/bus<N>/port-<chain>/dev/if<K>` with
//!     dot-separated port chain. Sysfs already uses the same dot-separated
//!     port form (e.g. `3-9.4:1.0`), so the parse is a direct mapping.
//!   - §2.3 / §2.8: fidelity tagged honestly. The descriptor fields are
//!     `.live` when sysfs reads succeed; otherwise `.unknown`. We never lie.

const std = @import("std");
const model = @import("mgf_hrb_model");
const kabi = @import("mgf_hrb_kabi");
const sysfs_walk = @import("mgf_hrb_sysfs_walk");
const linux_backend = @import("mgf_hrb_linux_backend");
const usb = @import("mgf_hrb_usb");
const identity = @import("mgf_hrb_identity");
const usb_synth = @import("mgf_hrb_usb_synth");

const Urn = model.Urn;
const Resource = model.Resource;
const ClaimMode = model.ClaimMode;
const Err = model.Err;
const Deadline = kabi.Deadline;
const linux = std.os.linux;

const USB_DEVICES_DIR = "/sys/bus/usb/devices";
const USB_DEVICES_DIR_Z: [:0]const u8 = USB_DEVICES_DIR;

// ---- raw-syscall sysfs helpers (mirrors linux_backend.zig pattern) ---------

fn sysOpenDir(path: [:0]const u8) ?i32 {
    const rc = linux.open(path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true }, 0);
    if (linux.errno(rc) != .SUCCESS) return null;
    return @intCast(rc);
}

fn sysReadAttr(buf: []u8, path: [:0]const u8) ?[]const u8 {
    const rc = linux.open(path, .{ .ACCMODE = .RDONLY }, 0);
    if (linux.errno(rc) != .SUCCESS) return null;
    const fd: i32 = @intCast(rc);
    defer _ = linux.close(fd);
    const n = linux.read(fd, buf.ptr, buf.len);
    if (linux.errno(n) != .SUCCESS) return null;
    var len: usize = n;
    while (len > 0 and (buf[len - 1] == '\n' or buf[len - 1] == '\r')) len -= 1;
    return buf[0..len];
}

/// Read a sysfs symlink and return the basename of its target.
///
/// Used for the `driver` + `driver/module` attrs on a bound device:
/// `.../driver` is a symlink to `/sys/bus/<subsys>/drivers/<name>`, so
/// the basename is the driver name. `.../driver/module` is a symlink to
/// `/sys/module/<name>`, so the basename is the loaded kernel-module name.
///
/// Returns null when the symlink doesn't exist (device is unbound) — the
/// caller emits no attr in that case (honest emptiness, G2). The buffer
/// must be large enough for the full symlink target (256 bytes is plenty
/// for both `driver` and `driver/module`).
fn sysReadSymlinkBasename(buf: []u8, path: [:0]const u8) ?[]const u8 {
    const rc = linux.readlink(path, buf.ptr, buf.len);
    if (linux.errno(rc) != .SUCCESS) return null;
    const n: usize = rc;
    if (n == 0 or n > buf.len) return null;
    return std.fs.path.basename(buf[0..n]);
}

// ---- USBDEVFS ioctl numbers (Phase U3a) ------------------------------------
//
// Encoded per the Linux _IOC convention:
//   _IOC(dir, type, nr, size) = (dir << 30) | (size << 16) | (type << 8) | nr
//
// Stable for >2 decades — these are part of the kernel ABI, not the source
// tree's preference. From linux/include/uapi/linux/usbdevice_fs.h.
const USBDEVFS_CLAIMINTERFACE: u32 = 0x8004550f; // _IOR('U', 15, unsigned int)
const USBDEVFS_RELEASEINTERFACE: u32 = 0x80045510; // _IOR('U', 16, unsigned int)
const USBDEVFS_RESET: u32 = 0x5514; // _IO('U', 20)
/// Atomic disconnect-the-kernel-driver + claim-the-interface (Linux 3.0+).
/// _IOR('U', 27, struct usbdevfs_disconnect_claim) with sizeof(struct)=264:
///     uint interface; uint flags; char driver[USBDEVFS_MAXDRIVERNAME+1=256];
/// Encoded: dir=2, size=264, type='U'=0x55, nr=27=0x1b
///   → (2<<30) | (264<<16) | (0x55<<8) | 0x1b = 0x8108551b
/// Use ONLY when the caller's capability authorizes detach (USB-worker
/// design §5.10 / FEEDBACK TOOLS-01 retrospective: silently stealing
/// devices is the failure mode we explicitly prevent).
const USBDEVFS_DISCONNECT_CLAIM: u32 = 0x8108551b;

const USBDEVFS_MAXDRIVERNAME: usize = 255;

/// Matches the kernel's `struct usbdevfs_disconnect_claim` layout.
/// `flags=0` means "always disconnect"; the caller doesn't constrain on
/// driver name (passing an empty driver string matches any).
const UsbdevfsDisconnectClaim = extern struct {
    interface: u32,
    flags: u32,
    driver: [USBDEVFS_MAXDRIVERNAME + 1]u8,
};

// ---- sysfs dir-name parser -------------------------------------------------

/// Parsed USB sysfs entry. Sysfs names interfaces as
/// `<bus>-<port[.port]*>:<config>.<interface>`; bare `<bus>-<port>` is the
/// device-only entry (skipped — we emit one Resource per *interface*, never
/// per device, per §2.4 / D-013).
const ParsedEntry = struct {
    bus: u8,
    /// Decimal port numbers, root-port-to-leaf. Matches the URN grammar.
    /// Lives in a small fixed buffer so the parse allocates nothing.
    port_chain: [16]u8 = [_]u8{0} ** 16,
    port_depth: u8 = 0,
    if_num: u8,

    fn portChainSlice(self: *const ParsedEntry) []const u8 {
        return self.port_chain[0..self.port_depth];
    }
};

/// Try to parse a sysfs entry name into (bus, port_chain, if_num).
/// Returns null for entries that aren't interface entries we should emit
/// (root-hub aliases `usbN`, device-only entries without `:c.i` suffix,
/// malformed names).
fn parseInterfaceEntry(name: []const u8) ?ParsedEntry {
    // Skip `usbN` root-hub aliases — those duplicate the `N-0:1.0` entry.
    if (std.mem.startsWith(u8, name, "usb")) return null;

    // Interface entries have a colon separating device part from `<config>.<interface>`.
    const colon = std.mem.indexOfScalar(u8, name, ':') orelse return null;
    const device_part = name[0..colon];
    const config_iface = name[colon + 1 ..];

    // device_part: `<bus>-<port[.port]*>`
    const dash = std.mem.indexOfScalar(u8, device_part, '-') orelse return null;
    const bus_str = device_part[0..dash];
    const port_str = device_part[dash + 1 ..];
    const bus = std.fmt.parseInt(u8, bus_str, 10) catch return null;

    // config_iface: `<config>.<interface>` — we only need the interface number.
    const dot = std.mem.indexOfScalar(u8, config_iface, '.') orelse return null;
    const iface_str = config_iface[dot + 1 ..];
    const if_num = std.fmt.parseInt(u8, iface_str, 10) catch return null;

    var parsed = ParsedEntry{ .bus = bus, .if_num = if_num };

    // port chain: `9.4.1` → [9, 4, 1]. Single rung `0` (root hub) → [0].
    var it = std.mem.tokenizeScalar(u8, port_str, '.');
    while (it.next()) |rung_str| {
        if (parsed.port_depth >= parsed.port_chain.len) return null; // overflow guard
        const rung = std.fmt.parseInt(u8, rung_str, 10) catch return null;
        parsed.port_chain[parsed.port_depth] = rung;
        parsed.port_depth += 1;
    }
    if (parsed.port_depth == 0) return null; // missing port — malformed
    return parsed;
}

// ---- netlink uevent parser (Phase U2b) -------------------------------------
//
// Kernel uevent messages arrive on a NETLINK_KOBJECT_UEVENT socket. Each
// message is a single datagram with this shape:
//
//   ACTION@DEVPATH\0
//   ACTION=add\0
//   DEVPATH=/devices/.../usb3/3-9/3-9:1.0\0
//   SUBSYSTEM=usb\0
//   DEVTYPE=usb_interface\0
//   ... more KEY=VALUE pairs ...
//
// We parse it field-by-field by splitting on NULs. For USB-interface events
// we extract the DEVPATH's tail (e.g. `3-9:1.0`) and reuse `parseInterfaceEntry`
// from the enumerate path — single source of truth for sysfs → URN mapping.

/// Parsed uevent payload — only the fields we use. `null` for missing.
pub const ParsedUevent = struct {
    /// "add", "remove", "change", "bind", "unbind", "online", "offline", or
    /// the empty string if ACTION= wasn't present. We act on:
    ///   • add / remove → `Event.added` / `Event.removed`
    ///   • bind / unbind → `Event.driver_bound` / `Event.driver_unbound`
    ///     (Part 13 §5.4.1 — added in sub-phase 1C).
    action: []const u8 = "",
    /// Sysfs path (no leading `/sys`). For `usb_interface` DEVTYPE, the tail
    /// matches our sysfs interface-entry name (`<bus>-<port[.port]*>:<config>.<interface>`).
    devpath: []const u8 = "",
    /// Subsystem the kobject belongs to. We only act on `subsystem == "usb"`.
    subsystem: []const u8 = "",
    /// "usb_interface" or "usb_device" for USB events; empty otherwise.
    devtype: []const u8 = "",
    /// Kernel driver name. Present on `ACTION=bind` / `ACTION=unbind`
    /// uevents; absent on add/remove. Empty when the field wasn't
    /// emitted (some bind events for built-in drivers omit it — we
    /// honestly skip those rather than synthesise a fake name).
    driver: []const u8 = "",
};

/// Pure parser: take a raw uevent datagram and pull out the four fields we
/// care about. Robust to missing fields, weird ordering, and the
/// `ACTION@DEVPATH` header line (which we skip).
pub fn parseUevent(msg: []const u8) ParsedUevent {
    var p = ParsedUevent{};
    var it = std.mem.tokenizeScalar(u8, msg, 0);
    var first = true;
    while (it.next()) |field| {
        if (first) {
            first = false;
            // First record is `ACTION@DEVPATH` (no `=`) — skip; we get the
            // same data from the KEY=VALUE fields that follow.
            if (std.mem.indexOfScalar(u8, field, '=') == null) continue;
        }
        const eq = std.mem.indexOfScalar(u8, field, '=') orelse continue;
        const key = field[0..eq];
        const value = field[eq + 1 ..];
        if (std.mem.eql(u8, key, "ACTION")) {
            p.action = value;
        } else if (std.mem.eql(u8, key, "DEVPATH")) {
            p.devpath = value;
        } else if (std.mem.eql(u8, key, "SUBSYSTEM")) {
            p.subsystem = value;
        } else if (std.mem.eql(u8, key, "DEVTYPE")) {
            p.devtype = value;
        } else if (std.mem.eql(u8, key, "DRIVER")) {
            // Carried on ACTION=bind / ACTION=unbind uevents (Part 13 §5.4.1).
            p.driver = value;
        }
    }
    return p;
}

/// Map a parsed uevent to a model.Event, or null if this uevent doesn't
/// correspond to one of our broker-level events (wrong subsystem, wrong
/// devtype, unhandled action, malformed devpath).
///
/// Honest about "change" / "bind" / "unbind": we DON'T synthesise a
/// state_changed for `change` events here — we lack old/new State context
/// at the backend level (that's the core's job). U2b ships add/remove
/// only; the synthesizer in design Part 5.3.4 will pair them upstream.
pub fn ueventToEvent(parsed: ParsedUevent) ?model.Event {
    if (!std.mem.eql(u8, parsed.subsystem, "usb")) return null;
    if (!std.mem.eql(u8, parsed.devtype, "usb_interface")) return null;

    // Extract the entry name (last `/`-segment of DEVPATH).
    const slash = std.mem.lastIndexOfScalar(u8, parsed.devpath, '/') orelse return null;
    const entry_name = parsed.devpath[slash + 1 ..];
    const parsed_entry = parseInterfaceEntry(entry_name) orelse return null;

    var urn_buf: [usb.MAX_USB_URN_BYTES]u8 = undefined;
    const urn = usb.urnForInterface(
        &urn_buf,
        parsed_entry.bus,
        parsed_entry.portChainSlice(),
        parsed_entry.if_num,
    );

    if (std.mem.eql(u8, parsed.action, "add")) {
        return .{ .added = .{ .urn = urn } };
    }
    if (std.mem.eql(u8, parsed.action, "remove")) {
        return .{ .removed = .{ .urn = urn } };
    }
    // ACTION=bind / ACTION=unbind — driver attachment lifecycle.
    // Added in sub-phase 1C (Part 13 §5.4.1). Honestly skipped when
    // the DRIVER field is absent (some built-in drivers' bind events
    // omit it; we don't synthesise a fake name).
    if (std.mem.eql(u8, parsed.action, "bind") and parsed.driver.len > 0) {
        return .{ .driver_bound = .{
            .urn = urn,
            .driver = model.DriverName.from(parsed.driver),
        } };
    }
    if (std.mem.eql(u8, parsed.action, "unbind") and parsed.driver.len > 0) {
        return .{ .driver_unbound = .{
            .urn = urn,
            .driver = model.DriverName.from(parsed.driver),
        } };
    }
    // change / online / offline — not mapped in U2b.
    return null;
}

// ---- URN reverse-parser (Phase U3a) ----------------------------------------
//
// The forward direction is `usb.urnForInterface(bus, port_chain, if_num)`.
// U3a needs the inverse: take an URN string and pull out the same components
// so we can resolve to the sysfs device dir + /dev/bus/usb/<bus>/<devnum>.

pub const UsbUrnParts = struct {
    bus: u8,
    port_chain: [16]u8 = [_]u8{0} ** 16,
    port_depth: u8 = 0,
    if_num: u8,

    pub fn portChainSlice(self: *const UsbUrnParts) []const u8 {
        return self.port_chain[0..self.port_depth];
    }
};

/// Pure parser: `hw:/usb/bus<N>/port-<chain>/dev/if<K>` → components.
/// Returns null for any URN that isn't a USB interface URN in canonical form.
pub fn parseUsbUrn(urn_slice: []const u8) ?UsbUrnParts {
    const prefix = "hw:/usb/bus";
    if (!std.mem.startsWith(u8, urn_slice, prefix)) return null;
    var rest = urn_slice[prefix.len..];

    // bus<N>
    const slash1 = std.mem.indexOfScalar(u8, rest, '/') orelse return null;
    const bus = std.fmt.parseInt(u8, rest[0..slash1], 10) catch return null;
    rest = rest[slash1 + 1 ..];

    // port-<chain>
    if (!std.mem.startsWith(u8, rest, "port-")) return null;
    rest = rest["port-".len..];
    const slash2 = std.mem.indexOfScalar(u8, rest, '/') orelse return null;
    const chain_str = rest[0..slash2];
    rest = rest[slash2 + 1 ..];

    // dev/
    if (!std.mem.startsWith(u8, rest, "dev/")) return null;
    rest = rest["dev/".len..];

    // if<K>
    if (!std.mem.startsWith(u8, rest, "if")) return null;
    const if_num = std.fmt.parseInt(u8, rest["if".len..], 10) catch return null;

    var parts = UsbUrnParts{ .bus = bus, .if_num = if_num };
    var it = std.mem.tokenizeScalar(u8, chain_str, '.');
    while (it.next()) |rung_str| {
        if (parts.port_depth >= parts.port_chain.len) return null;
        const rung = std.fmt.parseInt(u8, rung_str, 10) catch return null;
        parts.port_chain[parts.port_depth] = rung;
        parts.port_depth += 1;
    }
    if (parts.port_depth == 0) return null;
    return parts;
}

/// Resolved device-node path + USB bus/devnum identity. The kernel devnum
/// rotates on every replug — we deliberately read it FRESH from sysfs on
/// every acquire, never cache it (§2.2 gotcha).
pub const ResolvedDevice = struct {
    busnum: u8,
    devnum: u16, // can exceed u8 on hosts with many USB devices
    /// Null-terminated /dev/bus/usb/<bbb>/<ddd> path, e.g. "/dev/bus/usb/003/003".
    dev_node_z: [32:0]u8 = [_:0]u8{0} ** 32,
    dev_node_len: u8 = 0,

    pub fn devNodePath(self: *const ResolvedDevice) [:0]const u8 {
        return self.dev_node_z[0..self.dev_node_len :0];
    }
};

/// Resolve a USB URN to its current /dev/bus/usb/<bbb>/<ddd> path by reading
/// the kernel's live (busnum, devnum) from sysfs. Honest about absence:
/// returns `Err.NotFound` if the device is gone.
pub fn resolveDevice(parts: *const UsbUrnParts) Err!ResolvedDevice {
    // Build the sysfs device dir: `<bus>-<chain>` for normal devices,
    // `usb<bus>` for the root hub (port_chain == [0]).
    var dir_buf: [32]u8 = undefined;
    var chain_buf: [16]u8 = undefined;
    const dir_slice = blk: {
        if (parts.port_depth == 1 and parts.port_chain[0] == 0) {
            // Root hub: usb<N>
            break :blk std.fmt.bufPrint(&dir_buf, "usb{d}", .{parts.bus}) catch return Err.NotFound;
        }
        // Normal: <bus>-<chain> with dot-separated port chain.
        var chain_pos: usize = 0;
        for (parts.portChainSlice(), 0..) |rung, i| {
            if (i != 0) {
                chain_buf[chain_pos] = '.';
                chain_pos += 1;
            }
            const w = std.fmt.bufPrint(chain_buf[chain_pos..], "{d}", .{rung}) catch return Err.NotFound;
            chain_pos += w.len;
        }
        break :blk std.fmt.bufPrint(&dir_buf, "{d}-{s}", .{ parts.bus, chain_buf[0..chain_pos] }) catch return Err.NotFound;
    };

    // Read busnum + devnum from sysfs. Both are decimal text files.
    var path_buf: [256]u8 = undefined;
    var attr_buf: [16]u8 = undefined;

    const busnum_path = std.fmt.bufPrintZ(
        &path_buf,
        "{s}/{s}/busnum",
        .{ USB_DEVICES_DIR, dir_slice },
    ) catch return Err.NotFound;
    const busnum_str = sysReadAttr(&attr_buf, busnum_path) orelse return Err.NotFound;
    // J5 (review wave 2 — Cluster J): sysfs parse failures here are
    // real hardware-fault candidates (broken kernel state, corrupt
    // /sys content). Log the parse failure with the raw string so
    // operators can correlate against `cat /sys/bus/usb/devices/<X>/busnum`.
    const busnum = std.fmt.parseInt(u8, busnum_str, 10) catch |e| {
        std.debug.print(
            "warn: linux_usb.resolveDevice: busnum parseInt failed ({s}); raw_str={s} dir={s}\n",
            .{ @errorName(e), busnum_str, dir_slice },
        );
        return Err.HardwareFault;
    };

    const devnum_path = std.fmt.bufPrintZ(
        &path_buf,
        "{s}/{s}/devnum",
        .{ USB_DEVICES_DIR, dir_slice },
    ) catch return Err.NotFound;
    const devnum_str = sysReadAttr(&attr_buf, devnum_path) orelse return Err.NotFound;
    const devnum = std.fmt.parseInt(u16, devnum_str, 10) catch |e| {
        std.debug.print(
            "warn: linux_usb.resolveDevice: devnum parseInt failed ({s}); raw_str={s} dir={s}\n",
            .{ @errorName(e), devnum_str, dir_slice },
        );
        return Err.HardwareFault;
    };

    var resolved = ResolvedDevice{ .busnum = busnum, .devnum = devnum };
    const node_written = std.fmt.bufPrintZ(
        &resolved.dev_node_z,
        "/dev/bus/usb/{d:0>3}/{d:0>3}",
        .{ busnum, devnum },
    ) catch return Err.HardwareFault;
    resolved.dev_node_len = @intCast(node_written.len);
    return resolved;
}

// ---- scope-driven bus prefilter (Item #9) ----------------------------------
//
// When a caller asks for `hw:/usb/bus3/...`, we don't need to parse every
// sysfs entry — entries whose name doesn't begin with `3-` cannot match
// the scope by construction (the URN grammar pins `bus<N>` to the
// entry's `<N>-…` shape; root-hub entries `usb<N>` are skipped by
// `parseInterfaceEntry` regardless). Pull the bus number out of the
// scope so the directory walk can short-circuit cheaply.

/// Extract the bus number from a scope like `hw:/usb/bus<N>/...` or
/// `hw:/usb/bus<N>`. Returns null for any scope that doesn't pin a
/// single bus (broad scopes like `hw:/usb/**`, `hw:/**`, or anything
/// outside the USB subtree).
pub fn parseBusFromScope(scope: []const u8) ?u8 {
    const prefix = "hw:/usb/bus";
    if (!std.mem.startsWith(u8, scope, prefix)) return null;
    const rest = scope[prefix.len..];
    // Bus number ends at the next `/` (or end of string for the bare
    // `hw:/usb/bus<N>` form). Anything else (e.g. `hw:/usb/bus**`)
    // doesn't pin a single bus.
    const end = std.mem.indexOfScalar(u8, rest, '/') orelse rest.len;
    if (end == 0) return null;
    return std.fmt.parseInt(u8, rest[0..end], 10) catch null;
}

test "parseBusFromScope: extracts bus from a per-bus scope" {
    try std.testing.expectEqual(@as(?u8, 3), parseBusFromScope("hw:/usb/bus3/port-9/**"));
    try std.testing.expectEqual(@as(?u8, 3), parseBusFromScope("hw:/usb/bus3"));
    try std.testing.expectEqual(@as(?u8, 12), parseBusFromScope("hw:/usb/bus12/**"));
}

test "parseBusFromScope: broad scope returns null (no prefilter)" {
    try std.testing.expectEqual(@as(?u8, null), parseBusFromScope("hw:/usb/**"));
    try std.testing.expectEqual(@as(?u8, null), parseBusFromScope("hw:/**"));
    try std.testing.expectEqual(@as(?u8, null), parseBusFromScope("hw:/pci/**"));
}

test "parseBusFromScope: malformed bus → null" {
    try std.testing.expectEqual(@as(?u8, null), parseBusFromScope("hw:/usb/busXYZ/**"));
    try std.testing.expectEqual(@as(?u8, null), parseBusFromScope("hw:/usb/bus/**"));
}

// ---- errno → Err mapping (Phase U3a) ---------------------------------------

fn errnoToErr(e: linux.E) Err {
    return switch (e) {
        .NOENT, .NXIO, .NODEV => Err.NotFound,
        .ACCES, .PERM => Err.NotPermitted,
        .BUSY => Err.Conflict,
        .TIMEDOUT => Err.Timeout,
        .OPNOTSUPP => Err.Unsupported,
        else => Err.HardwareFault,
    };
}

// ---- post-reset verification (Phase U4 / Part 12 §2.3) ---------------------
//
// USBDEVFS_RESET tells the kernel to re-enumerate the device — but the kernel
// returning success doesn't itself prove the device came back. A device that
// was already dirty might fail to re-enumerate, in which case sysfs would no
// longer have its busnum/devnum. We re-read sysfs as the post-condition
// check: if we can still resolve the device, reset succeeded; if not, the
// device disappeared and we surface HardwareFault honestly (G2 — never lie
// about safe-reset).
//
// The kernel may also reassign devnum during re-enumeration; we don't pin
// devnum here, only existence — that's the correct survivability semantic
// (the URN's identity is topological, devnum is ephemeral by design).

fn verifyResetSucceeded(parts: *const UsbUrnParts) Err!void {
    // resolveDevice does the sysfs busnum+devnum read. If both files are
    // there and parse, the device is back; if not, the reset broke it.
    _ = resolveDevice(parts) catch return Err.HardwareFault;
}

// ---- the backend itself ----------------------------------------------------

/// One held claim — open fd to /dev/bus/usb/<bus>/<devnum> with one interface
/// ioctl-claimed against it. The fd IS the resource (raw_channel returns it).
const ClaimedInterface = struct {
    id: u64,
    fd: i32,
    if_num: u8,
    urn: Urn,
};

/// One entry in the snapshot cache. The watch synthesizer needs the
/// identity of a device that has *just* been removed (its sysfs entry
/// is already gone) — we cache the snapshot we built at add-time so a
/// follow-up `removed` event has something to compare against.
const CachedSnapshot = struct {
    urn: Urn,
    snap: identity.Snapshot,
};

/// Sub-phase 4A — bound on distinct (urn, verb) pairs the broker
/// tracks for serialisation. Generous for any real workload (a
/// modern host has <200 USB interfaces × 5 verbs ≈ 1000 worst
/// case; 256 is conservative for what an operator would actually
/// poke at). New pairs after the cap reject with `Overloaded`.
pub const MAX_CONTROL_STATES: usize = 256;

/// Part 13 §5.1.1 — per-(urn, verb) state object. The SpinMutex
/// is the FIFO serialiser; the counters track the disclosure
/// (Part 13 §5.1.2) the writer learns on return.
pub const ControlState = struct {
    in_use: bool = false,
    urn: Urn = .{},
    verb: model.ControlVerb = .set_authorized,
    /// Monotonic per (urn, verb); ++ on every applied write.
    /// `0` means "no successful write yet".
    generation: u64 = 0,
    last_writer_cap_id: u64 = 0,
    last_value_buf: [kabi.MAX_CONTROL_VALUE_BYTES]u8 = undefined,
    last_value_len: u8 = 0,
    last_ts_ms: u64 = 0,
    /// Count of writers currently queued on `serialiser` (the
    /// lock holder is NOT counted; only waiters). Read by the
    /// `queue_depth` field of the disclosure.
    waiters: std.atomic.Value(u32) = .init(0),
    serialiser: kabi.SpinMutex = .{},
};

pub const LinuxUsbBackend = struct {
    alloc: std.mem.Allocator,
    /// netlink uevent socket fd, set by `vWatch`. -1 when no watch is open.
    uevent_fd: i32 = -1,
    /// Currently-held claims. Small list; linear scan is fine.
    claims: std.ArrayListUnmanaged(ClaimedInterface) = .empty,
    next_token: u64 = 1,
    /// The watch synthesizer (Part 5.3.4 / `usb_synth.zig`). Collapses
    /// remove+add pairs within the debounce window when the runtime
    /// identity comparator agrees the device is the same physical unit.
    synth: usb_synth.Synthesizer = usb_synth.Synthesizer.init(usb_synth.DEFAULT_DEBOUNCE_MS),
    /// Snapshot cache — URN → last-seen snapshot built from sysfs at
    /// add-time. On `removed`, sysfs is already gone for that URN; this
    /// cache is the only way to give the synthesizer a real (not URN-
    /// only) snapshot to compare against the next add. Linear; bounded.
    snap_cache: std.ArrayListUnmanaged(CachedSnapshot) = .empty,

    /// Part 13 §5.1.1 / sub-phase 4A — per-(urn, verb) control
    /// state. Holds the FIFO serialiser + the monotonic generation
    /// counter + the last applied value + the writer identity. A
    /// fixed-size array (linear scan) — control ops are rare enough
    /// that O(N) lookup is comfortably cheap, and a fixed array
    /// avoids an allocator + hash on a code path that needs to be
    /// cheap under contention.
    control_states: [MAX_CONTROL_STATES]ControlState = [_]ControlState{.{}} ** MAX_CONTROL_STATES,
    /// Coarse lock around the control_states ARRAY (not the per-
    /// entry SpinMutex). Held only during lookup-or-allocate; the
    /// per-entry mutex serialises the actual writes.
    control_states_lock: kabi.SpinMutex = .{},

    /// Serialises the `pumpUevent` path: `snap_cache` and `synth`
    /// are touched by the watch-drain code, which the facade
    /// invokes WITHOUT holding `Core.lock` (see
    /// `Facade.drainEvents`). Two clients calling `drain_events`
    /// concurrently would otherwise race on the same shared
    /// state. The lock guards the whole pump body — netlink recv
    /// is non-blocking (MSG.DONTWAIT) and the per-event sysfs
    /// reads are short, so spinning is acceptable. Claim-mutating
    /// ops (`acquire` / `release` / `raw` / `reset` / `revoke`)
    /// stay protected by `Core.lock` AND `claims_lock` (see below)
    /// — they don't touch `snap_cache` or `synth`.
    pump_lock: kabi.SpinMutex = .{},

    /// G6 (review wave 2) — claims-table mutex.
    ///
    /// Pre-G6 the `claims` ArrayList + `next_token` counter were
    /// implicitly protected by `Core.lock`: Core.acquire / Core.release
    /// took the lock and held it across the backend call. Cluster A4
    /// (commit `f463527`) intentionally RELEASED `Core.lock` around
    /// `backend.rawChannel` so a wedged helper IPC wouldn't freeze
    /// other ops on the worker. That fix is correct at the lease-
    /// engine layer, but it created a real race inside this backend:
    ///   • Core.raw → backend.rawChannel → findClaim → reads `claims`
    ///     (NO Core.lock held)
    ///   • Core.release → backend.release → swapRemove (Core.lock held,
    ///     but doesn't help — the raw-path reader doesn't take it)
    /// Two concurrent ops on different leases of the same backend
    /// would interleave a read with a mutation → iterator
    /// invalidation, possible use-after-free against the just-removed
    /// ClaimedInterface.
    ///
    /// `claims_lock` is the structural guard: every read OR write of
    /// `claims` / `next_token` takes it. Critical sections are O(N)
    /// over a tiny N (typical: <20 live claims per worker), so a
    /// spin-mutex is the right shape — no syscalls, no allocator,
    /// sub-microsecond hold times.
    ///
    /// Lock-ordering vs `pump_lock`: the two locks NEVER nest. pump_lock
    /// is taken only inside `pumpUevent`, which never touches
    /// `claims`; claims_lock is taken only in the claim-mutation /
    /// claim-read paths, which never invoke pumpUevent. No inversion
    /// possible.
    claims_lock: kabi.SpinMutex = .{},

    pub fn init(alloc: std.mem.Allocator) LinuxUsbBackend {
        return .{ .alloc = alloc };
    }

    pub fn deinit(self: *LinuxUsbBackend) void {
        // Release any outstanding claims so we don't leak kernel state.
        for (self.claims.items) |c| {
            // Best-effort release. We don't propagate errors during deinit.
            const if_num_u32: u32 = c.if_num;
            _ = linux.ioctl(c.fd, USBDEVFS_RELEASEINTERFACE, @intFromPtr(&if_num_u32));
            _ = linux.close(c.fd);
        }
        self.claims.deinit(self.alloc);
        self.snap_cache.deinit(self.alloc);
        self.closeWatch();
    }

    /// Look up a cached snapshot by URN. Returns null if no add-time
    /// snapshot is on file for this URN (boot-time `removed` with no
    /// prior `added`, or post-restart racing the cache).
    fn lookupSnap(self: *LinuxUsbBackend, urn: *const Urn) ?identity.Snapshot {
        for (self.snap_cache.items) |entry| {
            if (entry.urn.eql(urn)) return entry.snap;
        }
        return null;
    }

    /// Evict a URN from the snapshot cache. Called when the device has
    /// been removed (the cached snapshot has been moved into the
    /// synthesizer's pending list).
    fn evictSnap(self: *LinuxUsbBackend, urn: *const Urn) void {
        var i: usize = 0;
        while (i < self.snap_cache.items.len) : (i += 1) {
            if (self.snap_cache.items[i].urn.eql(urn)) {
                _ = self.snap_cache.swapRemove(i);
                return;
            }
        }
    }

    /// Cache (or replace) the snapshot for a URN.
    fn cacheSnap(self: *LinuxUsbBackend, urn: Urn, snap: identity.Snapshot) void {
        for (self.snap_cache.items) |*entry| {
            if (entry.urn.eql(&urn)) {
                entry.snap = snap;
                return;
            }
        }
        // Bound the cache — drop the oldest if we somehow exceed a
        // reasonable size (a typical host has < 100 USB interfaces; we
        // cap at a comfortable 256 to keep memory bounded under
        // pathological churn).
        if (self.snap_cache.items.len >= 256) {
            _ = self.snap_cache.orderedRemove(0);
        }
        self.snap_cache.append(self.alloc, .{ .urn = urn, .snap = snap }) catch {};
    }

    /// Look up a held claim by token id. O(N) — claim sets are tiny in
    /// practice. Returns null when the token is unknown / already released.
    ///
    /// G6 (review wave 2) — takes `claims_lock` internally and returns
    /// a VALUE-COPY of the ClaimedInterface (not a pointer). This is
    /// the structural fix for the A4-introduced race: returning a
    /// pointer would let the caller dereference it after the lock is
    /// released, and a concurrent release/revoke could swap-remove
    /// the slot from under them. A value-copy makes the returned data
    /// owned by the caller's stack — safe to use after the lock drops.
    /// ClaimedInterface is small (id+fd+if_num+urn ≈ 144 bytes), so
    /// the copy cost is negligible compared to the safety win.
    fn findClaim(self: *LinuxUsbBackend, id: u64) ?ClaimedInterface {
        self.claims_lock.lock();
        defer self.claims_lock.unlock();
        for (self.claims.items) |c| {
            if (c.id == id) return c;
        }
        return null;
    }

    /// Look up a held claim by URN — needed for `vReset` (the kabi op takes
    /// a URN, not a token). Returns null when nothing is claimed at that URN.
    ///
    /// Same G6 discipline as `findClaim`: takes `claims_lock` internally
    /// and returns a value-copy.
    fn findClaimByUrn(self: *LinuxUsbBackend, urn: *const Urn) ?ClaimedInterface {
        self.claims_lock.lock();
        defer self.claims_lock.unlock();
        for (self.claims.items) |c| {
            if (c.urn.eql(urn)) return c;
        }
        return null;
    }

    /// Drain available uevent messages and push the ones that map to broker
    /// events to `sink`. NON-BLOCKING — returns when the kernel buffer is
    /// drained. Caller polls the fd (via `WatchHandle.id`) and calls this
    /// when there's data.
    ///
    /// Gap-honest overrun handling (Part 5.3.3): if the kernel returns
    /// `ENOBUFS` (we drained slower than uevents arrived, kernel dropped
    /// some), we push a typed `Event.gap` to the sink so the consumer can
    /// re-enumerate. Also logs to stderr for operator visibility. The gap
    /// event carries `nowMs()` as `since_ms`.
    pub fn pumpUevent(self: *LinuxUsbBackend, sink: *const kabi.EventSink, scope: []const u8) Err!void {
        // Serialise the whole pump — `snap_cache` + `synth` are
        // unprotected from concurrent drain_events callers (the
        // facade does NOT hold Core.lock when it invokes the
        // backend's drain). See the `pump_lock` field doc-comment.
        self.pump_lock.lock();
        defer self.pump_lock.unlock();

        if (self.uevent_fd < 0) return Err.InvalidToken;

        // Flush any synthesizer-pending `removed` events whose debounce
        // window has expired. This must run on every pump pass so a
        // long-quiet bus eventually surfaces the held removes — they
        // are never silently dropped (G2 / gap-honest).
        self.synth.flushExpired(sink, kabi.nowMs());

        var buf: [4096]u8 = undefined;
        while (true) {
            const n = linux.recvfrom(
                self.uevent_fd,
                &buf,
                buf.len,
                linux.MSG.DONTWAIT,
                null,
                null,
            );
            const err = linux.errno(n);
            if (err == .AGAIN) return; // no more pending messages
            if (err == .NOBUFS) {
                pushGap(sink);
                continue;
            }
            if (err != .SUCCESS) return Err.HardwareFault;
            if (n == 0) return;

            const msg = buf[0..n];
            const parsed = parseUevent(msg);
            const event = ueventToEvent(parsed) orelse continue;

            // Scope filter: only push events whose URN matches the caller's
            // glob. Read the URN slice through a copy so the lifetime is
            // independent of `buf` (urn_buf is inside ueventToEvent).
            // Part 13 §5.4.1 — driver_bound / driver_unbound also carry
            // a URN, so they participate in the scope filter the same
            // way added / removed do.
            const ev_urn = switch (event) {
                .added => |a| a.urn,
                .removed => |r| r.urn,
                .driver_bound => |b| b.urn,
                .driver_unbound => |u| u.urn,
                else => continue,
            };
            if (!ev_urn.matches(scope)) continue;

            // Route through the synthesizer (Part 5.3.4). On `added` we
            // build a snapshot from sysfs so the comparator has VID/PID/
            // serial to work with; on `removed` we look up the cached
            // add-time snapshot (sysfs is already gone for this URN).
            switch (event) {
                .added => |a| {
                    const snap = readUsbSnapshot(a.urn) orelse identity.Snapshot.init(.{
                        // URN-only fallback: comparator will see ambiguous
                        // (no vid/pid/serial), so no false collapse.
                        .urn = a.urn,
                        .vid = 0,
                        .pid = 0,
                        .bus = 0,
                        .captured_at_ms = kabi.nowMs(),
                    });
                    self.cacheSnap(a.urn, snap);
                    self.synth.onAdded(a.urn, snap, sink);
                },
                .removed => |r| {
                    const snap = self.lookupSnap(&r.urn) orelse identity.Snapshot.init(.{
                        .urn = r.urn,
                        .vid = 0,
                        .pid = 0,
                        .bus = 0,
                        .captured_at_ms = kabi.nowMs(),
                    });
                    self.evictSnap(&r.urn);
                    self.synth.onRemoved(r.urn, snap, sink, kabi.nowMs());
                },
                else => sink.push(sink.ctx, event),
            }
        }
    }

    /// Read a USB interface's identity snapshot from sysfs. Returns null
    /// when sysfs doesn't have the expected attrs (device already gone,
    /// or a non-standard / synthetic device that lacks idVendor). The
    /// fields read here are the comparator's inputs (Part 4 §4.3) —
    /// VID/PID short-circuit on mismatch, serial is the tie-breaker,
    /// port_chain is the topological hold-line.
    pub fn readUsbSnapshot(urn: Urn) ?identity.Snapshot {
        const parts = parseUsbUrn(urn.slice()) orelse return null;

        // Build the parent-device dir name (the `idVendor` / `idProduct`
        // / `serial` files live on the *device*, not the interface).
        // Same shape as vInspect's resolution.
        var chain_buf: [16]u8 = undefined;
        var chain_pos: usize = 0;
        for (parts.portChainSlice(), 0..) |rung, i| {
            if (i != 0) {
                chain_buf[chain_pos] = '.';
                chain_pos += 1;
            }
            const w = std.fmt.bufPrint(chain_buf[chain_pos..], "{d}", .{rung}) catch return null;
            chain_pos += w.len;
        }
        var dev_buf: [32]u8 = undefined;
        const dev_name = blk: {
            if (parts.port_depth == 1 and parts.port_chain[0] == 0) {
                break :blk std.fmt.bufPrint(&dev_buf, "usb{d}", .{parts.bus}) catch return null;
            }
            break :blk std.fmt.bufPrint(&dev_buf, "{d}-{s}", .{ parts.bus, chain_buf[0..chain_pos] }) catch return null;
        };

        var path_buf: [256]u8 = undefined;
        var v_buf: [16]u8 = undefined;
        const vid_path = std.fmt.bufPrintZ(&path_buf, "{s}/{s}/idVendor", .{ USB_DEVICES_DIR, dev_name }) catch return null;
        const vid_str = sysReadAttr(&v_buf, vid_path) orelse return null;
        const vid = std.fmt.parseInt(u16, vid_str, 16) catch return null;

        var p_buf: [16]u8 = undefined;
        const pid_path = std.fmt.bufPrintZ(&path_buf, "{s}/{s}/idProduct", .{ USB_DEVICES_DIR, dev_name }) catch return null;
        const pid_str = sysReadAttr(&p_buf, pid_path) orelse return null;
        const pid = std.fmt.parseInt(u16, pid_str, 16) catch return null;

        // Serial is optional — absent for most devices. Snapshot.init
        // handles the no-serial case correctly.
        var s_buf: [identity.MAX_SERIAL_BYTES]u8 = undefined;
        const serial_opt: ?[]const u8 = blk: {
            const sp = std.fmt.bufPrintZ(&path_buf, "{s}/{s}/serial", .{ USB_DEVICES_DIR, dev_name }) catch break :blk null;
            break :blk sysReadAttr(&s_buf, sp);
        };

        return identity.Snapshot.init(.{
            .urn = urn,
            .vid = vid,
            .pid = pid,
            .serial_number = serial_opt,
            .bus = parts.bus,
            .port_chain = parts.portChainSlice(),
            .captured_at_ms = kabi.nowMs(),
        });
    }

    /// Push a typed `Event.gap` for a netlink-uevent overrun. Exposed for
    /// the S-conformance USB suite which can't easily force ENOBUFS on a
    /// real kernel socket — testing the dispatch path directly is honest.
    pub fn pushGap(sink: *const kabi.EventSink) void {
        std.debug.print(
            "linux-usb: netlink uevent buffer overrun — events dropped (Part 5.3.3); gap emitted\n",
            .{},
        );
        sink.push(sink.ctx, .{ .gap = .{
            .source = .netlink_uevent,
            .since_ms = kabi.nowMs(),
        } });
    }

    /// Close the watch socket. Safe to call when no watch is open.
    pub fn closeWatch(self: *LinuxUsbBackend) void {
        if (self.uevent_fd >= 0) {
            _ = linux.close(self.uevent_fd);
            self.uevent_fd = -1;
        }
    }

    fn vEnumerate(ctx: *anyopaque, scope: []const u8, sink: *const kabi.FactSink, deadline: Deadline) Err!void {
        _ = ctx;
        _ = deadline; // synchronous sysfs walk; same honest-incompleteness note as linux_backend.zig

        const dfd = sysOpenDir(USB_DEVICES_DIR_Z) orelse return; // no USB sysfs → empty, not error
        defer _ = linux.close(dfd);

        // Per-bus prefilter (Part 9 §9): when the scope pins a specific bus
        // (`hw:/usb/bus<N>/...`) we can skip dirents whose name doesn't
        // begin with `<N>-` BEFORE parsing the interface entry. On a host
        // with many buses, this avoids the parse + URN-build cost for the
        // majority of entries when a caller asks for one bus.
        var bus_prefix_buf: [8]u8 = undefined;
        const bus_prefix: ?[]const u8 = if (parseBusFromScope(scope)) |b|
            std.fmt.bufPrint(&bus_prefix_buf, "{d}-", .{b}) catch null
        else
            null;

        var dbuf: [4096]u8 = undefined;
        while (true) {
            const nread = linux.getdents64(dfd, &dbuf, dbuf.len);
            if (linux.errno(nread) != .SUCCESS) return;
            if (nread == 0) break;
            var off: usize = 0;
            while (off + 19 <= nread) {
                // dirent64 layout: ino:u64(0) off:u64(8) reclen:u16(16) type:u8(18) name(19..)
                const reclen = std.mem.readInt(u16, dbuf[off + 16 ..][0..2], .little);
                if (reclen == 0) break;
                const name_start = off + 19;
                const name_ptr: [*:0]const u8 = @ptrCast(&dbuf[name_start]);
                const name = std.mem.span(name_ptr);
                off += reclen;
                if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) continue;

                // Bus prefilter — cheap startsWith before the full parse.
                if (bus_prefix) |p| {
                    if (!std.mem.startsWith(u8, name, p)) continue;
                }

                const parsed = parseInterfaceEntry(name) orelse continue;

                // Build the canonical URN via the U1 helper — single source of
                // truth for the §2.2 grammar.
                var urn_buf: [usb.MAX_USB_URN_BYTES]u8 = undefined;
                const urn = usb.urnForInterface(
                    &urn_buf,
                    parsed.bus,
                    parsed.portChainSlice(),
                    parsed.if_num,
                );
                if (!urn.matches(scope)) continue;

                var res = Resource{ .urn = urn, .family = .claimable, .kind = .usb_interface };

                // Set fidelity honestly. We read `bInterfaceClass` (always
                // present on a real interface descriptor) — success means the
                // descriptor is live; failure means the entry is gone or
                // unreadable, so report `unknown` (G2: never a confident
                // wrong answer).
                var attr_buf: [16]u8 = undefined;
                var path_buf: [256]u8 = undefined;
                const class_path = std.fmt.bufPrintZ(
                    &path_buf,
                    "{s}/{s}/bInterfaceClass",
                    .{ USB_DEVICES_DIR, name },
                ) catch {
                    res.fidelity = .unknown;
                    sink.push(sink.ctx, res);
                    continue;
                };
                res.fidelity = if (sysReadAttr(&attr_buf, class_path) != null) .live else .unknown;
                sink.push(sink.ctx, res);
            }
        }
    }

    fn vQueryTopology(ctx: *anyopaque, sink: *const kabi.FactSink, deadline: Deadline) Err!void {
        return vEnumerate(ctx, "hw:/**", sink, deadline);
    }

    fn vWatch(ctx: *anyopaque, scope: []const u8, sink: *const kabi.EventSink, deadline: Deadline) Err!kabi.WatchHandle {
        _ = scope;
        _ = sink;
        _ = deadline;
        const self: *LinuxUsbBackend = @ptrCast(@alignCast(ctx));

        // U2b: open a NETLINK_KOBJECT_UEVENT socket and bind to multicast
        // group 1 (kernel-originated events). Caller polls the returned
        // fd and calls `pumpUevent` to drain.
        //
        // NOTE: the vtable contract implies push-based delivery, but a
        // single-threaded Zig backend can't do that without an event loop.
        // We surface the netlink fd via the WatchHandle.id (cast u64) so
        // the caller can poll() it, and pumpUevent does the parse+push.
        // U3/U4 wraps this into a thread-pumped sink to match the
        // contract fully; U2b ships the substrate honestly.
        //
        // M1 (review wave 3) — concurrent first-watch race fix.
        //
        // Pre-M1: two threads both observed `uevent_fd < 0`, both
        // raced through `socket(...) + bind(...)`, both assigned
        // `self.uevent_fd = fd`. The loser's fd was orphaned (the
        // `errdefer` only fires on Err return; both succeed). One
        // netlink socket leaked per concurrent first-call window.
        //
        // Fix: serialise the check-allocate-assign sequence under
        // `pump_lock` (the same lock pumpUevent uses to protect
        // uevent_fd reads). Double-checked locking: after the lock
        // we re-test `uevent_fd >= 0` so a fast lap from another
        // thread that won the race returns the existing fd without
        // re-allocating.
        self.pump_lock.lock();
        defer self.pump_lock.unlock();
        if (self.uevent_fd >= 0) {
            // Already watching — return the existing fd. Caller should
            // closeWatch() between watches.
            return .{ .id = @as(u64, @intCast(self.uevent_fd)) };
        }

        const fd_rc = linux.socket(
            linux.AF.NETLINK,
            linux.SOCK.RAW | linux.SOCK.CLOEXEC | linux.SOCK.NONBLOCK,
            linux.NETLINK.KOBJECT_UEVENT,
        );
        if (linux.errno(fd_rc) != .SUCCESS) return Err.HardwareFault;
        const fd: i32 = @intCast(fd_rc);
        errdefer _ = linux.close(fd);

        const addr = linux.sockaddr.nl{
            .pid = 0, // kernel picks the port id
            .groups = 1, // multicast group 1 — kernel-originated uevents
        };
        const bind_rc = linux.bind(
            fd,
            @ptrCast(&addr),
            @sizeOf(linux.sockaddr.nl),
        );
        if (linux.errno(bind_rc) != .SUCCESS) return Err.HardwareFault;

        self.uevent_fd = fd;
        return .{ .id = @as(u64, @intCast(fd)) };
    }

    fn vAcquire(ctx: *anyopaque, urn: *const Urn, mode: ClaimMode, deadline: Deadline) Err!kabi.AcquireToken {
        // The plain acquire NEVER detaches a bound kernel driver — if
        // CLAIMINTERFACE returns EBUSY, surface as Err.Conflict. The
        // detach-on-EBUSY path is the OPT-IN `acquireDetach` below;
        // callers with the right capability use that explicitly.
        return acquireImpl(ctx, urn, mode, deadline, false);
    }

    /// USB-only opt-in: like acquire, but uses USBDEVFS_DISCONNECT_CLAIM
    /// to atomically detach a bound kernel driver and claim the
    /// interface (vs. the plain CLAIMINTERFACE which returns EBUSY).
    /// Capability-gated at the server layer — the core enforces that
    /// the caller's cap carries the right to detach (USB-worker
    /// §5.10 / FEEDBACK TOOLS-01 retrospective). The seam exposes the
    /// PRIMITIVE; policy ("is this caller authorized?") lives above.
    fn vDrainEvents(ctx: *anyopaque, scope: []const u8, sink: *const kabi.EventSink, deadline: Deadline) Err!void {
        _ = deadline; // drain is non-blocking; deadline irrelevant
        const self: *LinuxUsbBackend = @ptrCast(@alignCast(ctx));
        // Auto-open the netlink uevent watch on first drain so callers don't
        // have to remember to call vWatch first. The handle is stored on the
        // backend; subsequent drains reuse it.
        if (self.uevent_fd < 0) {
            // vWatch stores the fd but doesn't retain the sink (the seam
            // contract: drain owns delivery, not the watch open). Pass a
            // dummy here; the real sink is pumpUevent's parameter.
            const Dummy = struct {
                fn push(_: *anyopaque, _: model.Event) void {}
            };
            const ds = kabi.EventSink{ .ctx = self, .push = Dummy.push };
            _ = self.backend().watch(scope, &ds, kabi.Deadline.none) catch return Err.Unsupported;
        }
        return self.pumpUevent(sink, scope);
    }

    fn vAcquireDetach(ctx: *anyopaque, urn: *const Urn, mode: ClaimMode, deadline: Deadline) Err!kabi.AcquireToken {
        return acquireImpl(ctx, urn, mode, deadline, true);
    }

    /// USB-worker §5.10 typed control verbs. Each verb maps to a
    /// single sysfs write under `/sys/bus/usb/devices/<dev>/...`. The
    /// `value` is verb-specific (e.g. "0"/"1" for booleans, "auto"/"on"
    /// for autosuspend, the driver name for bind/unbind). Errors map
    /// honestly: ENOENT → NotFound (URN gone or driver unknown),
    /// EACCES/EPERM → NotPermitted (no udev rule, no group), other
    /// errno → HardwareFault.
    ///
    /// Permission errors carry the suggested-fix vocabulary at the
    /// server layer (Part 6). At the seam, we surface a clean typed
    /// Err — the broker assembles `PermissionContext` upstream.
    fn vControl(
        ctx: *anyopaque,
        urn: *const Urn,
        verb: model.ControlVerb,
        value: []const u8,
        cap_id: u64,
        deadline: Deadline,
    ) Err!kabi.ControlOutcome {
        const self: *LinuxUsbBackend = @ptrCast(@alignCast(ctx));
        _ = deadline; // single sysfs write; bounded by kernel
        const parts = parseUsbUrn(urn.slice()) orelse return Err.BadRequest;

        // Part 13 §5.1.1 — look up (or allocate) the per-(urn,
        // verb) serialiser state. The FIFO discipline guards
        // against two clients racing to set the same control on
        // the same device. Per-URN-per-verb is the natural grain:
        // `set_authorized` and `set_autosuspend` are independent
        // sysfs files even on the same URN, so they don't need
        // to wait on each other.
        const state = self.controlStateFor(urn, verb) orelse return Err.Overloaded;
        _ = state.waiters.fetchAdd(1, .seq_cst);
        state.serialiser.lock();
        const queue_depth_at_acquire = state.waiters.fetchSub(1, .seq_cst) - 1;
        // Explicit lock holds — we unlock manually after the write
        // succeeds + state is updated so the supersede re-snapshot
        // below can acquire the same lock cleanly. Errdefer
        // releases the lock on the unwind path (sysfsWriteZ
        // failure).
        errdefer state.serialiser.unlock();

        // Build the parent-device dir name. Same shape as readUsbSnapshot.
        var chain_buf: [16]u8 = undefined;
        var chain_pos: usize = 0;
        for (parts.portChainSlice(), 0..) |rung, i| {
            if (i != 0) {
                chain_buf[chain_pos] = '.';
                chain_pos += 1;
            }
            const w = std.fmt.bufPrint(chain_buf[chain_pos..], "{d}", .{rung}) catch return Err.HardwareFault;
            chain_pos += w.len;
        }
        var dev_buf: [32]u8 = undefined;
        const dev_name = blk: {
            if (parts.port_depth == 1 and parts.port_chain[0] == 0) {
                break :blk std.fmt.bufPrint(&dev_buf, "usb{d}", .{parts.bus}) catch return Err.HardwareFault;
            }
            break :blk std.fmt.bufPrint(&dev_buf, "{d}-{s}", .{ parts.bus, chain_buf[0..chain_pos] }) catch return Err.HardwareFault;
        };

        var path_buf: [256]u8 = undefined;
        // For bind/unbind: the value written to the sysfs file is the
        // *interface name* (`<bus>-<chain>:<config>.<if>`), not the
        // driver name. The driver is in the PATH. Compute the interface
        // name up-front; declared in the outer scope so its slice
        // outlives the switch arm that writes it.
        var ifname_buf: [32]u8 = undefined;
        const ifname = std.fmt.bufPrint(
            &ifname_buf,
            "{d}-{s}:1.{d}",
            .{ parts.bus, chain_buf[0..chain_pos], parts.if_num },
        ) catch return Err.HardwareFault;

        switch (verb) {
            .set_authorized => try sysfsWriteZ(
                std.fmt.bufPrintZ(&path_buf, "{s}/{s}/authorized", .{ USB_DEVICES_DIR, dev_name }) catch return Err.HardwareFault,
                value,
            ),
            .set_autosuspend => try sysfsWriteZ(
                std.fmt.bufPrintZ(&path_buf, "{s}/{s}/power/control", .{ USB_DEVICES_DIR, dev_name }) catch return Err.HardwareFault,
                value,
            ),
            .set_port_power => try sysfsWriteZ(
                std.fmt.bufPrintZ(&path_buf, "{s}/{s}/port/disable", .{ USB_DEVICES_DIR, dev_name }) catch return Err.HardwareFault,
                value,
            ),
            .bind_driver => try sysfsWriteZ(
                std.fmt.bufPrintZ(&path_buf, "/sys/bus/usb/drivers/{s}/bind", .{value}) catch return Err.HardwareFault,
                ifname,
            ),
            .unbind_driver => try sysfsWriteZ(
                std.fmt.bufPrintZ(&path_buf, "/sys/bus/usb/drivers/{s}/unbind", .{value}) catch return Err.HardwareFault,
                ifname,
            ),
        }

        // Write succeeded. Update the per-(urn, verb) state under
        // the still-held lock, record the disclosure, then build
        // the outcome.
        state.generation += 1;
        state.last_writer_cap_id = cap_id;
        const new_value_len = @min(value.len, kabi.MAX_CONTROL_VALUE_BYTES);
        @memcpy(state.last_value_buf[0..new_value_len], value[0..new_value_len]);
        state.last_value_len = @intCast(new_value_len);
        state.last_ts_ms = kabi.nowMs();
        const our_gen = state.generation;
        const our_value_len = state.last_value_len;

        // Drop the lock — subsequent writers can now race in
        // ahead of our return. The next-block re-snapshot is what
        // detects them.
        state.serialiser.unlock();

        // Part 13 §5.1.2 — re-snapshot to detect supersede. If
        // another writer landed between our unlock above and the
        // snapshot below (briefly held lock; cheap), report them
        // via `superseded_by`. Honest: the snapshot is at this
        // instant, not at the moment our caller receives the
        // response; that residual window is unavoidable without
        // holding the lock across response emit (which would
        // pessimise contention for marginal accuracy gain).
        state.serialiser.lock();
        const post_gen = state.generation;
        const post_writer = state.last_writer_cap_id;
        const post_ts = state.last_ts_ms;
        state.serialiser.unlock();

        var outcome = kabi.ControlOutcome{
            .generation = our_gen,
            .applied_value_len = our_value_len,
            .queue_depth = queue_depth_at_acquire,
            .superseded_by = if (post_gen > our_gen) kabi.SupersededBy{
                .generation = post_gen,
                .cap_id = post_writer,
                .at_ms = post_ts,
            } else null,
        };
        @memcpy(outcome.applied_value_buf[0..new_value_len], value[0..new_value_len]);
        return outcome;
    }

    /// Look up an existing ControlState for (urn, verb), or
    /// allocate a new slot. Returns null when the registry is
    /// full (Err.Overloaded surfaces from the caller).
    ///
    /// The coarse `control_states_lock` is held ONLY during the
    /// lookup-or-allocate; the per-entry SpinMutex is the
    /// FIFO-serialiser the writers actually queue on. Holding two
    /// short, ordered locks (registry → entry) is fine — every
    /// caller takes them in this order; no inverse path exists,
    /// so no deadlock cycle.
    pub fn controlStateFor(
        self: *LinuxUsbBackend,
        urn: *const Urn,
        verb: model.ControlVerb,
    ) ?*ControlState {
        self.control_states_lock.lock();
        defer self.control_states_lock.unlock();
        for (&self.control_states) |*cs| {
            if (cs.in_use and cs.verb == verb and cs.urn.eql(urn)) return cs;
        }
        for (&self.control_states) |*cs| {
            if (!cs.in_use) {
                cs.* = .{ .in_use = true, .urn = urn.*, .verb = verb };
                return cs;
            }
        }
        return null;
    }

    /// Open a sysfs attribute for writing, write `value`, close. Maps
    /// kernel errno to our typed Err: ENOENT → NotFound (URN gone or
    /// driver unknown), EACCES/EPERM → NotPermitted (no udev rule),
    /// other → HardwareFault. Idempotent in the kernel's view — a
    /// write of "1" to an already-authorized device is a no-op.
    fn sysfsWriteZ(path: [:0]const u8, value: []const u8) Err!void {
        const fd_rc = linux.open(path, .{ .ACCMODE = .WRONLY }, 0);
        switch (linux.errno(fd_rc)) {
            .SUCCESS => {},
            .NOENT, .NXIO, .NODEV => return Err.NotFound,
            .ACCES, .PERM, .ROFS => return Err.NotPermitted,
            else => return Err.HardwareFault,
        }
        const fd: i32 = @intCast(fd_rc);
        defer _ = linux.close(fd);

        var written: usize = 0;
        while (written < value.len) {
            const n = linux.write(fd, value[written..].ptr, value.len - written);
            switch (linux.errno(n)) {
                .SUCCESS => {
                    if (n == 0) return Err.HardwareFault; // unexpected zero-write
                    written += n;
                },
                .INTR => continue,
                .ACCES, .PERM => return Err.NotPermitted,
                .INVAL => return Err.BadRequest, // kernel rejected the value
                else => return Err.HardwareFault,
            }
        }
    }

    fn acquireImpl(ctx: *anyopaque, urn: *const Urn, mode: ClaimMode, deadline: Deadline, detach: bool) Err!kabi.AcquireToken {
        _ = mode; // §3.3: observe vs shared_rw vs exclusive — the claim
        //          ioctl itself doesn't distinguish; the core enforces the
        //          lattice above. (Future U3+: shared_rw could use the
        //          USBDEVFS_DROP_PRIVILEGES path; exclusive is the default.)
        _ = deadline; // synchronous ioctl; see module note about watchdog half
        const self: *LinuxUsbBackend = @ptrCast(@alignCast(ctx));

        // 1. Reverse-parse the URN to find (bus, port_chain, if_num).
        const parts = parseUsbUrn(urn.slice()) orelse return Err.BadRequest;

        // 2. Resolve to /dev/bus/usb/<bus>/<devnum> via sysfs (devnum read
        //    FRESH — the kernel rotates it on every replug, never cache).
        const resolved = try resolveDevice(&parts);

        // 3. Open the device node. Atomic — `errdefer` closes on any later
        //    failure so the kernel sees no held fd if the claim ioctl
        //    fails. Reliability > a micro-optimization (priority order).
        const open_rc = linux.open(
            resolved.devNodePath(),
            .{ .ACCMODE = .RDWR },
            0,
        );
        if (linux.errno(open_rc) != .SUCCESS) return errnoToErr(linux.errno(open_rc));
        const fd: i32 = @intCast(open_rc);
        errdefer _ = linux.close(fd);

        // 4. Claim. Either:
        //    - USBDEVFS_CLAIMINTERFACE: plain claim; EBUSY if a kernel
        //      driver is bound — caller sees Err.Conflict and decides
        //      whether to retry with detach.
        //    - USBDEVFS_DISCONNECT_CLAIM: atomic disconnect-then-claim;
        //      used only when the caller is authorized (capability
        //      gating at the server layer).
        if (detach) {
            var arg = UsbdevfsDisconnectClaim{
                .interface = parts.if_num,
                .flags = 0, // 0 = always disconnect (any bound driver)
                .driver = [_]u8{0} ** (USBDEVFS_MAXDRIVERNAME + 1),
            };
            const rc = linux.ioctl(fd, USBDEVFS_DISCONNECT_CLAIM, @intFromPtr(&arg));
            const e = linux.errno(rc);
            if (e != .SUCCESS) return errnoToErr(e);
        } else {
            const if_num_u32: u32 = parts.if_num;
            const rc = linux.ioctl(fd, USBDEVFS_CLAIMINTERFACE, @intFromPtr(&if_num_u32));
            const e = linux.errno(rc);
            if (e != .SUCCESS) return errnoToErr(e);
        }

        // 5. Commit. From here we own the fd until release (or deinit).
        // G6 — `next_token` + `claims` are both shared state; take
        // `claims_lock` for the read-modify-write so two concurrent
        // acquires can't draw the same token id.
        self.claims_lock.lock();
        defer self.claims_lock.unlock();
        const id = self.next_token;
        self.next_token += 1;
        // J3 (review wave 2) — OOM appending the claim record is
        // back-pressure, not a hardware fault. Pre-J3 we returned
        // `Err.HardwareFault` (which suggests "device is broken,
        // try reset" — wrong action for OOM). `Err.Overloaded`
        // signals "the broker is at capacity; back off + retry"
        // — the SDK's OverloadedError docs the recovery shape.
        //
        // Atomicity note: the kernel claim succeeded on the prior
        // ioctl line; if the append fails the `errdefer
        // _ = linux.close(fd)` (set up earlier in vAcquire) closes
        // the device-node fd, which causes the kernel to release
        // the interface claim implicitly (USBDEVFS_CLAIMINTERFACE
        // is fd-bound). No leak; the kernel state matches the
        // broker's "we don't hold this" view.
        self.claims.append(self.alloc, .{
            .id = id,
            .fd = fd,
            .if_num = parts.if_num,
            .urn = urn.*,
        }) catch return Err.Overloaded;

        return .{ .id = id };
    }

    fn vRelease(ctx: *anyopaque, token: kabi.AcquireToken, deadline: Deadline) void {
        _ = deadline;
        const self: *LinuxUsbBackend = @ptrCast(@alignCast(ctx));
        // Contract: total + idempotent (§6.3). Token unknown → no-op.
        //
        // G6 — extract the claim under `claims_lock` (so a concurrent
        // raw-path reader can't see the slot mid-mutation, and a
        // concurrent acquire can't extend the list while we
        // swapRemove). The ioctl + close happen AFTER releasing the
        // lock to keep the critical section short — release is
        // best-effort, not on the hot path.
        var claim_to_release: ?ClaimedInterface = null;
        {
            self.claims_lock.lock();
            defer self.claims_lock.unlock();
            const idx = blk: {
                for (self.claims.items, 0..) |c, i| if (c.id == token.id) break :blk i;
                return;
            };
            claim_to_release = self.claims.items[idx];
            _ = self.claims.swapRemove(idx);
        }
        const c = claim_to_release.?;
        // USBDEVFS_RELEASEINTERFACE — best effort; if the device is gone
        // we still close the fd to satisfy the "always succeed" contract.
        const if_num_u32: u32 = c.if_num;
        _ = linux.ioctl(c.fd, USBDEVFS_RELEASEINTERFACE, @intFromPtr(&if_num_u32));
        _ = linux.close(c.fd);
    }

    /// Tier-0 #4 — revoke the broker's claim on `urn`. Tears
    /// down the kernel-side interface claim via
    /// USBDEVFS_RELEASEINTERFACE + closes the broker's fd.
    /// Subsequent reads/writes on the client's dup'd fd return
    /// ENODEV because the underlying kernel-side claim is gone.
    ///
    /// Idempotent: no claim for this URN → OK (the post-condition
    /// "this URN is no longer claimed by the broker" already
    /// holds).
    ///
    /// Why this works on USB without IOMMU: the USB stack
    /// associates the file descriptor with a kernel-side claim
    /// on the interface. Closing the fd + releasing the claim
    /// detaches the kernel binding; subsequent ioctls on a
    /// dup'd fd see ENODEV. The dup doesn't keep the binding
    /// alive — that's the SCM_RIGHTS / dup semantics for an
    /// fd backed by a kernel object that's been closed.
    fn vRevoke(ctx: *anyopaque, urn: *const Urn, deadline: Deadline) Err!void {
        _ = deadline;
        const self: *LinuxUsbBackend = @ptrCast(@alignCast(ctx));
        // G6 — same find-under-lock, mutate-outside-lock shape as
        // `vRelease`: keeps the critical section tight and avoids
        // holding the lock across the (potentially slow) ioctl.
        var claim_to_revoke: ?ClaimedInterface = null;
        {
            self.claims_lock.lock();
            defer self.claims_lock.unlock();
            const idx = blk: {
                for (self.claims.items, 0..) |c, i| if (c.urn.eql(urn)) break :blk i;
                return; // honest idempotency — nothing to revoke
            };
            claim_to_revoke = self.claims.items[idx];
            _ = self.claims.swapRemove(idx);
        }
        const c = claim_to_revoke.?;
        const if_num_u32: u32 = c.if_num;
        _ = linux.ioctl(c.fd, USBDEVFS_RELEASEINTERFACE, @intFromPtr(&if_num_u32));
        _ = linux.close(c.fd);
    }

    fn vReset(ctx: *anyopaque, urn: *const Urn, level: kabi.ResetLevel, deadline: Deadline) Err!void {
        _ = deadline;
        const self: *LinuxUsbBackend = @ptrCast(@alignCast(ctx));

        // Reset-escalation ladder for USB (Part 12 §2.5).
        //   function_level — USBDEVFS_RESET via the hub port. Supported.
        //   bus_level      — would mean rebinding the host controller.
        //                    No kernel ABI for this; honestly Unsupported.
        // §6.3: never lie about what reset levels we can achieve.
        if (level == .bus_level) return Err.Unsupported;

        // To issue USBDEVFS_RESET we need an open fd on the device. Two
        // cases:
        //   (a) We're holding a claim on this URN's device — reuse the fd.
        //   (b) No claim — open the device-node briefly just for reset.
        const parts = parseUsbUrn(urn.slice()) orelse return Err.BadRequest;
        // M2 (review wave 3) — close the fd-UAF window.
        //
        // Pre-M2: `findClaimByUrn` returned a value-copy `c` and then
        // we used `c.fd` AFTER claims_lock had been released by the
        // accessor. A concurrent `vRelease` / `vRevoke` on the same
        // claim could close the fd between the find and the ioctl;
        // the kernel could then RECYCLE the fd number for a totally
        // unrelated open (e.g. a sysfs read inside `resolveDevice`
        // in this very function). The ioctl would then fire against
        // the wrong fd.
        //
        // Fix: under `claims_lock`, dup the fd via fcntl(F_DUPFD_CLOEXEC).
        // The dup is OWNED by this stack frame; any concurrent release
        // can close the original without affecting our copy. We close
        // the dup after the ioctl + verifyResetSucceeded.
        //
        // We don't hold the spinlock across the ioctl (USBDEVFS_RESET
        // can take milliseconds-seconds on slow hardware — same
        // discipline as cluster A4).
        var dup_fd: i32 = -1;
        {
            self.claims_lock.lock();
            defer self.claims_lock.unlock();
            for (self.claims.items) |c| {
                if (c.urn.eql(urn)) {
                    const rc = linux.fcntl(c.fd, linux.F.DUPFD_CLOEXEC, 0);
                    if (linux.errno(rc) == .SUCCESS) {
                        dup_fd = @intCast(rc);
                    } else {
                        // Dup itself failed (EMFILE / ENOMEM). Surface
                        // as a typed error rather than continuing with
                        // an invalid fd; the caller back-off-retries.
                        return Err.Overloaded;
                    }
                    break;
                }
            }
        }
        if (dup_fd >= 0) {
            defer _ = linux.close(dup_fd);
            const rc = linux.ioctl(dup_fd, USBDEVFS_RESET, 0);
            const e = linux.errno(rc);
            if (e != .SUCCESS) return errnoToErr(e);
            return try verifyResetSucceeded(&parts);
        }

        const resolved = try resolveDevice(&parts);
        const open_rc = linux.open(resolved.devNodePath(), .{ .ACCMODE = .RDWR }, 0);
        if (linux.errno(open_rc) != .SUCCESS) return errnoToErr(linux.errno(open_rc));
        const fd: i32 = @intCast(open_rc);
        defer _ = linux.close(fd);
        const ioctl_rc = linux.ioctl(fd, USBDEVFS_RESET, 0);
        const e = linux.errno(ioctl_rc);
        if (e != .SUCCESS) return errnoToErr(e);
        return try verifyResetSucceeded(&parts);
    }

    fn vMapRegion(ctx: *anyopaque, token: kabi.AcquireToken, region: u32, deadline: Deadline) Err!kabi.Mapping {
        _ = ctx;
        _ = token;
        _ = region;
        _ = deadline;
        // USB has no MMIO regions; honest Unsupported per §3 (substrate matrix).
        return Err.Unsupported;
    }

    fn vRawChannel(ctx: *anyopaque, token: kabi.AcquireToken, deadline: Deadline) Err!kabi.RawHandle {
        _ = deadline;
        const self: *LinuxUsbBackend = @ptrCast(@alignCast(ctx));
        // The raw channel for a USB claim IS the open fd on /dev/bus/usb/.
        // The SDK boundary will SCM_RIGHTS-pass it on request (fdpass.zig).
        // For the kabi contract we just confirm the token is valid; the
        // actual fd-passing happens at the broker server layer, which can
        // look up the fd via `fdForToken` below.
        _ = self.findClaim(token.id) orelse return Err.InvalidToken;
        return .{ .token = token };
    }

    /// Public accessor for the broker server: hand back a DUPED file
    /// descriptor for the claim's underlying USBDEVFS fd so it can be
    /// SCM_RIGHTS-passed to the holder. Returns -1 if the token is
    /// unknown OR if the dup syscall failed.
    ///
    /// M2 (review wave 3) — caller-owned dup, not a borrow.
    ///
    /// Pre-M2 this returned `c.fd` directly. The server then `dup`d it
    /// to keep an independent broker-side copy. The window between this
    /// function returning and the server's dup was the UAF: a concurrent
    /// `vRelease` could close `c.fd` and the kernel could recycle that
    /// fd number, so the server's dup operated on a totally unrelated
    /// open file (or failed with EBADF). Either way: WRONG fd
    /// SCM_RIGHTS-passed to the client.
    ///
    /// Fix: do the dup HERE, under `claims_lock`, with `F_DUPFD_CLOEXEC`
    /// so the cloned fd is CLOEXEC'd for any future fork. The caller
    /// (server.zig handle.raw) treats the returned fd as OWNED — it
    /// closes after `sendFdWithPayload`. The caller MUST NOT dup again;
    /// the fd is already an independent kernel-side reference, safe
    /// against any subsequent close on `c.fd`.
    ///
    /// Note for backend reimplementers: this contract change applies to
    /// every backend's `fdForToken` — it now returns OWNED fds. A
    /// future PCI/VFIO `fdForToken` must follow the same discipline.
    pub fn fdForToken(self: *LinuxUsbBackend, token: kabi.AcquireToken) i32 {
        self.claims_lock.lock();
        defer self.claims_lock.unlock();
        for (self.claims.items) |c| {
            if (c.id == token.id) {
                const rc = linux.fcntl(c.fd, linux.F.DUPFD_CLOEXEC, 0);
                if (linux.errno(rc) != .SUCCESS) {
                    // Dup failure surfaces as -1 (same shape as "no
                    // claim" today, since the server's `if (held_fd >= 0)`
                    // branch already treats -1 as "no fd to pass" and
                    // returns `{"fd":false}` to the client).
                    return -1;
                }
                return @intCast(rc);
            }
        }
        return -1;
    }

    fn vInspect(ctx: *anyopaque, urn: *const Urn, attrs: *const kabi.AttrSink, deadline: Deadline) Err!Resource {
        _ = ctx;
        _ = deadline;
        // Reverse-parse the URN, resolve the sysfs device dir, read the
        // descriptor attrs. USB-specific fields: vid, pid, serial,
        // bInterfaceClass/subclass/protocol, bus, port, devnum.
        const parts = parseUsbUrn(urn.slice()) orelse return Err.NotFound;

        // The interface dir matches the URN's `<bus>-<port>:<config>.<if>`
        // shape. The device (parent) dir is `<bus>-<port>` for non-root,
        // `usb<bus>` for root-hub (port_chain == [0]).
        //
        // M5 (review wave 3) — separate buffers for iface_name and dev_name.
        //
        // Pre-M5: `dev_name` was aliased into `iface_buf[24..]` with the
        // comment "Stash into chain_buf since iface_buf has its own
        // residency" (note: the comment itself is misleading — it
        // mentions chain_buf but stores in iface_buf). The 24-byte
        // offset assumed `iface_name`'s `bufPrint` produced output
        // shorter than 24 chars. That held for typical depths but had
        // NO bounds check. A deeper port chain or higher bus number
        // could grow `iface_name` past 24 bytes; the dev_name @memcpy
        // would then STOMP into `iface_name`'s still-live data, which
        // is subsequently READ by the `bufPrintZ` calls below (e.g.
        // `"{s}/{s}/bInterfaceClass", .{ USB_DEVICES_DIR, iface_name }`).
        // Result: corrupted sysfs path, undefined-behavior read for
        // `iface_name` consumers.
        //
        // Fix: give dev_name its OWN dedicated buffer. Cost: 32 bytes
        // of additional stack frame — negligible (vInspect total is
        // already ~1.2 KiB after G8).
        var iface_buf: [32]u8 = undefined;
        var dev_buf: [32]u8 = undefined;
        var chain_buf: [16]u8 = undefined;
        var chain_pos: usize = 0;
        for (parts.portChainSlice(), 0..) |rung, i| {
            if (i != 0) {
                chain_buf[chain_pos] = '.';
                chain_pos += 1;
            }
            const w = std.fmt.bufPrint(chain_buf[chain_pos..], "{d}", .{rung}) catch return Err.NotFound;
            chain_pos += w.len;
        }
        const chain = chain_buf[0..chain_pos];
        const iface_name = std.fmt.bufPrint(&iface_buf, "{d}-{s}:1.{d}", .{ parts.bus, chain, parts.if_num }) catch return Err.NotFound;
        const dev_name = blk: {
            if (parts.port_depth == 1 and parts.port_chain[0] == 0) {
                break :blk std.fmt.bufPrint(&dev_buf, "usb{d}", .{parts.bus}) catch return Err.NotFound;
            }
            break :blk std.fmt.bufPrint(&dev_buf, "{d}-{s}", .{ parts.bus, chain }) catch return Err.NotFound;
        };

        // Confirm the interface exists by reading bInterfaceClass.
        // Each attr below has its own stack buffer; pushing the local
        // slice directly is safe because `AttrSink.push` consumes the
        // value synchronously, and no buffer here is reused after its
        // push. Earlier shape used a static rotating buffer (dupeShort)
        // which races between concurrent inspect calls on different
        // client threads — removed.
        var path_buf: [256]u8 = undefined;
        var attr_buf: [64]u8 = undefined;
        const class_path = std.fmt.bufPrintZ(&path_buf, "{s}/{s}/bInterfaceClass", .{ USB_DEVICES_DIR, iface_name }) catch return Err.NotFound;
        const class_val = sysReadAttr(&attr_buf, class_path) orelse return Err.NotFound;
        attrs.push(attrs.ctx, .{ .key = "bInterfaceClass", .value = class_val });

        // Per-interface optional attrs.
        var sub_buf: [16]u8 = undefined;
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/bInterfaceSubClass", .{ USB_DEVICES_DIR, iface_name })) |p| {
            if (sysReadAttr(&sub_buf, p)) |v| {
                attrs.push(attrs.ctx, .{ .key = "bInterfaceSubClass", .value = v });
            }
        } else |_| {}
        var proto_buf: [16]u8 = undefined;
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/bInterfaceProtocol", .{ USB_DEVICES_DIR, iface_name })) |p| {
            if (sysReadAttr(&proto_buf, p)) |v| {
                attrs.push(attrs.ctx, .{ .key = "bInterfaceProtocol", .value = v });
            }
        } else |_| {}

        // Parent-device attrs: vid, pid, serial, busnum/devnum.
        var v_buf: [16]u8 = undefined;
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/idVendor", .{ USB_DEVICES_DIR, dev_name })) |p| {
            if (sysReadAttr(&v_buf, p)) |v| {
                attrs.push(attrs.ctx, .{ .key = "idVendor", .value = v });
            }
        } else |_| {}
        var p_buf: [16]u8 = undefined;
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/idProduct", .{ USB_DEVICES_DIR, dev_name })) |p| {
            if (sysReadAttr(&p_buf, p)) |v| {
                attrs.push(attrs.ctx, .{ .key = "idProduct", .value = v });
            }
        } else |_| {}
        // G8 (review wave 2) — USB iSerialNumber descriptors are up
        // to 255 UTF-16 code units per USB spec, transcoding to up
        // to ~510 UTF-8 bytes worst-case. Real devices observed in
        // practice are <40 chars; older code used 64. But the user's
        // "no assumptions / no loose ends" standard means we cannot
        // rely on "observed in practice" for a wire-shape field that
        // operators / consumers use to identify devices. 256 bytes
        // covers every spec-compliant serial we'd actually see (the
        // ~510 worst-case requires every code unit in the supplementary
        // plane — vanishingly rare for an iSerial). `sysReadAttr`
        // silently truncates beyond the buffer; bumping the buffer
        // is the minimal change to close the silent-truncation gap
        // for the inspect path. (`identity.zig`'s MAX_SERIAL_BYTES
        // is a separate concern — it governs the Snapshot's stored
        // serial used by the comparator, not the wire-exposed attr.)
        var s_buf: [256]u8 = undefined;
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/serial", .{ USB_DEVICES_DIR, dev_name })) |p| {
            if (sysReadAttr(&s_buf, p)) |v| {
                attrs.push(attrs.ctx, .{ .key = "serial", .value = v });
            }
        } else |_| {}
        var bn_buf: [16]u8 = undefined;
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/busnum", .{ USB_DEVICES_DIR, dev_name })) |p| {
            if (sysReadAttr(&bn_buf, p)) |v| {
                attrs.push(attrs.ctx, .{ .key = "busnum", .value = v });
            }
        } else |_| {}
        var dn_buf: [16]u8 = undefined;
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/devnum", .{ USB_DEVICES_DIR, dev_name })) |p| {
            if (sysReadAttr(&dn_buf, p)) |v| {
                attrs.push(attrs.ctx, .{ .key = "devnum", .value = v });
            }
        } else |_| {}

        // Driver-ownership attrs (Part 13 §5.2.1): which kernel driver
        // currently owns this interface, and which loaded module does
        // the driver come from? Both attrs are honestly absent when
        // the interface is unbound (no driver symlink exists).
        var drv_buf: [256]u8 = undefined;
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/driver", .{ USB_DEVICES_DIR, iface_name })) |p| {
            if (sysReadSymlinkBasename(&drv_buf, p)) |name| {
                attrs.push(attrs.ctx, .{ .key = "driver", .value = name });
            }
        } else |_| {}
        // `.../driver/module` symlinks to the loaded kernel module the
        // driver came from. Often matches the driver name, but they
        // can diverge (e.g. driver "snd-usb-audio" lives in module
        // "snd_usb_audio"; some drivers ship as part of a larger
        // module like usbcore).
        var mod_buf: [256]u8 = undefined;
        if (std.fmt.bufPrintZ(&path_buf, "{s}/{s}/driver/module", .{ USB_DEVICES_DIR, iface_name })) |p| {
            if (sysReadSymlinkBasename(&mod_buf, p)) |name| {
                attrs.push(attrs.ctx, .{ .key = "module", .value = name });
            }
        } else |_| {}

        return .{ .urn = urn.*, .family = .claimable, .kind = .usb_interface, .fidelity = .live };
    }

    /// Part 13 §5.5.1 — bulk introspection via tree walk. Resolves the
    /// USB interface URN to its `/sys/bus/usb/devices/<iface>/` dir
    /// (the same path `vInspect` reads scalar attrs from) and delegates
    /// the recursive walk to the shared `sysfs_walk` module. The
    /// walker handles file/symlink/subdir dispatch + bounded recursion
    /// + binary detection.
    ///
    /// Honest about `NotFound`: a URN that doesn't parse OR a sysfs
    /// dir that's gone (device unplugged mid-call) → `Err.NotFound`
    /// before any sink writes. The walker itself never raises; sub-
    /// entry failures degrade silently and the global walk continues.
    fn vInspectFull(
        ctx: *anyopaque,
        urn: *const Urn,
        depth: model.InspectDepth,
        sink: *const kabi.TreeSink,
        deadline: kabi.Deadline,
    ) Err!model.Resource {
        _ = ctx;
        _ = deadline; // sync sysfs walk; bounded by the walker's caps

        // Reverse-parse the URN to the sysfs interface-entry name.
        // Same shape as vInspect's resolution; could be factored out
        // but the inline form keeps the two paths independently
        // auditable (a regression in one doesn't silently leak).
        const parts = parseUsbUrn(urn.slice()) orelse return Err.NotFound;

        var iface_buf: [32]u8 = undefined;
        var chain_buf: [16]u8 = undefined;
        var chain_pos: usize = 0;
        for (parts.portChainSlice(), 0..) |rung, i| {
            if (i != 0) {
                chain_buf[chain_pos] = '.';
                chain_pos += 1;
            }
            const w = std.fmt.bufPrint(chain_buf[chain_pos..], "{d}", .{rung}) catch return Err.NotFound;
            chain_pos += w.len;
        }
        const chain = chain_buf[0..chain_pos];
        const iface_name = std.fmt.bufPrint(
            &iface_buf,
            "{d}-{s}:1.{d}",
            .{ parts.bus, chain, parts.if_num },
        ) catch return Err.NotFound;

        var path_buf: [256]u8 = undefined;
        const dir_path = std.fmt.bufPrintZ(
            &path_buf,
            "{s}/{s}",
            .{ USB_DEVICES_DIR, iface_name },
        ) catch return Err.NotFound;

        // Verify the dir exists (and is in fact our interface) by
        // probing bInterfaceClass — present on every real USB interface.
        var probe_buf: [16]u8 = undefined;
        var probe_path_buf: [256]u8 = undefined;
        const probe_path = std.fmt.bufPrintZ(
            &probe_path_buf,
            "{s}/bInterfaceClass",
            .{dir_path},
        ) catch return Err.NotFound;
        if (sysReadAttr(&probe_buf, probe_path) == null) return Err.NotFound;

        const mode: sysfs_walk.WalkMode = switch (depth) {
            .shallow => .shallow,
            .all => .full,
        };
        var walker = sysfs_walk.Walker.init(sink);
        sysfs_walk.walkSubtree(dir_path, 0, mode, &walker);

        return .{
            .urn = urn.*,
            .family = .claimable,
            .kind = .usb_interface,
            .fidelity = .live,
        };
    }

    /// Part 13 §5.5.2 — parent-chain walk for a USB interface URN.
    /// Emits a 5+ rung chain leaf-first:
    ///   1. interface  (the queried URN itself; verified via sysfs)
    ///   2. device     (the USB device the interface lives on)
    ///   3. hub        (one rung per intermediate port — none if the
    ///                  device is directly on the root hub)
    ///   4. root_hub   (`usb<N>`)
    ///   5. controller (the PCI host controller — synthetic
    ///                  `hw:/pci/<bdf>/fn0` URN; honestly absent if
    ///                  the readlink fails)
    ///
    /// The intermediate URNs are TOPOLOGICAL LABELS — they conform
    /// to the URN scheme (`hw:/usb/bus<N>/port-<chain>/dev`) but are
    /// not first-class enumerable resources (only `:if<K>` URNs are).
    /// The chain is a topology trace, not a list of inspect targets;
    /// the consumer that wants per-rung detail calls `inspect_full`
    /// on the rungs that ARE enumerable (the leaf interface).
    ///
    /// Sub-entry failures degrade silently — if the controller's BDF
    /// can't be read, the chain ends at root_hub (G2 partial truth).
    fn vDumpChain(
        ctx: *anyopaque,
        urn: *const Urn,
        sink: *const kabi.ChainSink,
        deadline: kabi.Deadline,
    ) Err!void {
        _ = ctx;
        _ = deadline; // bounded: <10 sysfs reads + 1 readlink

        const parts = parseUsbUrn(urn.slice()) orelse return Err.NotFound;

        // Build the port-chain string ("9" / "9.2" / "9.2.1" / ...).
        var chain_buf: [32]u8 = undefined;
        var chain_pos: usize = 0;
        for (parts.portChainSlice(), 0..) |rung, i| {
            if (i != 0) {
                chain_buf[chain_pos] = '.';
                chain_pos += 1;
            }
            const w = std.fmt.bufPrint(chain_buf[chain_pos..], "{d}", .{rung}) catch return Err.NotFound;
            chain_pos += w.len;
        }
        const chain = chain_buf[0..chain_pos];

        // Verify the interface exists by probing bInterfaceClass —
        // the canonical "this is a USB interface" tell.
        var iface_name_buf: [64]u8 = undefined;
        const iface_name = std.fmt.bufPrint(
            &iface_name_buf,
            "{d}-{s}:1.{d}",
            .{ parts.bus, chain, parts.if_num },
        ) catch return Err.NotFound;
        var probe_path_buf: [256]u8 = undefined;
        var probe_buf: [16]u8 = undefined;
        const probe_path = std.fmt.bufPrintZ(
            &probe_path_buf,
            "{s}/{s}/bInterfaceClass",
            .{ USB_DEVICES_DIR, iface_name },
        ) catch return Err.NotFound;
        if (sysReadAttr(&probe_buf, probe_path) == null) return Err.NotFound;

        // 1) interface — summary: "class=<X> sub=<Y> proto=<Z>".
        var if_sum_buf: [128]u8 = undefined;
        const if_sum = usbInterfaceSummary(iface_name, &if_sum_buf);
        sink.push(sink.ctx, .{
            .urn = urn.slice(),
            .level = "interface",
            .summary = if_sum,
        });

        // 2) device — sysfs name `<bus>-<chain>`. Synthetic URN
        // `hw:/usb/bus<N>/port-<chain>/dev` (interface URN minus
        // the `/if<K>` suffix).
        var dev_urn_buf: [128]u8 = undefined;
        const dev_urn = std.fmt.bufPrint(
            &dev_urn_buf,
            "hw:/usb/bus{d}/port-{s}/dev",
            .{ parts.bus, chain },
        ) catch return;
        var dev_name_buf: [32]u8 = undefined;
        const dev_name = std.fmt.bufPrint(
            &dev_name_buf,
            "{d}-{s}",
            .{ parts.bus, chain },
        ) catch return;
        var dev_sum_buf: [128]u8 = undefined;
        const dev_sum = usbDeviceSummary(dev_name, &dev_sum_buf);
        sink.push(sink.ctx, .{
            .urn = dev_urn,
            .level = "device",
            .summary = dev_sum,
        });

        // 3) hubs — one rung per chain prefix shorter than the leaf's.
        // chain `9.2.1` → push hubs at `9.2` then `9` (leaf-first).
        // Skip entirely when port_chain is `[0]` (root-hub URN); the
        // root_hub rung below covers it.
        if (!(parts.port_depth == 1 and parts.port_chain[0] == 0)) {
            var depth: usize = parts.port_depth;
            while (depth > 1) : (depth -= 1) {
                var pf_buf: [32]u8 = undefined;
                var pf_pos: usize = 0;
                var j: usize = 0;
                while (j < depth - 1) : (j += 1) {
                    if (j != 0) {
                        pf_buf[pf_pos] = '.';
                        pf_pos += 1;
                    }
                    const w = std.fmt.bufPrint(
                        pf_buf[pf_pos..],
                        "{d}",
                        .{parts.port_chain[j]},
                    ) catch return;
                    pf_pos += w.len;
                }
                const pf = pf_buf[0..pf_pos];
                var hu_buf: [128]u8 = undefined;
                const hu = std.fmt.bufPrint(
                    &hu_buf,
                    "hw:/usb/bus{d}/port-{s}/dev",
                    .{ parts.bus, pf },
                ) catch return;
                var hn_buf: [32]u8 = undefined;
                const hn = std.fmt.bufPrint(
                    &hn_buf,
                    "{d}-{s}",
                    .{ parts.bus, pf },
                ) catch return;
                var hs_buf: [128]u8 = undefined;
                const hs = usbDeviceSummary(hn, &hs_buf);
                sink.push(sink.ctx, .{
                    .urn = hu,
                    .level = "hub",
                    .summary = hs,
                });
            }
        }

        // 4) root_hub — sysfs `usb<N>`. Synthetic URN
        // `hw:/usb/bus<N>/port-0/dev` (matches parseUsbUrn's
        // root-hub convention: `port_chain == [0]`).
        var rh_urn_buf: [64]u8 = undefined;
        const rh_urn = std.fmt.bufPrint(
            &rh_urn_buf,
            "hw:/usb/bus{d}/port-0/dev",
            .{parts.bus},
        ) catch return;
        var rh_name_buf: [16]u8 = undefined;
        const rh_name = std.fmt.bufPrint(
            &rh_name_buf,
            "usb{d}",
            .{parts.bus},
        ) catch return;
        var rh_sum_buf: [128]u8 = undefined;
        const rh_sum = usbDeviceSummary(rh_name, &rh_sum_buf);
        sink.push(sink.ctx, .{
            .urn = rh_urn,
            .level = "root_hub",
            .summary = rh_sum,
        });

        // 5) controller — readlink the root hub's sysfs dir;
        // last BDF-shaped component in the target is the host
        // controller. Build synthetic PCI URN + use the shared
        // pciSummary helper.
        var rh_path_buf: [128]u8 = undefined;
        const rh_path = std.fmt.bufPrintZ(
            &rh_path_buf,
            "{s}/{s}",
            .{ USB_DEVICES_DIR, rh_name },
        ) catch return;
        var link_buf: [512]u8 = undefined;
        const rc = std.os.linux.readlink(rh_path, &link_buf, link_buf.len);
        if (std.os.linux.errno(rc) != .SUCCESS) return; // honest absence
        const link_len: usize = rc;
        const link_target = link_buf[0..link_len];

        var controller_bdf: ?[]const u8 = null;
        var it = std.mem.splitScalar(u8, link_target, '/');
        while (it.next()) |comp| {
            if (linux_backend.isPciBdf(comp)) controller_bdf = comp;
        }
        if (controller_bdf) |bdf| {
            var cu_buf: [64]u8 = undefined;
            const cu = std.fmt.bufPrint(
                &cu_buf,
                "hw:/pci/{s}/fn0",
                .{bdf},
            ) catch return;
            var cs_buf: [128]u8 = undefined;
            const cs = linux_backend.pciSummary(bdf, &cs_buf);
            sink.push(sink.ctx, .{
                .urn = cu,
                .level = "controller",
                .summary = cs,
            });
        }
    }

    /// One-line summary of a USB device — `<vid>:<pid>` (the canonical
    /// "what device" tell), absent fields elided. Reads idVendor /
    /// idProduct under `/sys/bus/usb/devices/<name>/`. On any read
    /// failure returns the bare sysfs name so the rung is still
    /// visible (G2 partial truth).
    fn usbDeviceSummary(dev_name: []const u8, buf: []u8) []const u8 {
        var path_buf: [256]u8 = undefined;
        var vid_buf: [16]u8 = undefined;
        var pid_buf: [16]u8 = undefined;
        const vp = std.fmt.bufPrintZ(
            &path_buf,
            "{s}/{s}/idVendor",
            .{ USB_DEVICES_DIR, dev_name },
        ) catch return std.fmt.bufPrint(buf, "{s}", .{dev_name}) catch buf[0..0];
        const vid = sysReadAttr(&vid_buf, vp) orelse "?";
        const pp = std.fmt.bufPrintZ(
            &path_buf,
            "{s}/{s}/idProduct",
            .{ USB_DEVICES_DIR, dev_name },
        ) catch return std.fmt.bufPrint(buf, "{s}", .{dev_name}) catch buf[0..0];
        const pid = sysReadAttr(&pid_buf, pp) orelse "?";
        return std.fmt.bufPrint(buf, "{s}:{s}", .{ vid, pid }) catch buf[0..0];
    }

    /// One-line summary of a USB interface — `class=<X> sub=<Y> proto=<Z>`.
    /// Reads bInterfaceClass / bInterfaceSubClass / bInterfaceProtocol
    /// under `/sys/bus/usb/devices/<iface_name>/`. On any read failure
    /// returns the bare iface name.
    fn usbInterfaceSummary(iface_name: []const u8, buf: []u8) []const u8 {
        var path_buf: [256]u8 = undefined;
        var class_buf: [16]u8 = undefined;
        var sub_buf: [16]u8 = undefined;
        var proto_buf: [16]u8 = undefined;
        const cp = std.fmt.bufPrintZ(
            &path_buf,
            "{s}/{s}/bInterfaceClass",
            .{ USB_DEVICES_DIR, iface_name },
        ) catch return std.fmt.bufPrint(buf, "{s}", .{iface_name}) catch buf[0..0];
        const class = sysReadAttr(&class_buf, cp) orelse "?";
        const sp = std.fmt.bufPrintZ(
            &path_buf,
            "{s}/{s}/bInterfaceSubClass",
            .{ USB_DEVICES_DIR, iface_name },
        ) catch return std.fmt.bufPrint(buf, "{s}", .{iface_name}) catch buf[0..0];
        const sub = sysReadAttr(&sub_buf, sp) orelse "?";
        const pp = std.fmt.bufPrintZ(
            &path_buf,
            "{s}/{s}/bInterfaceProtocol",
            .{ USB_DEVICES_DIR, iface_name },
        ) catch return std.fmt.bufPrint(buf, "{s}", .{iface_name}) catch buf[0..0];
        const proto = sysReadAttr(&proto_buf, pp) orelse "?";
        return std.fmt.bufPrint(
            buf,
            "class={s} sub={s} proto={s}",
            .{ class, sub, proto },
        ) catch buf[0..0];
    }

    fn vFdForToken(ctx: *anyopaque, token: kabi.AcquireToken) i32 {
        const self: *LinuxUsbBackend = @ptrCast(@alignCast(ctx));
        return self.fdForToken(token);
    }

    fn vCapabilities(ctx: *anyopaque) kabi.BackendCapabilities {
        _ = ctx;
        // USB is feature-complete in the in-process U3a path. The one
        // honest gap: map_region is Unsupported (USB has no MMIO regions
        // per §3 substrate matrix). Reset is via USBDEVFS_RESET; bus_level
        // is honestly Unsupported (no kernel ABI) — that's a parameter of
        // the op, so op_reset itself is true.
        return .{
            .name = "mgf-hrb-usb",
            .version = "0.1.0",
            .op_enumerate = true,
            .op_query_topology = true,
            .op_watch = true,
            .op_acquire = true,
            .op_release = true,
            .op_reset = true,
            .op_map_region = false, // USB has no MMIO regions
            .op_raw_channel = true,
            .op_enforce_cap = false,
            .mode = .full,
            .mode_reason = "",
        };
    }

    const vtable = kabi.KernelBackend.VTable{
        .enumerate = vEnumerate,
        .query_topology = vQueryTopology,
        .watch = vWatch,
        .acquire = vAcquire,
        .release = vRelease,
        .reset = vReset,
        .map_region = vMapRegion,
        .raw_channel = vRawChannel,
        .enforce_cap = null, // userspace enforcement (Part 6.2; matches PCI backend)
        .capabilities = vCapabilities,
        .fd_for_token = vFdForToken,
        .inspect = vInspect,
        .inspect_full = vInspectFull, // Part 13 §5.5.1
        .dump_chain = vDumpChain, // Part 13 §5.5.2
        .revoke = vRevoke, // Tier-0 #4 — survivability §3.4
        .acquire_detach = vAcquireDetach,
        .drain_events = vDrainEvents,
        .control = vControl,
    };

    pub fn backend(self: *LinuxUsbBackend) kabi.KernelBackend {
        return .{ .ctx = self, .vtable = &vtable };
    }
};

// ---- internal tests for the parser ----------------------------------------
// (Pinned here so the parser is correct before we trust it in vEnumerate.)

test "parser: interface entry on the root hub" {
    const p = parseInterfaceEntry("1-0:1.0") orelse return error.UnexpectedNull;
    try std.testing.expectEqual(@as(u8, 1), p.bus);
    try std.testing.expectEqual(@as(u8, 1), p.port_depth);
    try std.testing.expectEqual(@as(u8, 0), p.port_chain[0]);
    try std.testing.expectEqual(@as(u8, 0), p.if_num);
}

test "parser: nested-hub interface entry (dotted port chain)" {
    const p = parseInterfaceEntry("3-9.4:1.2") orelse return error.UnexpectedNull;
    try std.testing.expectEqual(@as(u8, 3), p.bus);
    try std.testing.expectEqual(@as(u8, 2), p.port_depth);
    try std.testing.expectEqual(@as(u8, 9), p.port_chain[0]);
    try std.testing.expectEqual(@as(u8, 4), p.port_chain[1]);
    try std.testing.expectEqual(@as(u8, 2), p.if_num);
}

test "parser: deep interface entry" {
    const p = parseInterfaceEntry("2-1.4.3.2:1.0") orelse return error.UnexpectedNull;
    try std.testing.expectEqual(@as(u8, 2), p.bus);
    try std.testing.expectEqual(@as(u8, 4), p.port_depth);
}

test "parser: device-only entries are skipped (no colon)" {
    try std.testing.expect(parseInterfaceEntry("3-9") == null);
    try std.testing.expect(parseInterfaceEntry("1-0") == null);
}

test "parser: root-hub aliases (usbN) are skipped" {
    try std.testing.expect(parseInterfaceEntry("usb1") == null);
    try std.testing.expect(parseInterfaceEntry("usb2") == null);
}

test "parser: garbage is rejected" {
    try std.testing.expect(parseInterfaceEntry("garbage") == null);
    try std.testing.expect(parseInterfaceEntry("3-:1.0") == null); // empty port
    try std.testing.expect(parseInterfaceEntry("-9:1.0") == null); // empty bus
    try std.testing.expect(parseInterfaceEntry("3-9:1.zzz") == null); // bad iface
}

// ===========================================================================
// R11 (review wave 2) — concurrent raw-path-read vs acquire/release race pin
// for the `claims` table (wave-2 G6).
//
// Cluster A4 (commit f463527) dropped `Core.lock` around `backend.rawChannel`
// so a wedged helper IPC couldn't freeze the worker — correct at the lease
// layer, but it left LinuxUsbBackend's `claims` ArrayList read by the now-
// unlocked raw path (`Core.raw → rawChannel → findClaim`) WHILE release/revoke
// still mutate it. A concurrent `swapRemove` reorders/reallocs `claims.items`
// under a mid-iteration reader → iterator invalidation + use-after-free on the
// removed ClaimedInterface. (Same hazard for the `next_token` RMW.)
//
// The fix (commit c04fb59) added `claims_lock` over every read/write of
// `claims`/`next_token` and made `findClaim`/`findClaimByUrn` return a
// VALUE-COPY (so the caller's struct stays valid after the lock drops). This
// pin proves that empirically WITHOUT real USB hardware: the race surface is
// only the table + lock, which a synthetic seed populates directly.
//
// Faithfulness: the writer reproduces the EXACT critical sections the real ops
// take — vAcquire's `claims.append` and vRelease's `claims.swapRemove`, one
// mutation per `claims_lock` hold — minus only the post-lock fd ioctl/close,
// which isn't on the race surface (and would close our pooled fds). The readers
// call the REAL `findClaim`/`findClaimByUrn`.
//
// Honest scope: this is a contention/sampling test, not the exhaustive proof
// R9/R10 could be — a data race has unbounded interleavings and this toolchain
// ships no ThreadSanitizer. Detection rests on (1) the value-copy invariant —
// any torn read from a mid-`swapRemove` slot fails the id↔urn↔if_num↔fd
// correspondence — and (2) the testing allocator catching a use-after-free on
// the buffer that the periodic `clearAndFree` releases. A regression that drops
// `claims_lock` lets a reader iterate a freed/reordered buffer and trips both.
// The real-hardware raw+release end-to-end (live USBDEVFS_CLAIMINTERFACE, the
// SCM_RIGHTS channel, device-fd teardown) needs USB + root and stays gated.
// ===========================================================================

const r11_pool_size: u64 = 8;
// A moderate live list (≤64 entries) keeps each findClaim/findClaimByUrn scan
// long enough to overlap a writer free, while staying fast. if_num wraps the
// byte range but stays a pure function of id, so the canonical-consistency
// check is unaffected.
const r11_id_count: u64 = 64;
// How often the writer FREES the backing buffer (clearAndFree). Frequent frees
// are what turn a dropped-lock regression into a catch: a reader iterating the
// just-freed/reallocated buffer trips the invariant or the testing allocator
// (verified by the negative control noted at the test). Small + independent of
// the list size so free events stay dense.
const r11_free_period: usize = 8;

/// Canonical claim shape for a given id — a pure function, so any claim the
/// readers observe in the table MUST match this exactly. A torn read that
/// blends two ClaimedInterfaces breaks the correspondence and is caught.
fn r11UrnFor(id: u64, buf: []u8) Urn {
    const slice = std.fmt.bufPrint(buf, "hw:/usb/race/dev{d}", .{id}) catch unreachable;
    return Urn.from(slice);
}
fn r11Consistent(c: ClaimedInterface, id: u64, pool: []const i32) bool {
    if (c.id != id) return false;
    if (c.if_num != @as(u8, @intCast(id & 0xff))) return false;
    if (c.fd != pool[@intCast(id % r11_pool_size)]) return false;
    var buf: [64]u8 = undefined;
    const want = r11UrnFor(id, &buf);
    return c.urn.eql(&want);
}

const R11Ctx = struct {
    lb: *LinuxUsbBackend,
    pool: []const i32,
    reader_iters: usize,
    writer_iters: usize,
    failed: std.atomic.Value(bool),
};

fn r11Writer(ctx: *R11Ctx) void {
    const lb = ctx.lb;
    // Writer-private presence bookkeeping (no lock needed — only this thread
    // touches it) so appends never create a duplicate id and removes never miss.
    var present = [_]bool{false} ** (r11_id_count + 1);
    var buf: [64]u8 = undefined;
    var i: usize = 0;
    while (i < ctx.writer_iters) : (i += 1) {
        const id: u64 = @as(u64, @intCast(i % r11_id_count)) + 1;
        lb.claims_lock.lock();
        if (present[id]) {
            // vRelease's critical section verbatim: scan for the id, swapRemove.
            var idx: usize = 0;
            var found = false;
            for (lb.claims.items, 0..) |c, k| {
                if (c.id == id) {
                    idx = k;
                    found = true;
                    break;
                }
            }
            if (found) _ = lb.claims.swapRemove(idx);
            present[id] = false;
        } else {
            // vAcquire's critical section verbatim: append a canonical claim.
            const u = r11UrnFor(id, &buf);
            if (lb.claims.append(lb.alloc, .{
                .id = id,
                .fd = ctx.pool[@intCast(id % r11_pool_size)],
                .if_num = @as(u8, @intCast(id & 0xff)),
                .urn = u,
            })) |_| {
                present[id] = true;
            } else |_| {}
        }
        lb.claims_lock.unlock();

        // Once per cycle, FREE the backing buffer (a real ArrayList op — the
        // same allocator free the deinit path uses). A pre-G6 unlocked reader
        // mid-iteration over the freed buffer is a use-after-free the testing
        // allocator catches; post-G6 the reader holds claims_lock, so it can't.
        if (i % r11_free_period == r11_free_period - 1) {
            lb.claims_lock.lock();
            lb.claims.clearAndFree(lb.alloc);
            for (&present) |*p| p.* = false;
            lb.claims_lock.unlock();
        }
    }
}

fn r11Reader(ctx: *R11Ctx) void {
    const lb = ctx.lb;
    var buf: [64]u8 = undefined;
    var i: usize = 0;
    while (i < ctx.reader_iters) : (i += 1) {
        const id: u64 = @as(u64, @intCast(i % r11_id_count)) + 1;
        // (a) findClaim by id — null is fine (the writer may have it out right
        // now); a non-null result must be the canonical claim for THIS id.
        if (lb.findClaim(id)) |c| {
            if (!r11Consistent(c, id, ctx.pool)) ctx.failed.store(true, .seq_cst);
        }
        // (b) findClaimByUrn — a non-null hit's urn must map back to its own id.
        const u = r11UrnFor(id, &buf);
        if (lb.findClaimByUrn(&u)) |c| {
            if (c.id != id or !r11Consistent(c, id, ctx.pool)) ctx.failed.store(true, .seq_cst);
        }
    }
}

test "R11 (review wave 2): claims table survives concurrent findClaim vs acquire/release churn" {
    var lb = LinuxUsbBackend.init(std.testing.allocator);
    defer lb.deinit();

    // A pool of harmless real fds the synthetic claims reference. The writer
    // touches the claims TABLE directly (append/swapRemove) and never closes
    // these, so they stay valid for the whole race; closed once at teardown.
    var pool: [r11_pool_size]i32 = undefined;
    var opened: usize = 0;
    defer for (pool[0..opened]) |fd| {
        _ = linux.close(fd);
    };
    while (opened < r11_pool_size) : (opened += 1) {
        const rc = linux.open("/dev/null", .{ .ACCMODE = .RDONLY }, 0);
        if (linux.errno(rc) != .SUCCESS) return error.OpenFailed;
        pool[opened] = @intCast(rc);
    }

    var ctx = R11Ctx{
        .lb = &lb,
        .pool = &pool,
        // Many concurrent iteration windows over a churning, frequently-freed
        // buffer. The CORRECT (locked) code is deterministically green here;
        // a dropped-lock regression is caught with high probability per run —
        // verified by the negative control (lock stripped from findClaim →
        // index-out-of-bounds as a reader iterates a freed/reallocated buffer).
        // Runs well under a second.
        .reader_iters = 80_000,
        .writer_iters = 80_000,
        .failed = std.atomic.Value(bool).init(false),
    };

    const writer = try std.Thread.spawn(.{}, r11Writer, .{&ctx});
    var readers: [6]std.Thread = undefined;
    for (&readers) |*t| t.* = try std.Thread.spawn(.{}, r11Reader, .{&ctx});
    writer.join();
    for (0..readers.len) |i| readers[i].join();

    // No reader ever observed a torn / mis-keyed claim — the value-copy +
    // claims_lock discipline held under sustained contention + buffer churn.
    try std.testing.expect(!ctx.failed.load(.seq_cst));

    // Post-storm table invariants: no duplicate ids, every survivor still
    // canonical for its own id (no corruption left behind).
    for (lb.claims.items, 0..) |c, a| {
        try std.testing.expect(r11Consistent(c, c.id, &pool));
        for (lb.claims.items, 0..) |d, b| {
            if (a != b) try std.testing.expect(c.id != d.id);
        }
    }
}
