Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger-ui

xtask/src/main.rs

//! `cargo xtask build | console | publish | verify-chain` — holger-ui's repository tasks, in Rust.
//!
//! `publish` and `verify-chain` replace `deploy/publish.sh` and `deploy/verify-chain.sh`,
//! which shelled out to python3, sed, curl, strings, ss, systemctl and tailscale.
//! Everything below is done in process: the CSP hash with sha2, HTTP with ureq, systemd
//! over D-Bus with zbus, the listener check from `/proc/net/tcp`, tailscale through its
//! LocalAPI socket.
//!
//! `build` (see [`build`]) carries NO build knowledge of its own: every package, profile
//! and target it uses is a field of a variant `holger-ui-recipe` declares, and
//! `.nornir/build-variants.toml` is that declaration EMITTED for the tools that discover
//! this repository by that path.
//!
//! **One named exception:** `build`, `console` and `publish` run `trunk build`. trunk has no
//! library API; it is the build tool this repo already requires (`cargo install trunk`),
//! and it is the only thing that produces the content-hashed bundle every door here
//! serves.

mod build;

use std::fs;
use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::time::Duration;

use anyhow::{bail, ensure, Context, Result};
use base64::Engine;
use sha2::{Digest, Sha256};

/// The tailnet origin the prod chain is served on.
const HOST: &str = "https://henrik-thinkpad-t14s-gen-6.tail4eceda.ts.net";
/// The quadlet-generated unit serving the console, and the server behind it.
const CONSOLE_UNIT: &str = "holger-console.service";
const SERVER_UNIT: &str = "holger-server.service";
/// nginx's loopback port.
const CONSOLE_PORT: u16 = 8480;

/// `cargo xtask build [--variant <name>] [--suite <name>] [--list]`; see [`build`].
///
/// The declaration is `holger-ui-recipe`'s. This function knows the name of no package,
/// no feature and no triple — it resolves rows through the constellation's shared
/// `suite_cli` parser and drives each one. There is no second place a build point of this
/// repository is defined.
fn task_build() -> Result<()> {
    let argv: Vec<String> = std::env::args().skip(2).collect();
    match build::run(&repo_root(), &argv) {
        Ok(names) => {
            println!("\n{} variant(s): {}", names.len(), names.join(" · "));
            Ok(())
        }
        Err(e) => bail!("{e}"),
    }
}

/// Every task's one-line usage, so a typo and a bare invocation print the same list and
/// neither can go stale on its own.
const USAGE: &str = "usage: cargo xtask build [--variant <name>] [--suite <name>] [--list]
       cargo xtask publish
       cargo xtask console          (the open bundle into crates/holger-ui/console, for `cargo install holger-ui`)
       cargo xtask verify-chain
       cargo xtask ci [--quick|--nightly] [--list] [--only <prefix>]
       cargo xtask browser [<test filter>]";

/// `cargo xtask ci`: holger-ui's CI plan on the shared runner (nornir-xtask-support
/// `ci`): its lock, its tests (every buy route answers from its owner, never the SPA
/// fallback, among them), the wasm console checked for its target, and (full) the MVP
/// rows. One JSON receipt per run.
fn task_ci() -> ExitCode {
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("xtask sits in the repo").to_path_buf();
    let plan = ci_plan(root);
    let args: Vec<String> = std::env::args().skip(2).collect();
    ExitCode::from(nornir_xtask_support::ci::main(&plan, &args).clamp(0, 255) as u8)
}

