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

nordisk/facett

facett-ops/tests/wheel_verify.rs

//! **RED FIRST for row 1.** `facett_ops::pip::verify_wheel` is the only thing standing
//! between "podman exited 0" and "the wheel we published actually carries the
//! components". A verifier that has only ever been run on a good wheel is a
//! verifier nobody has seen fail — LAW 2 — so every fault it can report is
//! constructed here, as a real zip on disk, and the test asserts THAT SPECIFIC
//! fault comes back.
//!
//! The wheels below are built by hand rather than by python on purpose: the point
//! is to hand the checker bytes that a build tool would never produce, which is
//! precisely the shape a corrupted or half-copied artifact has.

use std::io::Write;
use std::path::{Path, PathBuf};

use base64::Engine as _;
use sha2::{Digest, Sha256};
use facett_ops::pip::{self, WheelFault};

/// A minimal but VALID wasm module: the 4-byte `\0asm` magic + version 1. Small
/// enough to inline, real enough that the magic check passes for the right reason.
const TINY_WASM: &[u8] = b"\0asm\x01\x00\x00\x00";

/// A scratch dir under the session scratchpad — never `/tmp` (that is RAM on oden).
fn scratch(tag: &str) -> PathBuf {
    let base = std::env::var("CARGO_TARGET_TMPDIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| std::env::temp_dir());
    let dir = base.join(format!("wheel-verify-{tag}"));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).expect("create scratch dir");
    dir
}

fn record_line(path: &str, bytes: &[u8]) -> String {
    let digest = Sha256::digest(bytes);
    let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest);
    format!("{path},sha256={b64},{}\n", bytes.len())
}

/// The members a HEALTHY `facett` (components) wheel has. Mutating this list is how
/// each red case is built, so every fault differs from the green case in exactly
/// one way.
fn healthy_members() -> Vec<(String, Vec<u8>)> {
    let mut m: Vec<(String, Vec<u8>)> = vec![
        ("facett/__init__.py".into(), b"ASSETS = 'assets'\n".to_vec()),
        ("facett/streamlit.py".into(), b"import streamlit\n".to_vec()),
        ("facet_wrapped_web/__init__.py".into(), b"from facett import *\n".to_vec()),
        ("facett/assets/facet_wrapped_bg.wasm".into(), TINY_WASM.to_vec()),
        ("facett/assets/facet_wrapped.js".into(), b"export function init(){}\n".to_vec()),
        ("facett/assets/manifest.json".into(), b"{\"components\":[]}\n".to_vec()),
        ("facett/assets/index.html".into(), b"<canvas id=facet></canvas>\n".to_vec()),
        ("facett-0.2.1.dist-info/METADATA".into(), b"Metadata-Version: 2.4\nName: facett\nVersion: 0.2.1\n".to_vec()),
        ("facett-0.2.1.dist-info/WHEEL".into(), b"Wheel-Version: 1.0\n".to_vec()),
    ];
    // Every OTHER required asset (the non-core bundle groups and the routing table,
    // all from the recipe): a tiny real module for a `.wasm`, a line of text else.
    for asset in pip::required_assets() {
        let path = format!("facett/assets/{asset}");
        if !m.iter().any(|(n, _)| *n == path) {
            let bytes = if asset.ends_with(".wasm") { TINY_WASM.to_vec() } else { b"{}\n".to_vec() };
            m.push((path, bytes));
        }
    }
    m
}

/// How a wheel should be broken, if at all.
#[derive(Default)]
struct Damage {
    /// Write the RECORD for these bytes, then store DIFFERENT bytes for this path.
    tamper: Option<(String, Vec<u8>)>,
    /// Store this member but leave it out of RECORD entirely.
    unrecord: Option<String>,
    /// Record this member but do not store it.
    drop_member: Option<String>,
    /// Do not emit a RECORD at all.
    no_record: bool,
    /// Emit a second RECORD under another dist-info.
    second_record: bool,
    /// Lie about a member's size in RECORD (hash stays correct).
    lie_size: Option<String>,
}

