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");
}
}