/// holger-ui's CI plan, the steps in order.
fn ci_plan(root: PathBuf) -> nornir_xtask_support::ci::Plan {
    use nornir_xtask_support::ci::{Mode, Plan, Step};
    Plan {
        repo: "holger-ui".into(),
        root: root.clone(),
        steps: vec![
            nornir_xtask_support::lockcheck::step(&root),
            Step::cargo("test", &["test", "--locked", "--workspace"]),
            // The paid half (A28 M13): holger-ui-server's buy routes and pages and the console's
            // paid pages exist only under `metered`, so both suites run that way too; the open
            // build's own are in `test`.
            Step::cargo("test:metered", &["test", "--locked", "-p", "holger-ui-server", "-p", "holger-ui-web", "--features", "holger-ui-server/metered,holger-ui-web/metered"]),
            // holger.rs's public face is off in the default build, so `test` above never
            // compiles its mount; these do (TASKS-12 S-6, `public-site`).
            Step::cargo("public-site", &["test", "--locked", "-p", "holger-ui-server", "--features", "public-site"]),
            Step::cargo("public-site:metered", &["test", "--locked", "-p", "holger-ui-server", "--features", "metered,public-site"]),
            Step::cargo("buy-routes", &["test", "--locked", "-p", "holger-ui-server", "--features", "metered", "every_buy_route_answers_from_its_owner_never_the_spa"]),
            Step::cargo("wasm", &["check", "--locked", "-p", "holger-ui-web", "--target", "wasm32-unknown-unknown"]),
            // The paid console for its target too: the open check above compiles none of it.
            Step::cargo("wasm:metered", &["check", "--locked", "-p", "holger-ui-web", "--target", "wasm32-unknown-unknown", "--features", "metered"]),
            // The browser tests (browser-tests/): a headless Chromium against the real bundle,
            // holger-ui-server, holger and the shared fake ONE box. A host step: the verb enters
            // its own image (the toolchain image plus Chromium).
            Step::host("browser", "cargo", &["xtask", "browser"]).timeout(3600),
            // The rows build enters the declared container itself (the Build Thing), so it runs
            // on the host: inside the toolchain container there is no engine socket. It first
            // asks the engine for the pinned image by digest and skips out loud when the box
            // lacks it (the image is published in no registry; see `build::rows_skip_reason`).
            Step::in_process("rows:MVP", move |log| rows_mvp(&root, log)).from(Mode::Full),
        ],
    }
}

/// `rows:MVP`: `cargo xtask build --suite MVP` on the host, when this box holds the
/// pinned toolchain image; else a skip that names the digest and the reason.
fn rows_mvp(root: &Path, log: &mut dyn Write) -> nornir_xtask_support::Result<Option<String>> {
    let image = build::pinned_image_ref()?;
    let engine = std::env::var("NORNIR_CI_ENGINE").unwrap_or_else(|_| "podman".into());
    let present = match Command::new(&engine).args(["image", "exists", &image]).status() {
        Ok(s) if s.success() => Ok(true),
        Ok(s) if s.code() == Some(1) => Ok(false),
        Ok(s) => Err(format!("`{engine} image exists` exited {s}")),
        Err(e) => Err(format!("could not start {engine}: {e}")),
    };
    writeln!(log, "rows:MVP: the pinned toolchain image {image}: {present:?}")?;
    if let Some(why) = build::rows_skip_reason(&image, present) {
        writeln!(log, "{why}")?;
        return Ok(Some(why));
    }
    let out = Command::new("cargo").args(["xtask", "build", "--suite", "MVP"]).current_dir(root).output()?;
    log.write_all(&out.stdout)?;
    log.write_all(&out.stderr)?;
    if out.status.success() {
        Ok(None)
    } else {
        Err(format!("cargo xtask build --suite MVP exited {}", out.status).into())
    }
}