fn build_wheel(dir: &Path, file_name: &str, members: Vec<(String, Vec<u8>)>, d: Damage) -> PathBuf {
    let path = dir.join(file_name);
    let f = std::fs::File::create(&path).expect("create wheel");
    let mut zw = zip::ZipWriter::new(f);
    let opts: zip::write::FileOptions<'_, '_, ()> =
        zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);

    let mut record = String::new();
    for (name, bytes) in &members {
        if d.drop_member.as_deref() == Some(name.as_str()) {
            // Recorded below, but never written into the archive.
            record.push_str(&record_line(name, bytes));
            continue;
        }
        let stored: &[u8] = match &d.tamper {
            Some((t, replacement)) if t == name => replacement,
            _ => bytes,
        };
        zw.start_file(name.clone(), opts).expect("start member");
        zw.write_all(stored).expect("write member");

        if d.unrecord.as_deref() == Some(name.as_str()) {
            continue;
        }
        if d.lie_size.as_deref() == Some(name.as_str()) {
            let digest = Sha256::digest(bytes);
            let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest);
            record.push_str(&format!("{name},sha256={b64},{}\n", bytes.len() + 999));
            continue;
        }
        // NOTE: the record is written for the ORIGINAL bytes even when `tamper`
        // stored something else. That is the whole tampered-wheel scenario.
        record.push_str(&record_line(name, bytes));
    }

    if !d.no_record {
        let rec_path = "facett-0.2.1.dist-info/RECORD";
        record.push_str(&format!("{rec_path},,\n"));
        zw.start_file(rec_path, opts).expect("start RECORD");
        zw.write_all(record.as_bytes()).expect("write RECORD");

        if d.second_record {
            let rec2 = "facett-0.1.99.dist-info/RECORD";
            let mut r2 = record.clone();
            r2.push_str(&format!("{rec2},,\n"));
            zw.start_file(rec2, opts).expect("start RECORD 2");
            zw.write_all(r2.as_bytes()).expect("write RECORD 2");
        }
    }

    zw.finish().expect("finish zip");
    path
}

const GOOD_NAME: &str = "facett-0.2.1-py3-none-any.whl";

// ── the GREEN case, so every red below differs by exactly one thing ─────────

#[test]
fn a_healthy_wheel_verifies_and_reports_its_assets() {
    let dir = scratch("healthy");
    let whl = build_wheel(&dir, GOOD_NAME, healthy_members(), Damage::default());

    let report = pip::verify_wheel(&whl).expect("healthy wheel must verify");
    assert_eq!(report.distribution, "facett");
    assert_eq!(report.version, "0.2.1");
    // Every member carries a hash; RECORD itself carries none, by spec.
    let n = healthy_members().len();
    assert_eq!(report.recorded, n, "every non-RECORD member must be hash-checked");
    assert_eq!(report.files.len(), n + 1, "{n} members + RECORD");
    // The report carries SIZES, not a bool — a shrinking wasm is visible.
    assert_eq!(report.asset_len("facet_wrapped_bg.wasm"), Some(TINY_WASM.len() as u64));
    for asset in pip::required_assets() {
        assert!(report.asset_len(&asset).is_some_and(|n| n > 0), "{asset} must be non-empty");
    }
}

// ── RED: the bytes changed after the record was written ────────────────────

#[test]
fn tampered_bytes_are_caught_by_the_record_hash() {
    let dir = scratch("tampered");
    let target = "facett/assets/facet_wrapped_bg.wasm";
    // Same LENGTH, one byte different: a size check alone would pass this.
    let mut evil = TINY_WASM.to_vec();
    let last = evil.len() - 1;
    evil[last] ^= 0xff;
    let whl = build_wheel(
        &dir,
        GOOD_NAME,
        healthy_members(),
        Damage { tamper: Some((target.into(), evil)), ..Default::default() },
    );

    match pip::verify_wheel(&whl) {
        Err(WheelFault::RecordHashMismatch { path, recorded, actual }) => {
            assert_eq!(path, target);
            assert_ne!(recorded, actual, "the fault must carry BOTH hashes to be diagnosable");
        }
        other => panic!("expected RecordHashMismatch, got {other:?}"),
    }
}

// ── RED: a file rides along that RECORD does not list ──────────────────────

