Files
Kelorastus b7f3061be3 ODIAN Launcher: hub, installer, local stack and remembered sign-in (M2)
- Tauri 2 + Svelte 5 hub in ODIAN dark: patch notes and roadmap, install and
  update progress, local stack status, settings with DHP and MCP addresses.
- crates/odian-packs: Rust installer for pack format v1 (signed manifests,
  resumable verified downloads from a DHP or an ODIAN drive), byte-compatible
  with ODIAN_MCP_Server's Python odian_packs.
- Local stack on 127.0.0.1: Postgres -> Engine API -> tile server -> Odian.exe,
  one Job Object, clean shutdown; database snapshot restore on a fresh PC only.
- Keycloak sign-in through the DHP's MCP, remembered until Sign out with a
  launcher session that renews the Engine token silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 15:23:17 +02:00

208 lines
9.4 KiB
Rust

//! Cross-language fixtures: every expected value was computed by the Python reference
//! implementation (`tests/fixtures/gen_fixtures.py`); Rust must reproduce or verify it.
use std::path::PathBuf;
use base64::Engine;
use ed25519_dalek::{Signer, SigningKey};
use odian_packs::hashing::{hex, sha256_hex};
use odian_packs::manifest::{chunk_count, chunks_digest, signing_payload};
use odian_packs::signing::load_public_key;
use odian_packs::tiles::{block_digest, block_of, index_sha256, tile_line, verify_index, TileRow};
use odian_packs::{bundled_public_key, canonical_bytes, safe_relpath, validate, Error, IntegrityKind, VerifiedManifest};
use serde_json::Value;
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("fixtures").join(name)
}
fn read(name: &str) -> Vec<u8> {
std::fs::read(fixture_path(name)).unwrap_or_else(|e| panic!("fixture {name}: {e}"))
}
fn load(name: &str) -> Value {
serde_json::from_slice(&read(name)).unwrap()
}
fn s<'a>(v: &'a Value, key: &str) -> &'a str {
v[key].as_str().unwrap_or_else(|| panic!("{key} is not a string in {v}"))
}
fn is_signature_error(result: &Result<VerifiedManifest, Error>) -> bool {
matches!(result, Err(Error::Integrity { kind: IntegrityKind::Signature, .. }))
}
#[test]
fn canonical_bytes_match_python() {
let doc = load("canonical.json");
let cases = doc["cases"].as_array().unwrap();
assert!(cases.len() >= 30);
for case in cases {
let value: Value = serde_json::from_str(s(case, "input")).unwrap();
let got = hex(&canonical_bytes(&value).unwrap());
assert_eq!(got, s(case, "canonical_hex"), "case {}", s(case, "name"));
}
}
#[test]
fn canonical_refuses_non_integer_numbers() {
let doc = load("canonical.json");
for case in doc["rust_rejects"].as_array().unwrap() {
let value: Value = serde_json::from_str(s(case, "input")).unwrap();
assert!(
matches!(canonical_bytes(&value), Err(Error::Manifest(_))),
"case {} should be refused",
s(case, "name")
);
}
}
#[test]
fn signed_manifests_verify_like_python() {
let doc = load("signing.json");
let public = s(&doc, "public_key");
let cases = doc["cases"].as_array().unwrap();
assert_eq!(cases.len(), 5);
for case in cases {
let file = s(case, "file");
let result = VerifiedManifest::from_bytes(read(file), public);
match s(case, "python") {
"ok" => {
let verified = result.unwrap_or_else(|e| panic!("{file}: {e}"));
assert_eq!(verified.raw(), read(file).as_slice(), "raw bytes are kept verbatim");
assert_eq!(verified.version(), "0.51.0");
}
_ => assert!(is_signature_error(&result), "{file}: expected a signature error, got {result:?}"),
}
}
// Signed by a different key.
assert_eq!(s(&doc, "signed_with_other_key_verdict"), "error");
let other = VerifiedManifest::from_bytes(read("signed_manifest.utf8.json"), s(&doc, "other_public_key"));
assert!(is_signature_error(&other));
}
#[test]
fn signing_payload_and_signature_match_python() {
let doc = load("signing.json");
let signed = load("signed_manifest.utf8.json");
let payload = signing_payload(&signed).unwrap();
assert_eq!(hex(&payload), s(&doc, "payload_hex"));
// Ed25519 is deterministic: the same key over the same payload gives Python's signature.
let raw: [u8; 32] = (0u8..32).collect::<Vec<_>>().try_into().unwrap();
assert_eq!(hex(&raw), s(&doc, "private_key_raw_hex"));
let key = SigningKey::from_bytes(&raw);
let signature = base64::engine::general_purpose::STANDARD.encode(key.sign(&payload).to_bytes());
assert_eq!(format!("ed25519:{signature}"), s(&doc, "signature"));
let public = base64::engine::general_purpose::STANDARD.encode(key.verifying_key().to_bytes());
assert_eq!(format!("ed25519:{public}"), s(&doc, "public_key"));
}
#[test]
fn public_key_text_rules() {
let doc = load("signing.json");
let public = s(&doc, "public_key");
assert!(load_public_key(&format!(" {public}\n")).is_ok());
for bad in ["", "rsa:AAAA", "ed25519:AAAA", "ed25519:not base64!", &public.replace("ed25519:", "")] {
assert!(load_public_key(bad).is_err(), "{bad:?}");
}
}
#[test]
fn relpaths_match_python() {
let doc = load("relpaths.json");
let cases = doc["cases"].as_array().unwrap();
assert!(cases.len() >= 60);
for case in cases {
let path = s(case, "path");
assert_eq!(safe_relpath(path).is_ok(), s(case, "python") == "ok", "path {path:?}: python {}", s(case, "python_error"));
}
}
#[test]
fn validate_matches_python_except_documented_quirks() {
let doc = load("validate.json");
let cases = doc["cases"].as_array().unwrap();
let mut quirks = 0;
for case in cases {
let name = s(case, "name");
let value: Value = serde_json::from_str(s(case, "manifest_json")).unwrap();
// The installer's parse path: canonical form (no floats) first, then validate.
let rust = canonical_bytes(&value).and_then(|_| validate(&value));
let expect_ok = s(case, "rust_expect") == "ok";
assert_eq!(rust.is_ok(), expect_ok, "case {name}: rust {rust:?}, python {}", s(case, "python_error"));
if !case["quirk"].is_null() {
quirks += 1;
assert_eq!(s(case, "python"), "ok", "a quirk case is one Python accepts: {name}");
}
}
// The Python library was brought to parity (anchors, ASCII digits, integers and floats),
// so a regenerated fixture must show no case where the two validators disagree.
assert!(cases.len() >= 50 && quirks == 0, "{} cases, {quirks} quirks", cases.len());
}
#[test]
fn hashes_match_python() {
let doc = load("hashes.json");
let row = |v: &Value| -> TileRow {
(v[0].as_u64().unwrap(), v[1].as_u64().unwrap(), v[2].as_u64().unwrap(), v[3].as_str().unwrap().into(),
v[4].as_u64().unwrap(), v[5].as_str().unwrap().into())
};
let rows: Vec<TileRow> = doc["block_digest"]["rows"].as_array().unwrap().iter().map(row).collect();
assert_eq!(block_digest(&rows), s(&doc["block_digest"], "digest"));
assert_eq!(block_digest(&[]), s(&doc, "block_digest_empty"));
for line in doc["tile_lines"].as_array().unwrap() {
let (z, x, y, ext, size, sha) = row(&line["row"]);
assert_eq!(tile_line(z, x, y, &ext, size, &sha), s(line, "line"));
}
for case in doc["block_of"].as_array().unwrap() {
let n: Vec<u64> = case.as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
assert_eq!(block_of(n[0], n[1], n[2]), (n[3], n[4], n[5]));
}
let chunks: Vec<String> = serde_json::from_value(doc["chunks_digest"]["chunks"].clone()).unwrap();
assert_eq!(chunks_digest(&chunks), s(&doc["chunks_digest"], "digest"));
assert_eq!(chunks_digest::<&str>(&[]), s(&doc, "chunks_digest_empty"));
for case in doc["chunk_count"].as_array().unwrap() {
let n: Vec<u64> = case.as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
assert_eq!(chunk_count(n[0], n[1]), n[2]);
}
let index = &doc["index"];
let text = s(index, "text");
assert_eq!(index_sha256(&serde_json::from_str(text).unwrap()).unwrap(), s(index, "index_sha256"));
let parsed = verify_index(text.as_bytes(), s(index, "index_sha256"), "fixture index").unwrap();
assert_eq!(parsed.blocks.len(), 2);
let wrong = verify_index(text.as_bytes(), &"0".repeat(64), "fixture index");
assert!(matches!(wrong, Err(Error::Integrity { kind: IntegrityKind::IndexHash, .. })));
}
#[test]
fn real_published_manifest_verifies_with_the_bundled_key() {
let hashes = load("real/0.51.0.hashes.json");
let raw = read("real/0.51.0.manifest.json");
assert_eq!(raw.len() as u64, hashes["raw_len"].as_u64().unwrap());
assert_eq!(sha256_hex(&raw), s(&hashes, "raw_sha256"));
assert_eq!(bundled_public_key(), s(&hashes, "release_public_key"));
let verified = VerifiedManifest::from_bytes(raw.clone(), bundled_public_key()).expect("the real release verifies");
assert_eq!(verified.version(), s(&hashes, "version"));
let canonical = canonical_bytes(verified.value()).unwrap();
assert_eq!(canonical.len() as u64, hashes["canonical_len"].as_u64().unwrap());
assert_eq!(sha256_hex(&canonical), s(&hashes, "canonical_sha256"));
assert_eq!(sha256_hex(&signing_payload(verified.value()).unwrap()), s(&hashes, "payload_sha256"));
let m = verified.manifest();
assert_eq!(m.app.files.len() as u64, hashes["app_files"].as_u64().unwrap());
assert!(m.db.is_some() && m.tiles.is_some() && m.src.as_ref().is_some_and(|s| s.enabled));
// One changed character anywhere breaks it.
let text = String::from_utf8(raw).unwrap();
let released = verified.manifest().released_at.clone();
let mut bumped = released.clone().into_bytes();
let last_digit = bumped.iter().rposition(u8::is_ascii_digit).unwrap();
bumped[last_digit] = if bumped[last_digit] == b'9' { b'0' } else { bumped[last_digit] + 1 };
let tampered = text.replacen(&released, std::str::from_utf8(&bumped).unwrap(), 1);
assert!(is_signature_error(&VerifiedManifest::from_bytes(tampered.into_bytes(), bundled_public_key())));
// And the test key does not vouch for it.
let test_key = load("signing.json");
assert!(is_signature_error(&VerifiedManifest::from_bytes(read("real/0.51.0.manifest.json"), s(&test_key, "public_key"))));
}