/// **`cargo xtask browser`**: holger-ui's browser tests (`browser-tests/`, its own workspace
/// over the sibling checkouts) in the browser image: the toolchain image plus Debian's Chromium
/// (`browser-tests/Containerfile`), built once per content of that file and tagged by its hash.
/// Inside a container (the CI runner's, or `NORNIR_CI_HOST=1`) the tests run right here and
/// `$CHROME` (else `chromium`) must be installed.
fn task_browser(filter: Option<String>) -> anyhow::Result<()> {
    use anyhow::{bail, Context as _};
    use sha2::Digest as _;
    use std::process::Command;
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("xtask sits in the repo").to_path_buf();
    let mut args: Vec<String> = ["test", "--locked"].iter().map(|s| s.to_string()).collect();
    args.extend(["--".into(), "--test-threads".into(), "2".into()]);
    if let Some(f) = filter {
        args.push(f);
    }
    let run = |mut c: Command, what: &str| -> anyhow::Result<()> {
        let status = c.status().with_context(|| format!("start {what}"))?;
        if !status.success() {
            bail!("{what} exited {status}");
        }
        Ok(())
    };
    if nornir_xtask_support::ci::toolchain_is_here() {
        let mut c = Command::new("cargo");
        c.args(&args).current_dir(root.join("browser-tests"));
        return run(c, "the browser tests");
    }
    let mut image = nornir_xtask_support::ci::Container::from_env(&root);
    let file = root.join("browser-tests/Containerfile");
    let recipe = std::fs::read(&file).with_context(|| format!("read {}", file.display()))?;
    let tag = format!("localhost/holger-ui-browser:{}", &hex(&sha2::Sha256::digest(&recipe))[..16]);
    let present = Command::new(&image.engine).args(["image", "exists", &tag]).status().map(|s| s.success()).unwrap_or(false);
    if !present {
        let mut c = Command::new(&image.engine);
        c.args(["build", "-t", &tag, "--build-arg", &format!("BASE={}", image.image), "-f"]).arg(&file).arg(root.join("browser-tests"));
        run(c, "the browser image's build")?;
    }
    image.image = tag;
    std::fs::create_dir_all(&image.target).with_context(|| format!("create {}", image.target.display()))?;
    let argv = image.argv(Some("browser-tests"), &[], "cargo", &args);
    let mut c = Command::new(&argv[0]);
    c.args(&argv[1..]);
    run(c, "the browser tests in their image")
}

fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

fn main() -> ExitCode {
    let verb = std::env::args().nth(1);
    let result = match verb.as_deref() {
        Some("ci") => return task_ci(),
        Some("browser") => task_browser(std::env::args().nth(2)),
        Some("build") => task_build(),
        Some("publish") => publish(),
        Some("console") => console(),
        Some("verify-chain") => verify_chain().map(|ok| {
            if !ok {
                std::process::exit(1)
            }
        }),
        _ => {
            eprintln!("{USAGE}");
            return ExitCode::from(2);
        }
    };
    match result {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("xtask: {e:#}");
            ExitCode::FAILURE
        }
    }
}

fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("xtask sits under the repo root").to_path_buf()
}

/// `~/.local/share/holger-console` — the PUBLISHED copy the quadlet mounts, never `dist/`.
fn publish_dir() -> Result<PathBuf> {
    let home = std::env::var_os("HOME").context("HOME is not set")?;
    Ok(PathBuf::from(home).join(".local/share/holger-console"))
}

// ── console ─────────────────────────────────────────────────────────────────

/// `cargo xtask console`: build the open console bundle (trunk, release) and copy it into
/// `crates/holger-ui/console/`, where the `holger-ui` program's build embeds it and a
/// published crate carries it. Then `cargo install --locked --path crates/holger-ui`.
fn console() -> Result<()> {
    let root = repo_root();
    let dist = root.join("dist");
    // The named exception: trunk has no library API.
    let status = Command::new("trunk")
        .current_dir(&root)
        .args(["build", "--release"])
        .status()
        .context("run `trunk build` (is trunk installed? `cargo install trunk`)")?;
    ensure!(status.success(), "trunk build failed: {status}");
    ensure!(dist.join("index.html").is_file(), "trunk wrote no {}", dist.join("index.html").display());
    let into = root.join("crates/holger-ui/console");
    if into.exists() {
        fs::remove_dir_all(&into).with_context(|| format!("clear {}", into.display()))?;
    }
    let n = copy_tree(&dist, &into)?;
    println!("console: {n} file(s) from {} into {}", dist.display(), into.display());
    Ok(())
}

fn copy_tree(from: &Path, to: &Path) -> Result<usize> {
    fs::create_dir_all(to)?;
    let mut n = 0;
    for e in fs::read_dir(from)? {
        let e = e?;
        let (src, dst) = (e.path(), to.join(e.file_name()));
        if src.is_dir() {
            n += copy_tree(&src, &dst)?;
        } else {
            fs::copy(&src, &dst).with_context(|| format!("copy {}", src.display()))?;
            n += 1;
        }
    }
    Ok(n)
}

// ── publish ─────────────────────────────────────────────────────────────────