#[test]
fn an_unrecorded_member_is_refused() {
    let dir = scratch("unrecorded");
    let target = "facett/assets/manifest.json";
    let whl = build_wheel(
        &dir,
        GOOD_NAME,
        healthy_members(),
        Damage { unrecord: Some(target.into()), ..Default::default() },
    );
    assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::UnrecordedFile(target.into())));
}

// ── RED: RECORD promises a file the archive does not hold ──────────────────

#[test]
fn a_recorded_but_absent_member_is_refused() {
    let dir = scratch("absent");
    let target = "facett/assets/facet_wrapped.js";
    let whl = build_wheel(
        &dir,
        GOOD_NAME,
        healthy_members(),
        Damage { drop_member: Some(target.into()), ..Default::default() },
    );
    assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::MissingFromArchive(target.into())));
}

// ── RED: the size is a lie even though the hash is right ───────────────────

#[test]
fn a_lied_size_is_refused_even_with_a_correct_hash() {
    let dir = scratch("size");
    let target = "facett/__init__.py";
    let whl = build_wheel(
        &dir,
        GOOD_NAME,
        healthy_members(),
        Damage { lie_size: Some(target.into()), ..Default::default() },
    );
    match pip::verify_wheel(&whl) {
        Err(WheelFault::RecordSizeMismatch { path, recorded, actual }) => {
            assert_eq!(path, target);
            assert_eq!(recorded, actual + 999);
        }
        other => panic!("expected RecordSizeMismatch, got {other:?}"),
    }
}

// ── RED: no manifest at all ────────────────────────────────────────────────

#[test]
fn a_wheel_without_a_record_is_refused() {
    let dir = scratch("norecord");
    let whl = build_wheel(
        &dir,
        GOOD_NAME,
        healthy_members(),
        Damage { no_record: true, ..Default::default() },
    );
    assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::NoRecord));
}

#[test]
fn two_records_are_ambiguous_not_a_pass() {
    let dir = scratch("tworecords");
    let whl = build_wheel(
        &dir,
        GOOD_NAME,
        healthy_members(),
        Damage { second_record: true, ..Default::default() },
    );
    match pip::verify_wheel(&whl) {
        Err(WheelFault::AmbiguousRecord(v)) => assert_eq!(v.len(), 2),
        other => panic!("expected AmbiguousRecord, got {other:?}"),
    }
}

// ── RED: the payload is missing, which is the failure this lane exists for ─

#[test]
fn a_wheel_with_no_wasm_asset_is_refused() {
    let dir = scratch("nowasm");
    let target = "facett/assets/facet_wrapped_bg.wasm";
    let members: Vec<_> =
        healthy_members().into_iter().filter(|(n, _)| n != target).collect();
    let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
    assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::MissingAsset(target.into())));
}

#[test]
fn each_required_asset_is_individually_required() {
    // Not one asset — EVERY ONE (each bundle group's three, the loader, the routing table). A guard that only ever checks the wasm would let a
    // wheel ship without its loader page and still call itself green.
    for asset in pip::required_assets() {
        let dir = scratch(&format!("missing-{}", asset.replace('/', "_")));
        let target = format!("facett/assets/{asset}");
        let members: Vec<_> =
            healthy_members().into_iter().filter(|(n, _)| n != &target).collect();
        let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
        assert_eq!(
            pip::verify_wheel(&whl),
            Err(WheelFault::MissingAsset(target.clone())),
            "dropping {asset} must be caught by name"
        );
    }
}

// ── RED: present, recorded, hashing perfectly — and still not a component ──

#[test]
fn a_zero_byte_asset_records_perfectly_and_is_still_refused() {
    // THE identity-value case. An empty file has a valid sha256, a truthful size of
    // 0, and a RECORD line that verifies. "The path exists" cannot tell this from a
    // working bundle; the length can.
    let dir = scratch("empty");
    let target = "facett/assets/facet_wrapped.js";
    let members: Vec<_> = healthy_members()
        .into_iter()
        .map(|(n, b)| if n == target { (n, Vec::new()) } else { (n, b) })
        .collect();
    let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
    assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::EmptyAsset(target.into())));
}

