//! test/system/services/badge-scope-test — the guessable-id probe. Two providers
//! hand small integers to a client and then take them back from anyone who says
//! the number: the FAT server's node ids (a table of thirty-two) and the
//! compositor's layer ids (a table of sixteen). P4c scoped both to the badge that
//! opened them (docs/os-development/protocol-namespace.md — *handles must be
//! scoped per client, validated against the badge*), and this fixture is the
//! proof.
//!
//! It runs as two processes of the same binary, because the hole is a
//! *cross-client* one and a single process cannot demonstrate it:
//!
//!   - the **owner** (no arguments — the scenario spawns this one) opens a file
//!     on the volume and creates a layer, keeps both, and spawns
//!   - the **intruder** (the two ids as argv), which opens a file and a layer of
//!     its own and then names the owner's.
//!
//! Every refusal is paired with the same operation on the intruder's own id, so
//! the case cannot pass against a provider that refuses everything; and the owner
//! re-uses both ids after the intruder has finished — including after its attempt
//! to *close* the file node — so the case cannot pass against a provider that
//! honoured the intruder and merely reported failure. The pass marker is
//! `badge-scope-test: ok` from the intruder plus `badge-scope-test: owner intact`
//! from the owner.
//!
//! What it cannot check: which id the intruder was refused *for*. A refusal is
//! deliberately identical to "no such id" — that is the discipline being tested —
//! so the fixture proves the boundary by construction (an id it was told about by
//! its parent, which is holding it) rather than by reading anything back.

const std = @import("std");
const display = @import("display-client");
const fs = @import("file-system");
const ipc = @import("ipc");
const logging = @import("logging");
const process = @import("process");
const time = @import("time");

/// A scratch file on the volume, so the node the intruder tries to write through
/// is one nothing else reads. (A foreign write that *succeeded* would prove the
/// bug — it must not also damage the boot volume proving it.)
const held_path = "/volumes/usb/BADGE.TXT";
const held_contents = "held";

fn line(comptime format: []const u8, arguments: anytype) void {
    var buffer: [192]u8 = undefined;
    _ = logging.write(std.fmt.bufPrint(&buffer, format, arguments) catch return);
}

/// The fat server mounts /volumes/usb only after the whole USB storage chain is
/// up, and both instances race it.
fn waitForVolume() bool {
    var tries: u32 = 0;
    while (tries < 1400) : (tries += 1) {
        if (fs.openDirectory("/volumes/usb")) |opened| {
            var directory = opened;
            directory.close();
            return true;
        }
        time.sleepMillis(50);
    }
    return false;
}

pub fn main(init: process.Init) void {
    if (init.arguments.get(1)) |node_text| {
        const layer_text = init.arguments.get(2) orelse {
            _ = logging.write("badge-scope-test: FAILED (intruder without a layer id)\n");
            return;
        };
        const node = std.fmt.parseInt(u64, node_text, 10) catch return;
        const layer = std.fmt.parseInt(u32, layer_text, 10) catch return;
        intrude(node, layer);
        return;
    }
    own();
}

// --- the owner ---------------------------------------------------------------

fn own() void {
    if (!waitForVolume()) {
        _ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n");
        return;
    }
    var held = fs.open(held_path, .{ .create = true, .truncate = true }) orelse {
        _ = logging.write("badge-scope-test: FAILED (owner could not create its file)\n");
        return;
    };
    if ((held.writeAll(held_contents) orelse 0) != held_contents.len) {
        _ = logging.write("badge-scope-test: FAILED (owner could not write its file)\n");
        return;
    }
    const layer = display.createLayer(240, 40, 32, 32, 3) orelse {
        _ = logging.write("badge-scope-test: FAILED (owner could not create a layer)\n");
        return;
    };
    _ = layer.fill(0, 0, 32, 32, display.color(0x20, 0x80, 0xC0));
    line("badge-scope-test: owner holds node {d} and layer {d}\n", .{ held.node, layer.id });

    // The intruder is told both ids outright: guessing them is not what is being
    // tested (they are small integers — a prober would simply enumerate), and a
    // fixture that had to search would be asserting on a race instead of on the
    // rule.
    var node_text: [24]u8 = undefined;
    var layer_text: [12]u8 = undefined;
    const arguments = [_][]const u8{
        std.fmt.bufPrint(&node_text, "{d}", .{held.node}) catch return,
        std.fmt.bufPrint(&layer_text, "{d}", .{layer.id}) catch return,
    };
    const endpoint = ipc.createIpcEndpoint() orelse {
        _ = logging.write("badge-scope-test: FAILED (owner has no exit endpoint)\n");
        return;
    };
    const intruder = process.spawnSupervised("badge-scope-test", &arguments, endpoint) orelse {
        _ = logging.write("badge-scope-test: FAILED (could not spawn the intruder)\n");
        return;
    };
    var receive: [8]u8 = undefined;
    while (true) {
        const got = ipc.replyWait(endpoint, &.{}, &receive, null);
        if (got.isChildExit() and got.childProcessId() == intruder) break;
    }

    // Both ids must still be the owner's, and still work — the half of the proof
    // the intruder cannot give, because a provider that had honoured its close or
    // its destroy would have answered it exactly as one that refused.
    held.seekTo(0);
    var buffer: [16]u8 = undefined;
    const read = held.read(&buffer) orelse 0;
    const node_intact = read == held_contents.len and std.mem.eql(u8, buffer[0..read], held_contents);
    const layer_intact = layer.fill(0, 0, 32, 32, display.color(0x20, 0xC0, 0x80));
    if (node_intact and layer_intact) {
        _ = logging.write("badge-scope-test: owner intact\n");
    } else {
        line("badge-scope-test: FAILED (owner lost its ids: node={} layer={})\n", .{ node_intact, layer_intact });
    }
    _ = layer.destroy();
    held.close();
}