/// Build into the published copy, generate nginx.conf with a strict per-release CSP, and
/// restart the serving unit. `IDP_ORIGIN=https://idp.example` allows the OIDC IdP.
fn publish() -> Result<()> {
    let root = repo_root();
    let publ = publish_dir()?;
    let dist = publ.join("dist");

    // The named exception: trunk has no library API.
    let status = Command::new("trunk")
        .current_dir(&root)
        .args(["build", "--release", "--dist"])
        .arg(&dist)
        .status()
        .context("run `trunk build` (is trunk installed? `cargo install trunk`)")?;
    ensure!(status.success(), "trunk build failed: {status}");

    let html = fs::read_to_string(dist.join("index.html")).context("read the built index.html")?;
    let hash = bootstrap_hash(&html).context("trunk bootstrap script not found in index.html")?;

    let idp = std::env::var("IDP_ORIGIN").ok().filter(|s| !s.is_empty());
    let csp = csp(&hash, idp.as_deref());
    let template = fs::read_to_string(root.join("deploy/nginx.conf"))?;
    let conf = render_nginx_conf(&template, &csp)?;
    fs::write(publ.join("nginx.conf"), conf)?;

    // Only what the container bind-mounts needs to be readable by the mapped uid
    // (dist + nginx.conf). The data root under the same directory — the artifact
    // store, the promote seals, the audit segments — must NOT be opened up.
    add_read_bits_recursive(&dist)?;
    add_read_bits(&publ.join("nginx.conf"), false)?;

    restart_user_unit(CONSOLE_UNIT)?;
    let mut wasm: Vec<String> = fs::read_dir(&dist)?
        .filter_map(|e| e.ok())
        .map(|e| e.file_name().to_string_lossy().into_owned())
        .filter(|n| n.ends_with("_bg.wasm"))
        .collect();
    wasm.sort();
    println!("published: dist {} · CSP sha256-{hash}", wasm.join(" "));
    Ok(())
}

/// sha256 of the ONE inline `<script type="module">` trunk emits, base64 — so the CSP can
/// name it instead of allowing 'unsafe-inline'.
fn bootstrap_hash(html: &str) -> Option<String> {
    const OPEN: &str = "<script type=\"module\">";
    let start = html.find(OPEN)? + OPEN.len();
    let len = html[start..].find("</script>")?;
    let digest = Sha256::digest(&html.as_bytes()[start..start + len]);
    Some(base64::engine::general_purpose::STANDARD.encode(digest))
}

fn csp(hash: &str, idp_origin: Option<&str>) -> String {
    let connect = match idp_origin {
        Some(o) => format!("'self' {o}"),
        None => "'self'".to_string(),
    };
    format!(
        "default-src 'none'; script-src 'self' 'sha256-{hash}' 'wasm-unsafe-eval'; \
         style-src 'self' 'unsafe-inline'; connect-src {connect}; img-src 'self' data:; \
         font-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'"
    )
}

/// Every `# __CSP__` marker becomes the header. A template with no marker is refused: it
/// would publish a site with NO CSP at all.
fn render_nginx_conf(template: &str, csp: &str) -> Result<String> {
    const MARKER: &str = "# __CSP__";
    ensure!(template.contains(MARKER), "deploy/nginx.conf carries no `{MARKER}` marker");
    Ok(template.replace(MARKER, &format!("add_header Content-Security-Policy \"{csp}\" always;")))
}

/// `chmod -R a+rX`: read for all, and search/exec for all on directories and on files that
/// already carry an exec bit.
fn add_read_bits_recursive(dir: &Path) -> Result<()> {
    add_read_bits(dir, true)?;
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        if entry.file_type()?.is_dir() {
            add_read_bits_recursive(&entry.path())?;
        } else {
            add_read_bits(&entry.path(), false)?;
        }
    }
    Ok(())
}

fn add_read_bits(path: &Path, is_dir: bool) -> Result<()> {
    let mut perm = fs::metadata(path)?.permissions();
    let mode = perm.mode();
    let x = if is_dir || mode & 0o111 != 0 { 0o111 } else { 0 };
    perm.set_mode(mode | 0o444 | x);
    fs::set_permissions(path, perm).with_context(|| format!("chmod {}", path.display()))
}

// ── systemd --user over D-Bus ───────────────────────────────────────────────