#[test]
fn a_non_wasm_file_wearing_the_wasm_name_is_refused() {
    // The other half of the identity value: non-empty, correctly recorded, and not
    // a wasm module. This is what a failed build that wrote an error page into
    // dist/ looks like from the outside.
    let dir = scratch("notwasm");
    let target = "facett/assets/facet_wrapped_bg.wasm";
    let members: Vec<_> = healthy_members()
        .into_iter()
        .map(|(n, b)| if n == target { (n, b"<!doctype html>\n".to_vec()) } else { (n, b) })
        .collect();
    let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
    match pip::verify_wheel(&whl) {
        Err(WheelFault::NotWasm { first_bytes }) => assert_eq!(first_bytes, b"<!do".to_vec()),
        other => panic!("expected NotWasm, got {other:?}"),
    }
}

// ── RED: not a wheel at all ────────────────────────────────────────────────

#[test]
fn a_non_zip_is_refused_as_not_a_zip() {
    let dir = scratch("notzip");
    let whl = dir.join(GOOD_NAME);
    std::fs::write(&whl, b"this is not a zip archive").unwrap();
    assert!(matches!(pip::verify_wheel(&whl), Err(WheelFault::NotAZip(_))));
}

#[test]
fn a_missing_file_is_refused_as_unreadable() {
    let dir = scratch("missing");
    assert!(matches!(
        pip::verify_wheel(&dir.join(GOOD_NAME)),
        Err(WheelFault::Unreadable(_))
    ));
}

#[test]
fn wheel_names_that_are_not_pep427_are_refused() {
    for bad in ["facet_wrapped_web.whl", "facet_wrapped_web-0.1.19.tar.gz", "-0.1.19-py3-none-any.whl"] {
        assert!(
            matches!(pip::parse_wheel_name(bad), Err(WheelFault::BadWheelName(_))),
            "`{bad}` must not parse as a wheel name"
        );
    }
    assert_eq!(
        pip::parse_wheel_name(GOOD_NAME).unwrap(),
        ("facett".to_string(), "0.2.1".to_string())
    );
}

// ── 0.2.0: the modules, the version, and the DEMO wheel ─────────────────────

#[test]
fn a_components_wheel_without_its_streamlit_glue_or_alias_is_refused() {
    for gone in ["facett/streamlit.py", "facet_wrapped_web/__init__.py", "facett/__init__.py"] {
        let dir = scratch(&format!("nomodule-{}", gone.replace('/', "_")));
        let members: Vec<_> = healthy_members().into_iter().filter(|(n, _)| n != gone).collect();
        let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
        assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::MissingModule(gone.into())), "{gone}");
    }
}

#[test]
fn a_wheel_of_another_version_than_the_recipe_releases_is_refused() {
    let dir = scratch("version");
    let whl = build_wheel(&dir, "facett-0.1.19-py3-none-any.whl", healthy_members(), Damage::default());
    match pip::verify_wheel(&whl) {
        Err(WheelFault::VersionMismatch { wheel, declared }) => {
            assert_eq!(wheel, "0.1.19");
            assert_eq!(declared, facett_build_thing::PYPI_VERSION);
        }
        other => panic!("expected VersionMismatch, got {other:?}"),
    }
}

const DEMO_NAME: &str = "facett_demo-0.2.1-py3-none-any.whl";

/// A healthy `facett-demo` wheel: the required files, the console script, the
/// requirement on `facett[streamlit]` and the optional `korp` extra.
fn healthy_demo(metadata: &str, entry_points: &str) -> Vec<(String, Vec<u8>)> {
    let mut m: Vec<(String, Vec<u8>)> = pip::required_demo_files()
        .into_iter()
        .map(|f| {
            let body = if f.ends_with(".json") { b"{}\n".to_vec() } else { b"x = 1\n".to_vec() };
            (f, body)
        })
        .collect();
    m.push(("facett_demo-0.2.1.dist-info/METADATA".into(), metadata.as_bytes().to_vec()));
    m.push(("facett_demo-0.2.1.dist-info/entry_points.txt".into(), entry_points.as_bytes().to_vec()));
    m
}

const DEMO_METADATA: &str = "Metadata-Version: 2.4\nName: facett-demo\nVersion: 0.2.1\n\
Requires-Dist: facett[streamlit]<0.3,>=0.2.1\nProvides-Extra: korp\n\
Requires-Dist: grpcio>=1.60; extra == \"korp\"\n\n# facett-demo\n";
const DEMO_EPS: &str = "[console_scripts]\nfacett-demo = facett_demo.cli:main\n";

