//! linux_iommu — IOMMU-group resolution + DMA-safety posture for the PCI claim
//! path (Tier-1 #7 / FEEDBACK LINUX-01 / survivability D9).
//!
//! A PCI device granted for shared/exclusive (DMA-capable) use can, if it is not
//! behind an IOMMU, DMA anywhere in host memory — D9's "silent total compromise."
//! The IOMMU is the kernel's first line of defense; the broker's job is to never
//! *silently* grant DMA-capable access to a device the IOMMU can't constrain. So
//! the PCI acquire path consults this module: a device that is not behind an
//! IOMMU group is REFUSED for a DMA-capable claim (honest, G2 — the broker
//! declares the degraded posture by refusing, rather than faking a safe grant);
//! a device behind an IOMMU group is safely VFIO-bindable.
//!
//! Read-only `observe` claims need no DMA and are not this module's concern.
//!
//! ## Scope (what this is, and the hardware gate)
//!
//! This resolves the IOMMU group from sysfs (`/sys/bus/pci/devices/<BDF>/iommu_group`
//! symlink → group number) and decides the DMA-safety posture — the precondition
//! the VFIO bind needs and the honest no-IOMMU refusal. The actual enforcement
//! BIND (open `/dev/vfio/<group>`, detach the host driver, `VFIO_GROUP_GET_DEVICE_FD`,
//! and the matching IOMMU-mapping teardown on revoke) needs root AND the device
//! released from its host driver — it can't run against a live in-use device — so
//! it stays gated on a dedicated passthrough device. The posture decision here is
//! the buildable, testable foundation; `linux_backend.vAcquire` is where the bind
//! slots in once a passthrough device + privileges are available.

const std = @import("std");
const model = @import("mgf_hrb_model");

const Urn = model.Urn;
const linux = std.os.linux;

/// The DMA-safety posture of a resource for a DMA-capable (shared/exclusive) claim.
pub const Posture = enum {
    /// Not a PCI device (e.g. USB) — DMA is constrained by the bus, not this path.
    not_pci,
    /// A PCI device NOT behind an IOMMU group — granting DMA-capable access is
    /// structurally unsafe (D9). Refuse.
    no_iommu,
    /// A PCI device behind an IOMMU group — safely VFIO-bindable.
    iommu_protected,
};

/// Extract the BDF from a PCI URN `hw:/pci/<BDF>/fn0`, or null if not PCI. Pure.
pub fn bdfFromPciUrn(urn: *const Urn) ?[]const u8 {
    const prefix = "hw:/pci/";
    const s = urn.slice();
    if (!std.mem.startsWith(u8, s, prefix)) return null;
    const after = s[prefix.len..];
    const slash = std.mem.indexOfScalar(u8, after, '/') orelse return null;
    const bdf = after[0..slash];
    return if (bdf.len == 0) null else bdf;
}

/// Does the host have an IOMMU enabled at all? `/sys/kernel/iommu_groups` is
/// present (and populated) exactly when an IOMMU is active.
pub fn hostHasIommu() bool {
    return linux.errno(linux.access("/sys/kernel/iommu_groups", 0)) == .SUCCESS;
}

/// The IOMMU group number for a PCI device, or null if it isn't behind one (no
/// `iommu_group` symlink → not IOMMU-protected). Reads the sysfs symlink and
/// parses the trailing group number from its target
/// (`…/kernel/iommu_groups/<N>`). The group is what VFIO binds — the whole group
/// moves together, which is also why this resolution is the bind's precondition.
pub fn iommuGroupFor(bdf: []const u8) ?u32 {
    var path_buf: [256]u8 = undefined;
    const sym = std.fmt.bufPrintZ(&path_buf, "/sys/bus/pci/devices/{s}/iommu_group", .{bdf}) catch return null;
    var link_buf: [256]u8 = undefined;
    const rc = linux.readlink(sym, &link_buf, link_buf.len);
    if (linux.errno(rc) != .SUCCESS) return null; // no symlink → not behind an IOMMU
    const target = link_buf[0..rc]; // e.g. "../../../kernel/iommu_groups/1"
    const slash = std.mem.lastIndexOfScalar(u8, target, '/') orelse return null;
    return std.fmt.parseInt(u32, target[slash + 1 ..], 10) catch null;
}

/// The DMA-safety posture for a claim on `urn`. Non-PCI → `not_pci`; a PCI device
/// behind an IOMMU group → `iommu_protected`; a PCI device with no IOMMU group
/// (or a host with no IOMMU at all) → `no_iommu`.
pub fn postureFor(urn: *const Urn) Posture {
    const bdf = bdfFromPciUrn(urn) orelse return .not_pci;
    if (iommuGroupFor(bdf) != null) return .iommu_protected;
    return .no_iommu;
}

// ===========================================================================
// #7 / D9 — IOMMU-safety pins.
// ===========================================================================

const testing = std.testing;

test "D9: non-PCI URNs are not_pci; a PCI BDF without an iommu_group is no_iommu" {
    // USB is bus-constrained, not this path's concern.
    try testing.expectEqual(Posture.not_pci, postureFor(&Urn.from("hw:/usb/bus1/dev4/if0")));
    // A PCI BDF that doesn't exist on this host has no iommu_group symlink → the
    // device would be a DMA-unsafe grant (no IOMMU protection).
    try testing.expectEqual(Posture.no_iommu, postureFor(&Urn.from("hw:/pci/dead:ff:1f.7/fn0")));
    // Malformed PCI URN → not attributable → not_pci.
    try testing.expectEqual(Posture.not_pci, postureFor(&Urn.from("hw:/pci/")));
}

test "D9: BDF extraction" {
    const pci = Urn.from("hw:/pci/0000:01:00.0/fn0");
    try testing.expect(std.mem.eql(u8, "0000:01:00.0", bdfFromPciUrn(&pci).?));
    try testing.expect(bdfFromPciUrn(&Urn.from("hw:/usb/bus1/dev4/if0")) == null);
}

test "D9 (host integration): a real IOMMU-protected PCI device resolves to a group" {
    // Best-effort against this host. On a host with an IOMMU enabled, every real
    // PCI device sits in a group; pick the first one and assert the resolution +
    // posture agree. On a no-IOMMU host (containers / disabled in BIOS) this
    // simply finds nothing and passes — the deterministic edge cases above cover
    // the logic.
    if (!hostHasIommu()) return;
    const rc = linux.open("/sys/bus/pci/devices", .{ .ACCMODE = .RDONLY, .DIRECTORY = true }, 0);
    if (linux.errno(rc) != .SUCCESS) return;
    const fd: i32 = @intCast(rc);
    defer _ = linux.close(fd);
    var buf: [4096]u8 align(@alignOf(linux.dirent64)) = undefined;
    while (true) {
        const got = linux.getdents64(fd, &buf, buf.len);
        if (linux.errno(got) != .SUCCESS or got == 0) break;
        var pos: usize = 0;
        while (pos < got) {
            const dent: *linux.dirent64 = @ptrCast(@alignCast(&buf[pos]));
            pos += dent.reclen;
            const name = std.mem.span(@as([*:0]const u8, @ptrCast(&dent.name)));
            if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) continue;
            if (iommuGroupFor(name)) |_| {
                // Found a real IOMMU-protected device — its URN posture must agree.
                var urn_buf: [128]u8 = undefined;
                const urn_str = std.fmt.bufPrint(&urn_buf, "hw:/pci/{s}/fn0", .{name}) catch continue;
                try testing.expectEqual(Posture.iommu_protected, postureFor(&Urn.from(urn_str)));
                return; // one real device is enough to exercise the live path
            }
        }
    }
}