fn systemd_manager(conn: &zbus::blocking::Connection) -> Result<zbus::blocking::Proxy<'static>> {
    Ok(zbus::blocking::Proxy::new(
        conn,
        "org.freedesktop.systemd1",
        "/org/freedesktop/systemd1",
        "org.freedesktop.systemd1.Manager",
    )?)
}

fn restart_user_unit(unit: &str) -> Result<()> {
    let conn = zbus::blocking::Connection::session().context("connect to the user session bus")?;
    let _job: zbus::zvariant::OwnedObjectPath = systemd_manager(&conn)?
        .call("RestartUnit", &(unit, "replace"))
        .with_context(|| format!("RestartUnit {unit}"))?;
    Ok(())
}

/// `systemctl --user is-active <unit>`, as the ActiveState property.
fn user_unit_active_state(unit: &str) -> Result<String> {
    let conn = zbus::blocking::Connection::session().context("connect to the user session bus")?;
    let path: zbus::zvariant::OwnedObjectPath = systemd_manager(&conn)?
        .call("LoadUnit", &(unit,))
        .with_context(|| format!("LoadUnit {unit}"))?;
    let proxy = zbus::blocking::Proxy::new(&conn, "org.freedesktop.systemd1", path, "org.freedesktop.systemd1.Unit")?;
    Ok(proxy.get_property::<String>("ActiveState")?)
}

// ── verify-chain ────────────────────────────────────────────────────────────

struct Checks {
    failed: bool,
}

impl Checks {
    fn check(&mut self, name: &str, expected: &str, actual: &str) {
        if expected == actual {
            println!("OK   {name} ({actual})");
        } else {
            println!("FAIL {name} (väntade {expected}, fick {actual})");
            self.failed = true;
        }
    }
    fn ok(&mut self, line: &str) {
        println!("OK   {line}");
    }
    fn fail(&mut self, line: &str) {
        println!("FAIL {line}");
        self.failed = true;
    }
}