fn demo_wheel(tag: &str, members: Vec<(String, Vec<u8>)>) -> PathBuf {
    // `build_wheel` writes its RECORD under the components' dist-info; the demo needs
    // its own, so the RECORD line is re-pointed by building with a renamed record path.
    let dir = scratch(tag);
    let path = dir.join(DEMO_NAME);
    let f = std::fs::File::create(&path).unwrap();
    let mut zw = zip::ZipWriter::new(f);
    let opts: zip::write::FileOptions<'_, '_, ()> = zip::write::FileOptions::default();
    let mut record = String::new();
    for (name, bytes) in &members {
        zw.start_file(name.clone(), opts).unwrap();
        zw.write_all(bytes).unwrap();
        record.push_str(&record_line(name, bytes));
    }
    let rec = "facett_demo-0.2.1.dist-info/RECORD";
    record.push_str(&format!("{rec},,\n"));
    zw.start_file(rec, opts).unwrap();
    zw.write_all(record.as_bytes()).unwrap();
    zw.finish().unwrap();
    path
}

#[test]
fn a_healthy_demo_wheel_verifies() {
    let whl = demo_wheel("demo-ok", healthy_demo(DEMO_METADATA, DEMO_EPS));
    let r = pip::verify_demo_wheel(&whl).expect("healthy demo wheel");
    assert_eq!(r.distribution, "facett_demo");
    assert_eq!(r.assets.len(), pip::required_demo_files().len());
    assert_eq!(pip::Release::of_wheel(DEMO_NAME), Some(pip::Release::Demo));
    assert_eq!(pip::Release::of_wheel(GOOD_NAME), Some(pip::Release::Components));
}

#[test]
fn a_demo_wheel_missing_its_run_time_data_is_refused() {
    // setuptools leaves non-.py files out unless package-data names them — this is the
    // wheel that installs, starts, and dies on the first gallery read.
    for gone in ["facett_demo/demo_payloads.json", "facett_demo/errcodes.json", "facett_demo/_buildinfo.py"] {
        let members: Vec<_> = healthy_demo(DEMO_METADATA, DEMO_EPS).into_iter().filter(|(n, _)| n != gone).collect();
        let whl = demo_wheel(&format!("demo-{}", gone.replace('/', "_")), members);
        assert_eq!(pip::verify_demo_wheel(&whl), Err(WheelFault::MissingModule(gone.into())), "{gone}");
    }
}

#[test]
fn a_demo_wheel_without_the_console_script_is_refused() {
    let whl = demo_wheel("demo-noep", healthy_demo(DEMO_METADATA, "[console_scripts]\nfacett-demo-python = facett_demo.cli:main\n"));
    match pip::verify_demo_wheel(&whl) {
        Err(WheelFault::MissingEntryPoint { want, found }) => {
            assert_eq!(want, "facett-demo = facett_demo.cli:main");
            assert!(found.contains("facett-demo-python"), "{found}");
        }
        other => panic!("expected MissingEntryPoint, got {other:?}"),
    }
}

#[test]
fn a_demo_wheel_whose_components_requirement_is_only_an_extra_is_refused() {
    // `facett[streamlit]` behind `extra == ...` installs a demo with no components.
    let md = DEMO_METADATA.replace(
        "Requires-Dist: facett[streamlit]<0.3,>=0.2.1\n",
        "Requires-Dist: facett[streamlit]<0.3,>=0.2.1; extra == \"full\"\n",
    );
    let whl = demo_wheel("demo-noreq", healthy_demo(&md, DEMO_EPS));
    assert!(matches!(pip::verify_demo_wheel(&whl), Err(WheelFault::MissingRequirement(r)) if r.contains("facett[streamlit]")));
}

#[test]
fn a_demo_wheel_without_the_korp_extra_is_refused() {
    let md = DEMO_METADATA.replace("Provides-Extra: korp\n", "");
    let whl = demo_wheel("demo-nokorp", healthy_demo(&md, DEMO_EPS));
    assert!(matches!(pip::verify_demo_wheel(&whl), Err(WheelFault::MissingRequirement(r)) if r.contains("korp")));
}