// --- the intruder -------------------------------------------------------------

fn intrude(foreign_node: u64, foreign_layer: u32) void {
    if (!waitForVolume()) {
        _ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n");
        return;
    }
    const node_verdict = probeNode(foreign_node);
    const layer_verdict = probeLayer(foreign_layer);
    if (node_verdict and layer_verdict) {
        _ = logging.write("badge-scope-test: ok\n");
    } else {
        _ = logging.write("badge-scope-test: FAILED\n");
    }
}

/// The FAT half: the intruder's own node works, the owner's does not.
fn probeNode(foreign: u64) bool {
    var mine = fs.open(held_path, .{}) orelse {
        _ = logging.write("badge-scope-test: FAILED (intruder could not open its own file)\n");
        return false;
    };
    defer mine.close();
    if (mine.backend == null or mine.node == foreign) {
        line("badge-scope-test: FAILED (intruder's node {d} is not distinct from {d})\n", .{ mine.node, foreign });
        return false;
    }

    var buffer: [16]u8 = undefined;
    const own_read = (mine.read(&buffer) orelse 0) == held_contents.len;

    // The same backend channel, the same verbs, one different integer. Every one
    // of these worked before the owner check went in.
    var forged = fs.File{ .node = foreign, .backend = mine.backend };
    const read_refused = forged.read(&buffer) == null;
    const status_refused = forged.attributes() == null;
    const write_refused = forged.write("intruded") == null;
    forged.close(); // must not release the owner's node — the owner proves that after we exit

    mine.seekTo(0);
    const own_read_again = (mine.read(&buffer) orelse 0) == held_contents.len;

    const ok = own_read and own_read_again and read_refused and status_refused and write_refused;
    line("badge-scope-test: node own={} read_refused={} status_refused={} write_refused={} own_again={}\n", .{
        own_read, read_refused, status_refused, write_refused, own_read_again,
    });
    return ok;
}

/// The display half: the intruder's own layer obeys it, the owner's ignores it.
fn probeLayer(foreign: u32) bool {
    const mine = display.createLayer(200, 40, 32, 32, 4) orelse {
        _ = logging.write("badge-scope-test: FAILED (intruder could not create a layer)\n");
        return false;
    };
    if (mine.id == foreign) {
        line("badge-scope-test: FAILED (intruder's layer {d} is not distinct)\n", .{mine.id});
        return false;
    }
    const own_fill = mine.fill(0, 0, 32, 32, display.color(0xC0, 0x40, 0x40));
    const own_configure = mine.configure(200, 80, 4, true);

    const forged = display.Layer{ .id = foreign };
    const fill_refused = !forged.fill(0, 0, 32, 32, display.color(0xFF, 0, 0));
    const configure_refused = !forged.configure(0, 0, 9, false);
    const destroy_refused = !forged.destroy();

    const own_still = mine.fill(0, 0, 32, 32, display.color(0x40, 0xC0, 0x40));
    _ = mine.destroy();

    const ok = own_fill and own_configure and own_still and fill_refused and configure_refused and destroy_refused;
    line("badge-scope-test: layer own={} fill_refused={} configure_refused={} destroy_refused={} own_again={}\n", .{
        own_fill, fill_refused, configure_refused, destroy_refused, own_still,
    });
    return ok;
}