/// Verify the console's whole prod chain, outside-in. Run after a reboot (or anytime):
/// true only when every link answers.
fn verify_chain() -> Result<bool> {
    let mut c = Checks { failed: false };
    let state = |u: &str| user_unit_active_state(u).unwrap_or_else(|e| format!("okänt: {e:#}"));

    c.check("holger-server.service", "active", &state(SERVER_UNIT));
    c.check("holger-console (nginx)", "active", &state(CONSOLE_UNIT));
    c.check(
        "tailscale serve-konfig",
        "200",
        &tailscale_serve_config_status().map(|s| s.to_string()).unwrap_or_else(|e| format!("{e:#}")),
    );

    let agent: ureq::Agent = ureq::Agent::config_builder()
        .timeout_global(Some(Duration::from_secs(10)))
        .http_status_as_error(false)
        .build()
        .into();
    // Asked for as a browser would: the SPA fallback answers only a NAVIGATION
    // (no extension, `Accept: text/html` — `holger_ui_server::serve::is_navigation`);
    // a bare `*/*` GET of a deep link is a 404 on purpose.
    let status = |url: String| match agent.get(&url).header("Accept", "text/html").call() {
        Ok(r) => r.status().as_u16().to_string(),
        Err(e) => format!("{e}"),
    };
    c.check("statisk SPA via tailnet", "200", &status(format!("{HOST}/")));
    c.check("SPA-fallback (djuplänk)", "200", &status(format!("{HOST}/repositories")));
    c.check("saknad fil → 404", "404", &status(format!("{HOST}/saknas.txt")));

    // A gRPC-web frame header with an empty message.
    let empty_frame = [0u8; 5];
    let grpc = |svc_method: &str| {
        agent
            .post(&format!("{HOST}/grpc/holger.v1.{svc_method}"))
            .header("Content-Type", "application/grpc-web+proto")
            .send(&empty_frame[..])
    };

    match grpc("AdminService/Health").and_then(|mut r| r.body_mut().read_to_vec()) {
        Ok(body) if String::from_utf8_lossy(&body).contains("holger") => c.ok("gRPC-web Health"),
        Ok(body) => c.fail(&format!("gRPC-web Health ({})", printable(&body))),
        Err(e) => c.fail(&format!("gRPC-web Health ({e})")),
    }

    // The write gate must refuse anonymous — a chain that answers but does not gate is
    // NOT healthy. gRPC-web carries the status as a header or in the trailer frame.
    match grpc("RepositoryService/PutArtifact") {
        Ok(mut r) => {
            let header = r.headers().get("grpc-status").and_then(|v| v.to_str().ok()).map(str::trim).map(str::to_string);
            let body = r.body_mut().read_to_vec().unwrap_or_default();
            let trailer = String::from_utf8_lossy(&body).to_ascii_lowercase().replace(' ', "");
            let gated = header.as_deref() == Some("16") || trailer.contains("grpc-status:16");
            c.check("skrivgrind (anonym → 16)", "grpc-status: 16", if gated { "grpc-status: 16" } else { "saknas" });
        }
        Err(e) => c.fail(&format!("skrivgrind (anonym → 16) ({e})")),
    }

    // The CSP must be PRESENT and must match the page being served: a hand-copied
    // nginx.conf leaves the marker as a comment (no header at all), and a stale hash
    // silently blocks the wasm bootstrap.
    match agent.get(&format!("{HOST}/")).call() {
        Ok(mut r) => {
            let csp = r.headers().get("content-security-policy").and_then(|v| v.to_str().ok()).map(str::to_string);
            let html = r.body_mut().read_to_string().unwrap_or_default();
            match csp {
                None => c.fail("CSP-header (saknas — publicerades nginx.conf för hand i stället för `cargo xtask publish`?)"),
                Some(csp) => {
                    let live = bootstrap_hash(&html).unwrap_or_else(|| "ingen-inline-bootstrap".to_string());
                    if csp.contains(&live) {
                        c.ok("CSP-header (hash matchar sidan)");
                    } else {
                        c.fail("CSP-hash matchar inte serverad sida (kör `cargo xtask publish`)");
                    }
                    if csp.contains("connect-src 'self'") {
                        c.ok("CSP connect-src 'self' (exfiltreringsspärren)");
                    } else {
                        c.fail("CSP connect-src är svagare än SECURITY.md påstår");
                    }
                }
            }
        }
        Err(e) => c.fail(&format!("CSP-header ({e})")),
    }

    // nginx must answer ONLY on loopback: with host networking a bare `listen 8480`
    // binds every interface and the console becomes reachable on the LAN over plain
    // HTTP, past tailscale serve and its TLS.
    let listeners = tcp_listeners(CONSOLE_PORT)?;
    if !listeners.is_empty() && listeners.iter().all(|a| a == "127.0.0.1") {
        c.ok("nginx lyssnar bara på loopback");
    } else {
        c.fail(&format!("nginx lyssnar utanför loopback ({})", listeners.join(" ")));
    }

    if !c.failed {
        println!("— hela kedjan svarar —");
    }
    Ok(!c.failed)
}

fn printable(bytes: &[u8]) -> String {
    String::from_utf8_lossy(bytes).chars().filter(|c| !c.is_control()).collect()
}

/// tailscaled's LocalAPI `serve-config`, over its unix socket — what `tailscale serve
/// status` asks. Returns the HTTP status.
fn tailscale_serve_config_status() -> Result<u16> {
    let sock = ["/var/run/tailscale/tailscaled.sock", "/run/tailscale/tailscaled.sock"]
        .into_iter()
        .find(|p| Path::new(p).exists())
        .context("tailscaled socket not found")?;
    let mut s = UnixStream::connect(sock).with_context(|| format!("connect {sock}"))?;
    s.set_read_timeout(Some(Duration::from_secs(10)))?;
    s.write_all(b"GET /localapi/v0/serve-config HTTP/1.0\r\nHost: local-tailscaled.sock\r\n\r\n")?;
    let mut head = [0u8; 64];
    let n = s.read(&mut head)?;
    let line = String::from_utf8_lossy(&head[..n]);
    let code = line.split_whitespace().nth(1).context("empty LocalAPI response")?;
    code.parse().with_context(|| format!("LocalAPI status line: {line:?}"))
}

/// The local addresses of every LISTEN socket on `port`, from `/proc/net/tcp{,6}` —
/// `127.0.0.1`, `0.0.0.0`, or an IPv6 hex address.
fn tcp_listeners(port: u16) -> Result<Vec<String>> {
    let want = format!("{port:04X}");
    let mut out = Vec::new();
    for table in ["/proc/net/tcp", "/proc/net/tcp6"] {
        let Ok(text) = fs::read_to_string(table) else { continue };
        for row in text.lines().skip(1) {
            let cols: Vec<&str> = row.split_whitespace().collect();
            // sl  local_address  rem_address  st  …   (st 0A = LISTEN)
            let (Some(local), Some(state)) = (cols.get(1), cols.get(3)) else { continue };
            let Some((addr, p)) = local.rsplit_once(':') else { continue };
            if *state != "0A" || p != want {
                continue;
            }
            out.push(if addr.len() == 8 { ipv4_from_proc_hex(addr)? } else { format!("[{addr}]") });
        }
    }
    Ok(out)
}

/// `/proc/net/tcp` prints an IPv4 address as the host-order u32 in hex.
fn ipv4_from_proc_hex(hex: &str) -> Result<String> {
    let v = u32::from_str_radix(hex, 16)?;
    let b = v.to_le_bytes();
    if cfg!(target_endian = "big") {
        bail!("big-endian /proc/net/tcp decoding is not implemented");
    }
    Ok(format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3]))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_rows_step_checks_for_the_pinned_image_before_it_builds() {
        use nornir_xtask_support::ci::Mode;
        let plan = ci_plan(PathBuf::from("/nonexistent"));
        let rows = plan.steps.iter().find(|s| s.label() == "rows:MVP").expect("the plan has rows:MVP");
        assert!(rows.runs_in(Mode::Full) && !rows.runs_in(Mode::Quick));
        // In process: it asks the engine for the pinned digest and skips out loud when the
        // box lacks it, rather than minting a 2 GB image only to refuse its digest.
        assert!(rows.describe().starts_with("[in-process]"), "{}", rows.describe());
    }

    #[test]
    fn the_bootstrap_hash_is_the_sha256_of_the_inline_module_script() {
        let html = "<html><script type=\"module\">import init from './x.js';init();</script></html>";
        let expect = base64::engine::general_purpose::STANDARD
            .encode(Sha256::digest(b"import init from './x.js';init();"));
        assert_eq!(bootstrap_hash(html).as_deref(), Some(expect.as_str()));
        assert_eq!(bootstrap_hash("<html></html>"), None);
    }

    /// Known answer, pinned on the pre-bump sha2 0.10 (2026-10-03, finish-bump) and
    /// cross-checked with Python's hashlib: the CSP token a published site carries
    /// must not move when sha2 does — a changed token is a bootstrap the browser
    /// refuses to run.
    #[test]
    fn kat_bootstrap_hash_is_byte_identical() {
        let html = "<html><script type=\"module\">import init from './x.js';init();</script></html>";
        assert_eq!(
            bootstrap_hash(html).as_deref(),
            Some("nHi03532VBWLM5lGyrwWhDVJ6vLw757ir1smxOW7UNw=")
        );
    }

    #[test]
    fn every_marker_becomes_the_header_and_a_template_without_one_is_refused() {
        let csp = csp("abc", Some("https://login.microsoftonline.com"));
        assert!(csp.contains("connect-src 'self' https://login.microsoftonline.com;"));
        let conf = render_nginx_conf("a\n    # __CSP__\nb\n    # __CSP__\n", &csp).unwrap();
        assert_eq!(conf.matches("add_header Content-Security-Policy").count(), 2);
        assert!(!conf.contains("__CSP__"));
        assert!(render_nginx_conf("no marker", &csp).is_err());
    }

    #[test]
    fn the_repo_nginx_conf_carries_the_marker_everywhere_headers_are_set() {
        let template = fs::read_to_string(repo_root().join("deploy/nginx.conf")).unwrap();
        assert_eq!(template.matches("# __CSP__").count(), 3);
    }

    #[test]
    fn proc_ipv4_hex_is_host_order() {
        assert_eq!(ipv4_from_proc_hex("0100007F").unwrap(), "127.0.0.1");
        assert_eq!(ipv4_from_proc_hex("00000000").unwrap(), "0.0.0.0");
    }
}