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>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
//! 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"))));
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
//! End to end against a local stub site: `tests/stub_site.py` builds a small signed pack
|
||||
//! store with the Python reference code (odian_packs) and serves it with the real update
|
||||
//! service (`odian_update.api.create_app`) plus nginx-like Range routes. The same store is
|
||||
//! also installed as an ODIAN drive.
|
||||
//!
|
||||
//! These tests spawn Python, so they are `#[ignore]`d by default. They need Python 3 with
|
||||
//! fastapi, uvicorn, httpx and cryptography, and the ODIAN_MCP_Server sources:
|
||||
//!
|
||||
//! ```text
|
||||
//! ODIAN_PACKS_PYTHONPATH=C:/Users/JFEXP_D/Documents/Gitea/ODIAN_MCP_Server/src \
|
||||
//! cargo test -p odian-packs --test e2e_site -- --ignored
|
||||
//! ```
|
||||
//!
|
||||
//! `ODIAN_PACKS_PYTHON` picks the interpreter (default `python`).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use odian_packs::hashing::sha256_hex;
|
||||
use odian_packs::{
|
||||
bundled_public_key, current_manifest, fetch_notes, install, version_manifest, Error, InstallOptions, IntegrityKind,
|
||||
Progress, Source, Stage, Summary,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
const CHUNK: usize = 1024 * 1024;
|
||||
const NOTES: &str = "# ODIAN 0.51.0\n\nPremier pack signé.\n";
|
||||
|
||||
fn python() -> String {
|
||||
std::env::var("ODIAN_PACKS_PYTHON").unwrap_or_else(|_| "python".into())
|
||||
}
|
||||
|
||||
fn pythonpath() -> String {
|
||||
std::env::var("ODIAN_PACKS_PYTHONPATH")
|
||||
.expect("ODIAN_PACKS_PYTHONPATH must point at ODIAN_MCP_Server/src (see the header of tests/e2e_site.rs)")
|
||||
}
|
||||
|
||||
struct Stub {
|
||||
child: Child,
|
||||
stdin: Option<ChildStdin>,
|
||||
_stdout: BufReader<ChildStdout>,
|
||||
info: Value,
|
||||
dir: tempfile::TempDir,
|
||||
}
|
||||
|
||||
impl Stub {
|
||||
fn start(args: &[&str]) -> Stub {
|
||||
let dir = tempfile::Builder::new().prefix("odian-packs-e2e-").tempdir().unwrap();
|
||||
let log_path = dir.path().join("stub.log");
|
||||
let log = std::fs::File::create(&log_path).unwrap();
|
||||
let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("stub_site.py");
|
||||
let mut child = Command::new(python())
|
||||
.arg(script)
|
||||
.arg("--root")
|
||||
.arg(dir.path().join("store"))
|
||||
.args(args)
|
||||
.env("PYTHONPATH", pythonpath())
|
||||
.env("PYTHONDONTWRITEBYTECODE", "1")
|
||||
.env("PYTHONIOENCODING", "utf-8")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(log)
|
||||
.spawn()
|
||||
.expect("spawn python");
|
||||
let stdin = child.stdin.take();
|
||||
let mut stdout = BufReader::new(child.stdout.take().unwrap());
|
||||
let mut line = String::new();
|
||||
let _ = stdout.read_line(&mut line);
|
||||
if line.trim().is_empty() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
panic!("stub site did not start:\n{}", std::fs::read_to_string(&log_path).unwrap_or_default());
|
||||
}
|
||||
let info: Value = serde_json::from_str(&line).unwrap();
|
||||
Stub { child, stdin, _stdout: stdout, info, dir }
|
||||
}
|
||||
|
||||
fn base(&self) -> String {
|
||||
self.info["base_url"].as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn root(&self) -> PathBuf {
|
||||
PathBuf::from(self.info["root"].as_str().unwrap())
|
||||
}
|
||||
|
||||
fn key(&self) -> String {
|
||||
self.info["public_key"].as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn site(&self) -> Source {
|
||||
Source::Site { base_url: self.base(), token: "good".into() }
|
||||
}
|
||||
|
||||
fn drive(&self) -> Source {
|
||||
Source::Drive { root: self.root() }
|
||||
}
|
||||
|
||||
fn dest(&self, name: &str) -> PathBuf {
|
||||
self.dir.path().join(name)
|
||||
}
|
||||
|
||||
fn store_manifest_path(&self) -> PathBuf {
|
||||
self.root().join("packs").join("versions").join("0.51.0").join("manifest.json")
|
||||
}
|
||||
|
||||
fn store_manifest(&self) -> Value {
|
||||
serde_json::from_slice(&std::fs::read(self.store_manifest_path()).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn app_sha(&self, install_path: &str) -> String {
|
||||
let m = self.store_manifest();
|
||||
let entry = m["app"]["files"].as_array().unwrap().iter().find(|f| f["path"] == install_path).unwrap();
|
||||
entry["sha256"].as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn app_blob(&self, install_path: &str) -> PathBuf {
|
||||
self.root().join("packs").join("app").join(self.app_sha(install_path))
|
||||
}
|
||||
|
||||
async fn ctl(&self, path: &str) -> Value {
|
||||
let text = reqwest::get(format!("{}{}", self.base(), path)).await.unwrap().text().await.unwrap();
|
||||
serde_json::from_str(&text).unwrap()
|
||||
}
|
||||
|
||||
async fn requests(&self) -> Vec<(String, Option<String>)> {
|
||||
let log = self.ctl("/_ctl/log").await;
|
||||
log["requests"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| (r["path"].as_str().unwrap().to_string(), r["range"].as_str().map(str::to_string)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Stub {
|
||||
fn drop(&mut self) {
|
||||
drop(self.stdin.take()); // the stub exits when its stdin closes
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
|
||||
fn only(stages: &[Stage]) -> InstallOptions {
|
||||
InstallOptions { only: stages.to_vec(), ..InstallOptions::default() }
|
||||
}
|
||||
|
||||
async fn install_from(source: &Source, dest: &Path, key: &str, opts: InstallOptions) -> Result<Summary, Error> {
|
||||
let manifest = current_manifest(source, key).await?;
|
||||
install(source, dest, &manifest, opts, |_| {}, CancellationToken::new()).await
|
||||
}
|
||||
|
||||
/// Every file under `root`: relative path -> sha256.
|
||||
fn files(root: &Path) -> BTreeMap<String, String> {
|
||||
fn walk(dir: &Path, prefix: &str, out: &mut BTreeMap<String, String>) {
|
||||
for entry in std::fs::read_dir(dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
let rel = if prefix.is_empty() { name } else { format!("{prefix}/{name}") };
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
walk(&entry.path(), &rel, out);
|
||||
} else {
|
||||
out.insert(rel, sha256_hex(&std::fs::read(entry.path()).unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = BTreeMap::new();
|
||||
walk(root, "", &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn flip(path: &Path, offset: usize) {
|
||||
let mut data = std::fs::read(path).unwrap();
|
||||
data[offset] ^= 0xFF;
|
||||
std::fs::write(path, data).unwrap();
|
||||
}
|
||||
|
||||
fn integrity(result: Result<Summary, Error>, expected: IntegrityKind) -> String {
|
||||
match result {
|
||||
Err(Error::Integrity { kind, what, detail }) if kind == expected => format!("{what}: {detail}"),
|
||||
other => panic!("expected an {expected:?} integrity error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_payload(path: &str) -> bool {
|
||||
path.starts_with("/packs/") || path.starts_with("/src/") || path.contains("/block/")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn site_install_verifies_everything_then_rerun_downloads_nothing() {
|
||||
let stub = Stub::start(&[]);
|
||||
let (site, key, dest) = (stub.site(), stub.key(), stub.dest("install"));
|
||||
let manifest = current_manifest(&site, &key).await.unwrap();
|
||||
assert_eq!(manifest.raw(), std::fs::read(stub.store_manifest_path()).unwrap().as_slice());
|
||||
|
||||
let events: Arc<Mutex<Vec<Progress>>> = Arc::default();
|
||||
let sink = events.clone();
|
||||
let summary = install(&site, &dest, &manifest, InstallOptions::default(), move |p| sink.lock().unwrap().push(p), CancellationToken::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let app = summary.app.clone().unwrap();
|
||||
assert_eq!((app.files, app.downloaded, app.skipped), (5, 5, 0));
|
||||
let db = summary.db.clone().unwrap();
|
||||
assert_eq!((db.files, db.downloaded, db.skipped), (3, 3, 0));
|
||||
let src: Vec<(String, usize)> = summary.src.clone().unwrap().into_iter().map(|s| (s.path, s.chunks)).collect();
|
||||
assert_eq!(src, vec![
|
||||
("osm_source/world_aeroway_v1.sqlite".to_string(), 3),
|
||||
("osm_source/small.bin".to_string(), 1),
|
||||
("osm_source/empty.bin".to_string(), 0),
|
||||
]);
|
||||
let tiles = summary.tiles.clone().unwrap();
|
||||
assert_eq!((tiles[0].blocks, tiles[0].downloaded, tiles[0].files), (4, 4, 8));
|
||||
assert!(summary.transferred_bytes > 3 * CHUNK as u64);
|
||||
|
||||
// Every byte in place, in the fetch.py layout.
|
||||
let store = stub.root();
|
||||
let m = stub.store_manifest();
|
||||
let version_dir = dest.join("versions").join("0.51.0");
|
||||
for f in m["app"]["files"].as_array().unwrap() {
|
||||
let path = version_dir.join(f["path"].as_str().unwrap());
|
||||
assert_eq!(sha256_hex(&std::fs::read(&path).unwrap()), f["sha256"].as_str().unwrap(), "{}", path.display());
|
||||
}
|
||||
assert_eq!(std::fs::metadata(version_dir.join("Odian/assets/empty.conf")).unwrap().len(), 0);
|
||||
for f in m["db"]["files"].as_array().unwrap() {
|
||||
let path = dest.join("data").join(f["path"].as_str().unwrap());
|
||||
assert_eq!(sha256_hex(&std::fs::read(&path).unwrap()), f["sha256"].as_str().unwrap());
|
||||
}
|
||||
for f in m["src"]["files"].as_array().unwrap() {
|
||||
let rel = f["path"].as_str().unwrap();
|
||||
let path = dest.join("data").join("src").join(rel);
|
||||
assert_eq!(std::fs::read(&path).unwrap(), std::fs::read(store.join("src").join(rel)).unwrap());
|
||||
let marker = std::fs::read_to_string(format!("{}.ok", path.display())).unwrap();
|
||||
assert_eq!(marker, f["chunks_sha256"].as_str().unwrap());
|
||||
assert!(!Path::new(&format!("{}.chunks.json", path.display())).exists());
|
||||
}
|
||||
assert_eq!(std::fs::read(version_dir.join("manifest.json")).unwrap(), manifest.raw());
|
||||
let set_dir = dest.join("data/tiles/dark/test_ds");
|
||||
let mut tile_files = files(&set_dir);
|
||||
let done = tile_files.remove(".blocks-done.json").map(|_| std::fs::read_to_string(set_dir.join(".blocks-done.json")).unwrap()).unwrap();
|
||||
let expected_tiles = ["0/0/0.png", "3/1/2.png", "7/1/2.png", "7/127/63.png", "7/3/2.jpg", "7/3/2.png", "7/63/63.pbf", "7/64/5.png"];
|
||||
assert_eq!(tile_files.keys().map(String::as_str).collect::<Vec<_>>(), expected_tiles, "the late tile 7/5/5.png must not be installed");
|
||||
for rel in expected_tiles {
|
||||
assert_eq!(tile_files[rel], sha256_hex(&std::fs::read(store.join("tiles/dark/test_ds").join(rel)).unwrap()));
|
||||
}
|
||||
assert!(done.starts_with("{\"index_sha256\": \""), "Python's json.dumps layout: {done}");
|
||||
let done: Value = serde_json::from_str(&done).unwrap();
|
||||
assert_eq!(done["index_sha256"], m["tiles"]["sets"][0]["index_sha256"]);
|
||||
assert_eq!(done["blocks"].as_array().unwrap().len(), 4);
|
||||
|
||||
// No request for the zero-byte file: it never needs the network.
|
||||
let log = stub.requests().await;
|
||||
assert!(!log.iter().any(|(p, _)| p.ends_with(&stub.app_sha("Odian/assets/empty.conf"))));
|
||||
|
||||
// Progress: stages in order, ending at their totals, contract-shaped JSON.
|
||||
let events = events.lock().unwrap().clone();
|
||||
let mut stages: Vec<Stage> = events.iter().map(|p| p.stage).collect();
|
||||
stages.dedup();
|
||||
assert_eq!(stages, vec![Stage::Manifest, Stage::App, Stage::Db, Stage::Src, Stage::Tiles]);
|
||||
assert!(events.iter().all(|p| p.stage_count == 5));
|
||||
for stage in [Stage::App, Stage::Db, Stage::Src, Stage::Tiles] {
|
||||
let last = events.iter().rev().find(|p| p.stage == stage).unwrap();
|
||||
assert_eq!((last.done_bytes, last.done_items), (last.total_bytes, last.total_items), "{stage:?}");
|
||||
}
|
||||
assert!(events.iter().any(|p| p.message.as_deref() == Some("tiles dark/test_ds: 4/4 blocks")));
|
||||
let json = serde_json::to_value(&events[0]).unwrap();
|
||||
assert_eq!(json["stage"], "manifest");
|
||||
for field in ["stage_index", "stage_count", "done_bytes", "total_bytes", "done_items", "total_items", "bytes_per_sec", "message"] {
|
||||
assert!(json.get(field).is_some(), "{field}");
|
||||
}
|
||||
|
||||
// Re-run: nothing is downloaded again.
|
||||
stub.ctl("/_ctl/reset").await;
|
||||
let again = install(&site, &dest, &manifest, InstallOptions::default(), |_| {}, CancellationToken::new()).await.unwrap();
|
||||
assert_eq!(again.transferred_bytes, 0);
|
||||
assert_eq!(again.app.unwrap().downloaded, 0);
|
||||
assert_eq!(again.db.unwrap().downloaded, 0);
|
||||
assert!(again.src.unwrap().iter().all(|s| s.chunks == 0));
|
||||
assert_eq!(again.tiles.unwrap()[0].downloaded, 0);
|
||||
let log = stub.requests().await;
|
||||
assert!(log.iter().all(|(p, _)| !is_payload(p)), "{log:?}");
|
||||
|
||||
assert_eq!(fetch_notes(&site, "0.51.0").await.unwrap(), NOTES);
|
||||
assert_eq!(version_manifest(&site, "0.51.0", &key).await.unwrap().raw(), manifest.raw());
|
||||
assert!(matches!(version_manifest(&site, "9.9.9", &key).await, Err(Error::NotFound { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn wrong_token_is_unauthorized_and_edited_manifest_is_a_signature_error() {
|
||||
let stub = Stub::start(&[]);
|
||||
let key = stub.key();
|
||||
let bad = Source::Site { base_url: stub.base(), token: "bad".into() };
|
||||
assert!(matches!(current_manifest(&bad, &key).await, Err(Error::Unauthorized { status: 401, .. })));
|
||||
|
||||
let manifest = current_manifest(&stub.site(), &key).await.unwrap();
|
||||
let dest = stub.dest("install");
|
||||
let result = install(&bad, &dest, &manifest, only(&[Stage::App]), |_| {}, CancellationToken::new()).await;
|
||||
assert!(matches!(result, Err(Error::Unauthorized { .. })), "{result:?}");
|
||||
assert!(!dest.join("versions/0.51.0/manifest.json").exists());
|
||||
|
||||
// The stub's manifest is signed by the test key, not the release key.
|
||||
let release = current_manifest(&stub.site(), bundled_public_key()).await;
|
||||
assert!(matches!(release, Err(Error::Integrity { kind: IntegrityKind::Signature, .. })));
|
||||
|
||||
let path = stub.store_manifest_path();
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
let edited = text.replacen("\"released_at\": \"2026-09-15T12:00:00Z\"", "\"released_at\": \"2026-09-15T12:00:01Z\"", 1);
|
||||
assert_ne!(text, edited);
|
||||
std::fs::write(&path, edited).unwrap();
|
||||
let result = current_manifest(&stub.site(), &key).await;
|
||||
assert!(matches!(result, Err(Error::Integrity { kind: IntegrityKind::Signature, .. })), "{result:?}");
|
||||
|
||||
std::fs::remove_file(stub.root().join("packs/versions/CURRENT")).unwrap();
|
||||
assert!(matches!(current_manifest(&stub.site(), &key).await, Err(Error::NotFound { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn flipped_app_byte_is_an_integrity_error() {
|
||||
let stub = Stub::start(&[]);
|
||||
let (site, key, dest) = (stub.site(), stub.key(), stub.dest("install"));
|
||||
let manifest = current_manifest(&site, &key).await.unwrap();
|
||||
let sha = stub.app_sha("Odian/Odian.exe");
|
||||
flip(&stub.app_blob("Odian/Odian.exe"), 1234);
|
||||
stub.ctl("/_ctl/reset").await;
|
||||
|
||||
let detail = integrity(install(&site, &dest, &manifest, only(&[Stage::App]), |_| {}, CancellationToken::new()).await, IntegrityKind::FileHash);
|
||||
assert!(detail.contains(&sha), "{detail}");
|
||||
let gets = stub.requests().await.iter().filter(|(p, _)| p.ends_with(&sha)).count();
|
||||
assert_eq!(gets, 2, "one clean retry, then give up");
|
||||
let target = dest.join("versions/0.51.0/Odian/Odian.exe");
|
||||
assert!(!target.exists() && !Path::new(&format!("{}.part", target.display())).exists());
|
||||
assert!(!dest.join("versions/0.51.0/manifest.json").exists());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn flipped_tile_byte_is_an_integrity_error() {
|
||||
let stub = Stub::start(&[]);
|
||||
let (site, key, dest) = (stub.site(), stub.key(), stub.dest("install"));
|
||||
flip(&stub.root().join("tiles/dark/test_ds/7/63/63.pbf"), 100);
|
||||
|
||||
let detail = integrity(install_from(&site, &dest, &key, only(&[Stage::Tiles])).await, IntegrityKind::BlockDigest);
|
||||
assert!(detail.contains("block/7/0/0.tar"), "{detail}");
|
||||
let set_dir = dest.join("data/tiles/dark/test_ds");
|
||||
assert!(!set_dir.join("7/63/63.pbf").exists() && !set_dir.join("7/1/2.png").exists(), "nothing of a bad block moves in");
|
||||
if let Ok(done) = std::fs::read_to_string(set_dir.join(".blocks-done.json")) {
|
||||
assert!(!done.contains("\"7/0/0:"), "{done}");
|
||||
}
|
||||
assert!(!dest.join("versions/0.51.0/manifest.json").exists());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn flipped_source_chunk_byte_is_an_integrity_error_and_the_rest_resumes() {
|
||||
let stub = Stub::start(&[]);
|
||||
let (site, key, dest) = (stub.site(), stub.key(), stub.dest("install"));
|
||||
let original = stub.root().join("src/osm_source/world_aeroway_v1.sqlite");
|
||||
flip(&original, CHUNK + 5);
|
||||
|
||||
let detail = integrity(install_from(&site, &dest, &key, only(&[Stage::Src])).await, IntegrityKind::ChunkHash);
|
||||
assert!(detail.contains("chunk 1"), "{detail}");
|
||||
let target = dest.join("data/src/osm_source/world_aeroway_v1.sqlite");
|
||||
assert!(!Path::new(&format!("{}.ok", target.display())).exists());
|
||||
let progress: Value = serde_json::from_str(&std::fs::read_to_string(format!("{}.chunks.json", target.display())).unwrap()).unwrap();
|
||||
let verified: Vec<u64> = progress["verified"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
||||
assert!(!verified.contains(&1), "{verified:?}");
|
||||
|
||||
flip(&original, CHUNK + 5); // the site is fixed
|
||||
stub.ctl("/_ctl/reset").await;
|
||||
let summary = install_from(&site, &dest, &key, only(&[Stage::Src])).await.unwrap();
|
||||
let world = &summary.src.unwrap()[0];
|
||||
assert_eq!(world.chunks + world.skipped, 3);
|
||||
assert_eq!(world.skipped, verified.len());
|
||||
let ranges: Vec<String> = stub
|
||||
.requests()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(p, _)| p.ends_with("world_aeroway_v1.sqlite"))
|
||||
.map(|(_, r)| r.unwrap())
|
||||
.collect();
|
||||
assert_eq!(ranges.len(), world.chunks);
|
||||
assert!(ranges.contains(&format!("bytes={}-{}", CHUNK, 2 * CHUNK - 1)), "{ranges:?}");
|
||||
assert_eq!(std::fs::read(&target).unwrap(), std::fs::read(&original).unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn interrupted_part_resumes_with_a_range_request() {
|
||||
let stub = Stub::start(&[]);
|
||||
let (site, key) = (stub.site(), stub.key());
|
||||
let manifest = current_manifest(&site, &key).await.unwrap();
|
||||
let sha = stub.app_sha("Odian/data/big.bin");
|
||||
let big = std::fs::read(stub.app_blob("Odian/data/big.bin")).unwrap();
|
||||
let app_total: u64 = manifest.manifest().app.files.iter().map(|f| f.size).sum();
|
||||
|
||||
// A .part left by an earlier run (Python or Rust).
|
||||
let dest = stub.dest("seeded");
|
||||
let target = dest.join("versions/0.51.0/Odian/data/big.bin");
|
||||
let part = PathBuf::from(format!("{}.part", target.display()));
|
||||
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
|
||||
let have = CHUNK + 1000;
|
||||
std::fs::write(&part, &big[..have]).unwrap();
|
||||
stub.ctl("/_ctl/reset").await;
|
||||
let summary = install(&site, &dest, &manifest, only(&[Stage::App]), |_| {}, CancellationToken::new()).await.unwrap();
|
||||
assert_eq!(summary.app.unwrap().downloaded, 5);
|
||||
assert_eq!(summary.transferred_bytes, app_total - have as u64);
|
||||
let log = stub.requests().await;
|
||||
let big_requests: Vec<_> = log.iter().filter(|(p, _)| p.ends_with(&sha)).collect();
|
||||
assert_eq!(big_requests.len(), 1);
|
||||
assert_eq!(big_requests[0].1.as_deref(), Some(format!("bytes={have}-").as_str()));
|
||||
assert_eq!(std::fs::read(&target).unwrap(), big);
|
||||
assert!(!part.exists());
|
||||
|
||||
// A connection that really drops mid-file.
|
||||
let dest = stub.dest("cut");
|
||||
let target = dest.join("versions/0.51.0/Odian/data/big.bin");
|
||||
let part = PathBuf::from(format!("{}.part", target.display()));
|
||||
stub.ctl(&format!("/_ctl/cut?prefix=/packs/app/{sha}&after=700000&count=1")).await;
|
||||
let result = install(&site, &dest, &manifest, only(&[Stage::App]), |_| {}, CancellationToken::new()).await;
|
||||
assert!(matches!(result, Err(Error::Http { .. })), "{result:?}");
|
||||
let have = std::fs::metadata(&part).unwrap().len();
|
||||
assert!(have > 0 && have <= 700_000, "{have}");
|
||||
stub.ctl("/_ctl/reset").await;
|
||||
install(&site, &dest, &manifest, only(&[Stage::App]), |_| {}, CancellationToken::new()).await.unwrap();
|
||||
let log = stub.requests().await;
|
||||
let big_requests: Vec<_> = log.iter().filter(|(p, _)| p.ends_with(&sha)).collect();
|
||||
assert_eq!(big_requests.len(), 1);
|
||||
assert_eq!(big_requests[0].1.as_deref(), Some(format!("bytes={have}-").as_str()));
|
||||
assert_eq!(std::fs::read(&target).unwrap(), big);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn busy_site_is_retried_with_retry_after() {
|
||||
let stub = Stub::start(&[]);
|
||||
let (site, key) = (stub.site(), stub.key());
|
||||
stub.ctl("/_ctl/reset").await;
|
||||
stub.ctl("/_ctl/fail?prefix=/v1/versions/current&status=503&count=2&retry_after=0").await;
|
||||
current_manifest(&site, &key).await.unwrap();
|
||||
let tries = stub.requests().await.iter().filter(|(p, _)| p == "/v1/versions/current").count();
|
||||
assert_eq!(tries, 3);
|
||||
|
||||
stub.ctl("/_ctl/fail?prefix=/v1/versions/current&status=503&count=4&retry_after=0").await;
|
||||
let result = current_manifest(&site, &key).await;
|
||||
assert!(matches!(result, Err(Error::SiteBusy { attempts: 4, .. })), "{result:?}");
|
||||
|
||||
// A 502 on a tile block and a 504 on an app file are retried too.
|
||||
stub.ctl("/_ctl/fail?prefix=/v1/versions/0.51.0/tiles/dark/test_ds/block/&status=502&count=1&retry_after=0").await;
|
||||
stub.ctl("/_ctl/fail?prefix=/packs/app/&status=504&count=1&retry_after=0").await;
|
||||
let summary = install_from(&site, &stub.dest("install"), &key, only(&[Stage::App, Stage::Tiles])).await.unwrap();
|
||||
assert_eq!(summary.tiles.unwrap()[0].downloaded, 4);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn cancel_stops_and_keeps_verified_work() {
|
||||
let stub = Stub::start(&[]);
|
||||
let (site, key, dest) = (stub.site(), stub.key(), stub.dest("install"));
|
||||
let manifest = current_manifest(&site, &key).await.unwrap();
|
||||
let cancel = CancellationToken::new();
|
||||
let trigger = cancel.clone();
|
||||
let result = install(&site, &dest, &manifest, InstallOptions::default(), move |p| {
|
||||
if p.stage == Stage::App && p.done_items >= 1 {
|
||||
trigger.cancel();
|
||||
}
|
||||
}, cancel)
|
||||
.await;
|
||||
assert!(matches!(result, Err(Error::Cancelled)), "{result:?}");
|
||||
assert!(!dest.join("versions/0.51.0/manifest.json").exists());
|
||||
|
||||
let summary = install(&site, &dest, &manifest, InstallOptions::default(), |_| {}, CancellationToken::new()).await.unwrap();
|
||||
assert!(summary.app.unwrap().skipped >= 1);
|
||||
assert!(dest.join("versions/0.51.0/manifest.json").exists());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn drive_install_equals_site_install() {
|
||||
let stub = Stub::start(&[]);
|
||||
let (site, drive, key) = (stub.site(), stub.drive(), stub.key());
|
||||
let from_site = stub.dest("from-site");
|
||||
install_from(&site, &from_site, &key, InstallOptions::default()).await.unwrap();
|
||||
|
||||
let manifest = current_manifest(&drive, &key).await.unwrap();
|
||||
assert_eq!(manifest.raw(), std::fs::read(stub.store_manifest_path()).unwrap().as_slice());
|
||||
let from_drive = stub.dest("from-drive");
|
||||
let summary = install(&drive, &from_drive, &manifest, InstallOptions::default(), |_| {}, CancellationToken::new()).await.unwrap();
|
||||
assert_eq!(summary.app.unwrap().downloaded, 5);
|
||||
assert_eq!(summary.tiles.unwrap()[0].downloaded, 4);
|
||||
assert_eq!(files(&from_site), files(&from_drive), "site and drive installs must be identical, file by file");
|
||||
|
||||
let again = install(&drive, &from_drive, &manifest, InstallOptions::default(), |_| {}, CancellationToken::new()).await.unwrap();
|
||||
assert_eq!(again.transferred_bytes, 0);
|
||||
assert_eq!(fetch_notes(&drive, "0.51.0").await.unwrap(), NOTES);
|
||||
assert_eq!(version_manifest(&drive, "0.51.0", &key).await.unwrap().raw(), manifest.raw());
|
||||
|
||||
// The same checks guard a drive.
|
||||
let tile = stub.root().join("tiles/dark/test_ds/7/64/5.png");
|
||||
flip(&tile, 3);
|
||||
let detail = integrity(install_from(&drive, &stub.dest("drive-tile"), &key, only(&[Stage::Tiles])).await, IntegrityKind::BlockDigest);
|
||||
assert!(detail.contains("7/1/0"), "{detail}");
|
||||
flip(&tile, 3);
|
||||
flip(&stub.app_blob("api/odian_api.exe"), 7);
|
||||
integrity(install_from(&drive, &stub.dest("drive-app"), &key, only(&[Stage::App])).await, IntegrityKind::FileHash);
|
||||
}
|
||||
|
||||
fn python_fetch(stub: &Stub, dest: &Path) -> Value {
|
||||
let code = "import json, sys\n\
|
||||
from odian_packs.fetch import fetch\n\
|
||||
summary = fetch(sys.argv[1], 'good', sys.argv[2], public_key=sys.argv[3], log=lambda m: None)\n\
|
||||
print(json.dumps(summary))\n";
|
||||
let output = Command::new(python())
|
||||
.arg("-c")
|
||||
.arg(code)
|
||||
.arg(stub.base())
|
||||
.arg(dest)
|
||||
.arg(stub.key())
|
||||
.env("PYTHONPATH", pythonpath())
|
||||
.env("PYTHONDONTWRITEBYTECODE", "1")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "python fetch failed:\n{}", String::from_utf8_lossy(&output.stderr));
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
serde_json::from_str(stdout.lines().last().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[ignore = "spawns Python: set ODIAN_PACKS_PYTHONPATH and run with --ignored (see the file header)"]
|
||||
async fn python_and_rust_installs_are_interchangeable() {
|
||||
// fetch.py cannot install a zero-byte source file (see the report), so leave it out here.
|
||||
let stub = Stub::start(&["--no-empty-src"]);
|
||||
let (site, key) = (stub.site(), stub.key());
|
||||
|
||||
let python_first = stub.dest("python-first");
|
||||
let py = tokio::task::block_in_place(|| python_fetch(&stub, &python_first));
|
||||
assert_eq!(py["app"]["downloaded"], 5);
|
||||
let rust = install_from(&site, &python_first, &key, InstallOptions::default()).await.unwrap();
|
||||
assert_eq!(rust.transferred_bytes, 0, "Rust re-verifies a Python install and downloads nothing: {rust:?}");
|
||||
assert_eq!(rust.app.unwrap().skipped, 5);
|
||||
|
||||
let rust_first = stub.dest("rust-first");
|
||||
install_from(&site, &rust_first, &key, InstallOptions::default()).await.unwrap();
|
||||
let py = tokio::task::block_in_place(|| python_fetch(&stub, &rust_first));
|
||||
assert_eq!(py["app"]["downloaded"], 0, "{py}");
|
||||
assert_eq!(py["db"]["downloaded"], 0, "{py}");
|
||||
assert!(py["src"].as_array().unwrap().iter().all(|s| s["chunks"] == 0), "{py}");
|
||||
assert_eq!(py["tiles"][0]["downloaded"], 0, "{py}");
|
||||
|
||||
assert_eq!(files(&python_first), files(&rust_first));
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# Fixtures are compared byte for byte with what Python produced: never convert line endings.
|
||||
* -text
|
||||
+211
File diff suppressed because one or more lines are too long
+373
@@ -0,0 +1,373 @@
|
||||
"""Generate the cross-language fixtures the odian-packs Rust tests read.
|
||||
|
||||
PYTHONPATH=C:/Users/JFEXP_D/Documents/Gitea/ODIAN_MCP_Server/src \
|
||||
python crates/odian-packs/tests/fixtures/gen_fixtures.py [--real-manifest PATH]
|
||||
|
||||
Every expected value below is computed by the reference implementation (odian_packs);
|
||||
the Rust tests must reproduce or verify each one. Outputs sit next to this script:
|
||||
|
||||
canonical.json canonical bytes of tricky JSON inputs (+ inputs Rust refuses)
|
||||
signing.json test key, verdicts; signed/tampered manifests as separate files
|
||||
relpaths.json safe_relpath verdicts
|
||||
validate.json validate() verdicts, with the Python quirks Rust deliberately rejects
|
||||
hashes.json block_of, block_digest, chunks_digest, chunk_count, index_sha256
|
||||
real/0.51.0.* the real published 0.51.0 manifest and its Python hashes
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
from odian_packs import manifest as mf # noqa: E402
|
||||
from odian_packs import signing, tiles # noqa: E402
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
DEFAULT_REAL = (r"C:\Users\JFEXP_D\AppData\Local\Temp\claude\C--Users-JFEXP-D-Documents-Gitea"
|
||||
r"\d92cce4d-3852-472e-a7e3-10137a35c218\scratchpad\m1\packs\versions\0.51.0\manifest.json")
|
||||
|
||||
|
||||
def out_path(name):
|
||||
path = os.path.join(HERE, *name.split("/"))
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def write_bytes(name, data):
|
||||
with open(out_path(name), "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
def write_json(name, obj):
|
||||
# ASCII-only fixture files: no editor or checkout can change their bytes.
|
||||
write_bytes(name, (json.dumps(obj, indent=1, ensure_ascii=True) + "\n").encode("ascii"))
|
||||
|
||||
|
||||
def sha(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def reversed_keys(obj):
|
||||
if isinstance(obj, dict):
|
||||
return {k: reversed_keys(obj[k]) for k in reversed(list(obj))}
|
||||
if isinstance(obj, list):
|
||||
return [reversed_keys(v) for v in obj]
|
||||
return obj
|
||||
|
||||
|
||||
def verdict(fn):
|
||||
try:
|
||||
fn()
|
||||
return "ok", ""
|
||||
except Exception as exc: # noqa: BLE001 - any exception is a refusal
|
||||
return "error", f"{type(exc).__name__}: {exc}"
|
||||
|
||||
|
||||
# ---- canonical JSON -------------------------------------------------------------
|
||||
|
||||
def minimal_manifest():
|
||||
return {"schema": 1, "product": "odian", "version": "0.51.0", "released_at": "2026-09-15T12:00:00Z",
|
||||
"app": {"files": [{"path": "Odian/Odian.exe", "size": 3, "sha256": sha(b"exe")}]}}
|
||||
|
||||
|
||||
def canonical_fixture():
|
||||
control = "".join(chr(i) for i in range(0x20)) + "\x7f\x80\x9f\u2028\u2029"
|
||||
nested = "leaf"
|
||||
for i in range(40):
|
||||
nested = {f"k{i % 3}": [nested, i]} if i % 2 else [nested, {"i": i}]
|
||||
values = [
|
||||
("empty_object", {}),
|
||||
("empty_array", []),
|
||||
("empty_string", ""),
|
||||
("null", None),
|
||||
("true", True),
|
||||
("false", False),
|
||||
("empty_containers_nested", {"b": [], "a": {}, "c": [[], {}], "d": ""}),
|
||||
("non_ascii", {"fr": "données été à Noël", "de": "Straße", "zh": "中文路径", "ar": "مرحبا", "ru": "Привет"}),
|
||||
("emoji", {"emoji": "😀🚀 and 👨\u200d👩\u200d👧 zwj", "flag": "🇫🇷"}),
|
||||
("control_chars", {"all": control}),
|
||||
("quotes_backslashes", {"q": 'say "hi"', "b": "C:\\Odian\\x", "mixed": "\\\"\\n/<>&'"}),
|
||||
("key_order_code_points", {"\ufb01": 1, "😀": 2, "a": 3, "B": 4, "": 5, "é": 6, "aa": 7, "a\u0000": 8,
|
||||
"\uffff": 9, "Z": 10, "\u00ff": 11}),
|
||||
("nested_sorting", {"z": {"y": [{"b": [], "a": {}}, [], {}], "x": None}, "a": [True, False, None, "", 0],
|
||||
"m": {"k2": {"b": 1, "a": 2}, "k1": 3}}),
|
||||
("big_ints", {"n": [0, -1, 1, 2**31, 2**32, 2**53, 2**53 + 1, 2**63 - 1, -2**63, 2**64 - 1, 2437523419]}),
|
||||
("deep_nesting", nested),
|
||||
("minimal_manifest", minimal_manifest()),
|
||||
]
|
||||
cases = []
|
||||
for name, value in values:
|
||||
for style, text in (
|
||||
("ascii_escaped_reversed", json.dumps(reversed_keys(value), indent=2, ensure_ascii=True)),
|
||||
("raw_utf8", json.dumps(value, ensure_ascii=False)),
|
||||
):
|
||||
assert json.loads(text) == value
|
||||
cases.append({"name": f"{name}/{style}", "input": text,
|
||||
"canonical_hex": mf.canonical_bytes(json.loads(text)).hex()})
|
||||
rejects = []
|
||||
for name, text in (("float", "1.5"), ("float_integral", "1.0"), ("exponent", "1e3"), ("negative_zero", "-0"),
|
||||
("negative_zero_float", "-0.0"), ("above_u64", "18446744073709551616"),
|
||||
("below_i64", "-9223372036854775809"), ("float_in_object", '{"a": 2.5}'),
|
||||
("float_in_array", "[1, 0.1]")):
|
||||
rejects.append({"name": name, "input": text,
|
||||
"python_canonical_hex": mf.canonical_bytes(json.loads(text)).hex()})
|
||||
write_json("canonical.json", {"cases": cases, "rust_rejects": rejects})
|
||||
|
||||
|
||||
# ---- signing ---------------------------------------------------------------------
|
||||
|
||||
def full_manifest():
|
||||
chunks = [sha(b"c0"), sha(b"c1")]
|
||||
return {
|
||||
"schema": 1, "product": "odian", "version": "0.51.0", "released_at": "2026-09-15T12:00:00Z",
|
||||
"min_launcher": "0.1.0", "notes": "notes.md",
|
||||
"app": {"files": [
|
||||
{"path": "Odian/Odian.exe", "size": 3, "sha256": sha(b"exe")},
|
||||
{"path": "Odian/assets/empty.conf", "size": 0, "sha256": sha(b"")},
|
||||
{"path": "Odian/données/carte été.png", "size": 5, "sha256": sha(b"carte")},
|
||||
]},
|
||||
"db": {"schema_version": 0, "snapshot": "20260915T101500Z", "files": [
|
||||
{"section": "pre-data", "path": "db/20260915T101500Z/pre-data.dump", "size": 1, "sha256": sha(b"p")},
|
||||
{"section": "data", "table": "public.t1", "path": "db/20260915T101500Z/data/public.t1.dump",
|
||||
"size": 1, "sha256": sha(b"d")},
|
||||
]},
|
||||
"tiles": {"block_shift": 6, "sets": [
|
||||
{"style": "dark", "dataset": "ds", "journal_seq": 9, "blocks": 2, "files": 3, "bytes": 2437523419,
|
||||
"index": "tiles/dark.ds.index.json", "index_sha256": "a" * 64},
|
||||
]},
|
||||
"src": {"enabled": True, "chunk_bytes": 10, "files": [
|
||||
{"path": "osm/x.sqlite", "size": 15, "chunks": chunks, "chunks_sha256": mf.chunks_digest(chunks)},
|
||||
]},
|
||||
"extra_note": "unknown keys are signed too: \"quoted\" \\ \u0001 😀",
|
||||
}
|
||||
|
||||
|
||||
def signing_fixture():
|
||||
raw_key = bytes(range(32))
|
||||
key = signing.private_key_from_raw(raw_key)
|
||||
public = signing.public_key_text(key.public_key())
|
||||
other = signing.public_key_text(signing.private_key_from_raw(bytes(range(1, 33))).public_key())
|
||||
signed = signing.sign(mf.validate(full_manifest()), key)
|
||||
|
||||
tampered = copy.deepcopy(signed)
|
||||
tampered["app"]["files"][0]["size"] = 4
|
||||
flipped = copy.deepcopy(signed)
|
||||
sig = bytearray(base64.b64decode(signed["signature"][len("ed25519:"):]))
|
||||
sig[0] ^= 0xFF
|
||||
flipped["signature"] = "ed25519:" + base64.b64encode(bytes(sig)).decode("ascii")
|
||||
unsigned = {k: v for k, v in signed.items() if k != "signature"}
|
||||
|
||||
files = {
|
||||
"signed_manifest.utf8.json": (json.dumps(signed, indent=2, sort_keys=True, ensure_ascii=False) + "\n").encode("utf-8"),
|
||||
"signed_manifest.ascii.json": (json.dumps(signed, indent=2, sort_keys=True) + "\n").encode("ascii"),
|
||||
"tampered_manifest.json": (json.dumps(tampered, indent=2, sort_keys=True) + "\n").encode("ascii"),
|
||||
"signature_flipped_manifest.json": (json.dumps(flipped, indent=2, sort_keys=True) + "\n").encode("ascii"),
|
||||
"unsigned_manifest.json": (json.dumps(unsigned, indent=2, sort_keys=True) + "\n").encode("ascii"),
|
||||
}
|
||||
cases = []
|
||||
for name, data in files.items():
|
||||
write_bytes(name, data)
|
||||
doc = json.loads(data.decode("utf-8"))
|
||||
ok, error = verdict(lambda: (mf.validate(doc), signing.verify(doc, public)))
|
||||
cases.append({"file": name, "python": ok, "python_error": error})
|
||||
other_ok, _ = verdict(lambda: signing.verify(signed, other))
|
||||
write_json("signing.json", {
|
||||
"private_key_raw_hex": raw_key.hex(),
|
||||
"public_key": public,
|
||||
"other_public_key": other,
|
||||
"signed_with_other_key_verdict": other_ok,
|
||||
"signature": signed["signature"],
|
||||
"payload_hex": mf.signing_payload(signed).hex(),
|
||||
"cases": cases,
|
||||
})
|
||||
|
||||
|
||||
# ---- safe_relpath -----------------------------------------------------------------
|
||||
|
||||
RELPATHS = [
|
||||
"Odian/x.exe", "db/20260915T082529Z/data/public.odb_air_timeline_2026_02.dump",
|
||||
"../a", "a/../b", "/a", "C:/a", "a\\b", "a//b", "", "./a", "a/", "a/.", ".", "..", "a/..",
|
||||
"Odian/Odian.exe:hidden", "a/b.", "a/b ", "CON", "con", "a/CoN.TxT", "a/aux.txt", "a/Lpt1.log", "NUL.tar.gz",
|
||||
"a/nul..x", "a/prn", "a/prn_x", "a/com¹", "a/COM².txt", "a/lpt³", "a/lpt0", "a/com0", "a/comA", "a/com10",
|
||||
"a/con .txt", "a/b\x00c", "a/b\nc", "a/b\x1fc", "a/b\x7fc", "a/b\u0085c", "a/b\u2028c", "a/b?c", "a/b*c",
|
||||
'a/b"c', "a/b|c", "a/b<c", "a/b>c", "a/console.txt", "a/auxiliary", "a/.hidden", "a/b c.txt", " a/b",
|
||||
"a/ b", "a/b.c.", "données/été.png", "中文/路径.txt", "a/😀.png", "a/b#c%20d", "a/aux\u00a0",
|
||||
"a/com\u0660", "a/\u212aon",
|
||||
]
|
||||
|
||||
|
||||
def relpath_fixture():
|
||||
cases = []
|
||||
for path in RELPATHS:
|
||||
ok, error = verdict(lambda: mf.safe_relpath(path))
|
||||
cases.append({"path": path, "python": ok, "python_error": error})
|
||||
write_json("relpaths.json", {"cases": cases})
|
||||
|
||||
|
||||
# ---- validate ---------------------------------------------------------------------
|
||||
|
||||
def validate_fixture():
|
||||
def app0(m):
|
||||
return m["app"]["files"][0]
|
||||
|
||||
def setp(fn):
|
||||
def mutate(m):
|
||||
fn(m)
|
||||
return m
|
||||
return mutate
|
||||
|
||||
empty_src = {"path": "e.bin", "size": 0, "chunks": [], "chunks_sha256": mf.chunks_digest([])}
|
||||
# (name, mutation, reason Rust deliberately differs from Python, or None)
|
||||
mutations = [
|
||||
("valid_full", lambda m: m, None),
|
||||
("valid_minimal", lambda m: minimal_manifest(), None),
|
||||
("no_app_section", setp(lambda m: m.pop("app")), None),
|
||||
("app_null", setp(lambda m: m.update(app=None)), None),
|
||||
("app_files_missing", setp(lambda m: m.update(app={})), None),
|
||||
("app_entry_not_object", setp(lambda m: m.update(app={"files": [None]})), None),
|
||||
("db_entry_not_object", setp(lambda m: m["db"].update(files=[1])), None),
|
||||
("tiles_set_not_object", setp(lambda m: m["tiles"].update(sets=[1])), None),
|
||||
("src_file_not_object", setp(lambda m: m["src"].update(files=["x"])), None),
|
||||
("manifest_not_object", lambda m: [1], None),
|
||||
("schema_2", setp(lambda m: m.update(schema=2)), None),
|
||||
("schema_true", setp(lambda m: m.update(schema=True)),
|
||||
"Python's `== 1` accepts True; Rust wants the integer 1"),
|
||||
("schema_float", setp(lambda m: m.update(schema=1.0)),
|
||||
"Python's `== 1` accepts 1.0; Rust refuses floats anywhere"),
|
||||
("product_wrong", setp(lambda m: m.update(product="other")), None),
|
||||
("version_two_parts", setp(lambda m: m.update(version="1.0")), None),
|
||||
("version_leading_zero", setp(lambda m: m.update(version="01.2.3")), None),
|
||||
("version_int", setp(lambda m: m.update(version=5)), None),
|
||||
("version_missing", setp(lambda m: m.pop("version")), None),
|
||||
("version_trailing_newline", setp(lambda m: m.update(version="0.51.0\n")),
|
||||
"Python's regex `$` matches before a final newline; Rust refuses it"),
|
||||
("version_unicode_digit", setp(lambda m: m.update(version="0.51.\u0663")),
|
||||
"Python's `\\d` matches any Unicode digit; Rust takes ASCII digits only"),
|
||||
("released_at_int", setp(lambda m: m.update(released_at=5)), None),
|
||||
("min_launcher_bad", setp(lambda m: m.update(min_launcher="0.1")), None),
|
||||
("notes_traversal", setp(lambda m: m.update(notes="../notes.md")), None),
|
||||
("notes_int", setp(lambda m: m.update(notes=5)), None),
|
||||
("sha_upper", setp(lambda m: app0(m).update(sha256="A" * 64)), None),
|
||||
("sha_short", setp(lambda m: app0(m).update(sha256="a" * 63)), None),
|
||||
("sha_trailing_newline", setp(lambda m: app0(m).update(sha256="a" * 64 + "\n")),
|
||||
"Python's regex `$` matches before a final newline; Rust refuses it"),
|
||||
("dup_app_path_case", setp(lambda m: m["app"]["files"].append({**app0(m), "path": "ODIAN/odian.EXE"})), None),
|
||||
("dup_app_path_unicode_case", setp(lambda m: m["app"]["files"].extend([
|
||||
{"path": "Odian/Ä.txt", "size": 1, "sha256": "b" * 64},
|
||||
{"path": "odian/ä.txt", "size": 1, "sha256": "c" * 64}])), None),
|
||||
("app_path_traversal", setp(lambda m: app0(m).update(path="../evil.exe")), None),
|
||||
("size_negative", setp(lambda m: app0(m).update(size=-1)), None),
|
||||
("size_bool", setp(lambda m: app0(m).update(size=True)), None),
|
||||
("size_float", setp(lambda m: app0(m).update(size=3.0)), None),
|
||||
("size_string", setp(lambda m: app0(m).update(size="3")), None),
|
||||
("size_above_u64", setp(lambda m: app0(m).update(size=2**64)),
|
||||
"Rust numbers are 64-bit; a size above u64::MAX is refused"),
|
||||
("db_empty_files", setp(lambda m: m["db"].update(files=[])), None),
|
||||
("db_bad_section", setp(lambda m: m["db"]["files"][0].update(section="schema")), None),
|
||||
("db_data_without_table", setp(lambda m: m["db"]["files"][1].pop("table")), None),
|
||||
("db_data_empty_table", setp(lambda m: m["db"]["files"][1].update(table="")), None),
|
||||
("db_path_outside_snapshot", setp(lambda m: m["db"]["files"][0].update(path="db/20260101T000000Z/x.dump")), None),
|
||||
("db_snapshot_bad", setp(lambda m: m["db"].update(snapshot="2026-09-15")), None),
|
||||
("db_schema_version_bool", setp(lambda m: m["db"].update(schema_version=True)), None),
|
||||
("tiles_block_shift_7", setp(lambda m: m["tiles"].update(block_shift=7)), None),
|
||||
("tiles_block_shift_float", setp(lambda m: m["tiles"].update(block_shift=6.0)),
|
||||
"Python's `== 6` accepts 6.0; Rust refuses floats anywhere"),
|
||||
("tiles_bad_style", setp(lambda m: m["tiles"]["sets"][0].update(style="da rk")), None),
|
||||
("tiles_style_trailing_newline", setp(lambda m: m["tiles"]["sets"][0].update(
|
||||
style="dark\n", index="tiles/dark\n.ds.index.json")),
|
||||
"Python's regex `$` matches before a final newline; Rust refuses it"),
|
||||
("tiles_dup_set", setp(lambda m: m["tiles"]["sets"].append(dict(m["tiles"]["sets"][0]))), None),
|
||||
("tiles_wrong_index_name", setp(lambda m: m["tiles"]["sets"][0].update(index="tiles/other.json")), None),
|
||||
("tiles_count_negative", setp(lambda m: m["tiles"]["sets"][0].update(journal_seq=-1)), None),
|
||||
("src_enabled_int", setp(lambda m: m["src"].update(enabled=1)), None),
|
||||
("src_chunk_bytes_zero", setp(lambda m: m["src"].update(chunk_bytes=0)), None),
|
||||
("src_chunk_count_wrong", setp(lambda m: m["src"]["files"][0].update(size=25)), None),
|
||||
("src_chunks_sha_wrong", setp(lambda m: m["src"]["files"][0].update(chunks_sha256="c" * 64)), None),
|
||||
("src_empty_file", setp(lambda m: m["src"]["files"].append(empty_src)), None),
|
||||
("signature_not_ed25519", setp(lambda m: m.update(signature="rsa:xx")), None),
|
||||
("signature_int", setp(lambda m: m.update(signature=5)), None),
|
||||
("unknown_keys", setp(lambda m: m.update(future={"x": [1, "y"]})), None),
|
||||
("unknown_float_key", setp(lambda m: m.update(ratio=0.5)),
|
||||
"Rust refuses floats anywhere in a manifest, even under keys validate() ignores"),
|
||||
]
|
||||
cases = []
|
||||
for name, mutate, quirk in mutations:
|
||||
doc = mutate(copy.deepcopy(full_manifest()))
|
||||
ok, error = verdict(lambda: mf.validate(doc))
|
||||
if ok == "error":
|
||||
quirk = None # Python refuses it as well: not a difference
|
||||
rust = "error" if quirk else ok
|
||||
cases.append({"name": name, "manifest_json": json.dumps(doc, ensure_ascii=True), "python": ok,
|
||||
"python_error": error, "rust_expect": rust, "quirk": quirk})
|
||||
write_json("validate.json", {"cases": cases})
|
||||
|
||||
|
||||
# ---- hashes -----------------------------------------------------------------------
|
||||
|
||||
def hashes_fixture():
|
||||
rows = [(7, 63, 5, ".png", 13, sha(b"b")), (7, 0, 0, ".png", 13, sha(b"a")), (7, 0, 0, ".jpg", 9, sha(b"c")),
|
||||
(7, 10, 2, ".png", 700, sha(b"d")), (7, 2, 10, ".png", 1, sha(b"e"))]
|
||||
chunks = [sha(b"x"), sha(b"y"), sha(b"z")]
|
||||
index = {"format": 1, "style": "dark", "dataset": "ds", "block_shift": 6, "journal_seq": 376157, "files": 3,
|
||||
"bytes": 2437523419, "blocks": [
|
||||
{"z": 0, "bx": 0, "by": 0, "files": 1, "bytes": 5321, "digest": sha(b"b0")},
|
||||
{"z": 7, "bx": 1, "by": 0, "files": 2, "bytes": 2437518098, "digest": sha(b"b1")}]}
|
||||
write_json("hashes.json", {
|
||||
"block_digest": {"rows": [list(r) for r in rows], "digest": tiles.block_digest(rows)},
|
||||
"block_digest_empty": tiles.block_digest([]),
|
||||
"tile_lines": [{"row": list(r), "line": tiles.tile_line(*r).decode("ascii")} for r in rows],
|
||||
"block_of": [[z, x, y, *tiles.block_of(z, x, y)] for z, x, y in
|
||||
((0, 0, 0), (7, 63, 63), (7, 64, 0), (20, 1048575, 65), (3, 7, 7))],
|
||||
"chunks_digest": {"chunks": chunks, "digest": mf.chunks_digest(chunks)},
|
||||
"chunks_digest_empty": mf.chunks_digest([]),
|
||||
"chunk_count": [[size, cb, mf.chunk_count(size, cb)] for size, cb in
|
||||
((0, 10), (1, 10), (10, 10), (11, 10), (884244480, mf.CHUNK_BYTES), (2**40, mf.CHUNK_BYTES))],
|
||||
"index": {"text": json.dumps(index, indent=2, sort_keys=True) + "\n", "index_sha256": tiles.index_sha256(index)},
|
||||
})
|
||||
|
||||
|
||||
# ---- the real published manifest ---------------------------------------------------
|
||||
|
||||
def real_fixture(source):
|
||||
target = out_path("real/0.51.0.manifest.json")
|
||||
if source and os.path.isfile(source):
|
||||
shutil.copyfile(source, target)
|
||||
with open(target, "rb") as f:
|
||||
raw = f.read()
|
||||
doc = json.loads(raw.decode("utf-8"))
|
||||
mf.validate(doc)
|
||||
signing.verify(doc, signing.bundled_public_key())
|
||||
canonical = mf.canonical_bytes(doc)
|
||||
write_json("real/0.51.0.hashes.json", {
|
||||
"version": doc["version"],
|
||||
"raw_len": len(raw),
|
||||
"raw_sha256": sha(raw),
|
||||
"canonical_len": len(canonical),
|
||||
"canonical_sha256": sha(canonical),
|
||||
"payload_sha256": sha(mf.signing_payload(doc)),
|
||||
"release_public_key": signing.bundled_public_key(),
|
||||
"app_files": len(doc["app"]["files"]),
|
||||
})
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--real-manifest", default=DEFAULT_REAL)
|
||||
args = parser.parse_args()
|
||||
canonical_fixture()
|
||||
signing_fixture()
|
||||
relpath_fixture()
|
||||
validate_fixture()
|
||||
hashes_fixture()
|
||||
real_fixture(args.real_manifest)
|
||||
print(f"fixtures written to {HERE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"block_digest": {
|
||||
"rows": [
|
||||
[
|
||||
7,
|
||||
63,
|
||||
5,
|
||||
".png",
|
||||
13,
|
||||
"3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d"
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
".png",
|
||||
13,
|
||||
"ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb"
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
".jpg",
|
||||
9,
|
||||
"2e7d2c03a9507ae265ecf5b5356885a53393a2029d241394997265a1a25aefc6"
|
||||
],
|
||||
[
|
||||
7,
|
||||
10,
|
||||
2,
|
||||
".png",
|
||||
700,
|
||||
"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4"
|
||||
],
|
||||
[
|
||||
7,
|
||||
2,
|
||||
10,
|
||||
".png",
|
||||
1,
|
||||
"3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea"
|
||||
]
|
||||
],
|
||||
"digest": "57db4d13b815dbc73ae79fd3ed093fe6c735abafd1898526db086332592f3838"
|
||||
},
|
||||
"block_digest_empty": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"tile_lines": [
|
||||
{
|
||||
"row": [
|
||||
7,
|
||||
63,
|
||||
5,
|
||||
".png",
|
||||
13,
|
||||
"3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d"
|
||||
],
|
||||
"line": "7/63/5.png 13 3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d\n"
|
||||
},
|
||||
{
|
||||
"row": [
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
".png",
|
||||
13,
|
||||
"ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb"
|
||||
],
|
||||
"line": "7/0/0.png 13 ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb\n"
|
||||
},
|
||||
{
|
||||
"row": [
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
".jpg",
|
||||
9,
|
||||
"2e7d2c03a9507ae265ecf5b5356885a53393a2029d241394997265a1a25aefc6"
|
||||
],
|
||||
"line": "7/0/0.jpg 9 2e7d2c03a9507ae265ecf5b5356885a53393a2029d241394997265a1a25aefc6\n"
|
||||
},
|
||||
{
|
||||
"row": [
|
||||
7,
|
||||
10,
|
||||
2,
|
||||
".png",
|
||||
700,
|
||||
"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4"
|
||||
],
|
||||
"line": "7/10/2.png 700 18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\n"
|
||||
},
|
||||
{
|
||||
"row": [
|
||||
7,
|
||||
2,
|
||||
10,
|
||||
".png",
|
||||
1,
|
||||
"3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea"
|
||||
],
|
||||
"line": "7/2/10.png 1 3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea\n"
|
||||
}
|
||||
],
|
||||
"block_of": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
63,
|
||||
63,
|
||||
7,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
64,
|
||||
0,
|
||||
7,
|
||||
1,
|
||||
0
|
||||
],
|
||||
[
|
||||
20,
|
||||
1048575,
|
||||
65,
|
||||
20,
|
||||
16383,
|
||||
1
|
||||
],
|
||||
[
|
||||
3,
|
||||
7,
|
||||
7,
|
||||
3,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"chunks_digest": {
|
||||
"chunks": [
|
||||
"2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
|
||||
"a1fce4363854ff888cff4b8e7875d600c2682390412a8cf79b37d0b11148b0fa",
|
||||
"594e519ae499312b29433b7dd8a97ff068defcba9755b6d5d00e84c524d67b06"
|
||||
],
|
||||
"digest": "71d8fb1b613dcdc24ac90f01cab91b1c9cb56fd4b40a73862cf87f7636b63c31"
|
||||
},
|
||||
"chunks_digest_empty": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"chunk_count": [
|
||||
[
|
||||
0,
|
||||
10,
|
||||
0
|
||||
],
|
||||
[
|
||||
1,
|
||||
10,
|
||||
1
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
1
|
||||
],
|
||||
[
|
||||
11,
|
||||
10,
|
||||
2
|
||||
],
|
||||
[
|
||||
884244480,
|
||||
268435456,
|
||||
4
|
||||
],
|
||||
[
|
||||
1099511627776,
|
||||
268435456,
|
||||
4096
|
||||
]
|
||||
],
|
||||
"index": {
|
||||
"text": "{\n \"block_shift\": 6,\n \"blocks\": [\n {\n \"bx\": 0,\n \"by\": 0,\n \"bytes\": 5321,\n \"digest\": \"c02c0b965e023abee808f2b548d8d5193a8b5229be6f3121a6f16e2d41a449b3\",\n \"files\": 1,\n \"z\": 0\n },\n {\n \"bx\": 1,\n \"by\": 0,\n \"bytes\": 2437518098,\n \"digest\": \"7dc96f776c8423e57a2785489a3f9c43fb6e756876d6ad9a9cac4aa4e72ec193\",\n \"files\": 2,\n \"z\": 7\n }\n ],\n \"bytes\": 2437523419,\n \"dataset\": \"ds\",\n \"files\": 3,\n \"format\": 1,\n \"journal_seq\": 376157,\n \"style\": \"dark\"\n}\n",
|
||||
"index_sha256": "02bfedeafc1aec68814eeafa4e116d0c7b94d8bbb164c8cab17a58a1241473fa"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "0.51.0",
|
||||
"raw_len": 993204,
|
||||
"raw_sha256": "a0e86b662cd228497a697d22096e0d888bea40484802a4bf393e5283cc883971",
|
||||
"canonical_len": 756473,
|
||||
"canonical_sha256": "901818050d56e309cedbc5adfbfafa63a30c6d9ac59d85450d9fa0681d1bbc3d",
|
||||
"payload_sha256": "1be8d279322c6d16b7cd7a4e942cfc15207fca795be45bbdbe55f94c42de629f",
|
||||
"release_public_key": "ed25519:DS7z/nhxZE9ffn0PKVtw5fMUBRLdQ424OE236A0tbts=",
|
||||
"app_files": 4950
|
||||
}
|
||||
+26835
File diff suppressed because it is too large
Load Diff
+309
@@ -0,0 +1,309 @@
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"path": "Odian/x.exe",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "db/20260915T082529Z/data/public.odb_air_timeline_2026_02.dump",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "../a",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: '../a'"
|
||||
},
|
||||
{
|
||||
"path": "a/../b",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: 'a/../b'"
|
||||
},
|
||||
{
|
||||
"path": "/a",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must be relative with '/' separators: '/a'"
|
||||
},
|
||||
{
|
||||
"path": "C:/a",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'C:' holds a control character or one of <>:\"|?*: 'C:/a'"
|
||||
},
|
||||
{
|
||||
"path": "a\\b",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must be relative with '/' separators: 'a\\\\b'"
|
||||
},
|
||||
{
|
||||
"path": "a//b",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: 'a//b'"
|
||||
},
|
||||
{
|
||||
"path": "",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: empty path"
|
||||
},
|
||||
{
|
||||
"path": "./a",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: './a'"
|
||||
},
|
||||
{
|
||||
"path": "a/",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: 'a/'"
|
||||
},
|
||||
{
|
||||
"path": "a/.",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: 'a/.'"
|
||||
},
|
||||
{
|
||||
"path": ".",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: '.'"
|
||||
},
|
||||
{
|
||||
"path": "..",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: '..'"
|
||||
},
|
||||
{
|
||||
"path": "a/..",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: 'a/..'"
|
||||
},
|
||||
{
|
||||
"path": "Odian/Odian.exe:hidden",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'Odian.exe:hidden' holds a control character or one of <>:\"|?*: 'Odian/Odian.exe:hidden'"
|
||||
},
|
||||
{
|
||||
"path": "a/b.",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b.' ends with a dot or space: 'a/b.'"
|
||||
},
|
||||
{
|
||||
"path": "a/b ",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b ' ends with a dot or space: 'a/b '"
|
||||
},
|
||||
{
|
||||
"path": "CON",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'CON' is a reserved Windows device name: 'CON'"
|
||||
},
|
||||
{
|
||||
"path": "con",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'con' is a reserved Windows device name: 'con'"
|
||||
},
|
||||
{
|
||||
"path": "a/CoN.TxT",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'CoN.TxT' is a reserved Windows device name: 'a/CoN.TxT'"
|
||||
},
|
||||
{
|
||||
"path": "a/aux.txt",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'aux.txt' is a reserved Windows device name: 'a/aux.txt'"
|
||||
},
|
||||
{
|
||||
"path": "a/Lpt1.log",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'Lpt1.log' is a reserved Windows device name: 'a/Lpt1.log'"
|
||||
},
|
||||
{
|
||||
"path": "NUL.tar.gz",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'NUL.tar.gz' is a reserved Windows device name: 'NUL.tar.gz'"
|
||||
},
|
||||
{
|
||||
"path": "a/nul..x",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'nul..x' is a reserved Windows device name: 'a/nul..x'"
|
||||
},
|
||||
{
|
||||
"path": "a/prn",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'prn' is a reserved Windows device name: 'a/prn'"
|
||||
},
|
||||
{
|
||||
"path": "a/prn_x",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/com\u00b9",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'com\u00b9' is a reserved Windows device name: 'a/com\u00b9'"
|
||||
},
|
||||
{
|
||||
"path": "a/COM\u00b2.txt",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'COM\u00b2.txt' is a reserved Windows device name: 'a/COM\u00b2.txt'"
|
||||
},
|
||||
{
|
||||
"path": "a/lpt\u00b3",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'lpt\u00b3' is a reserved Windows device name: 'a/lpt\u00b3'"
|
||||
},
|
||||
{
|
||||
"path": "a/lpt0",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'lpt0' is a reserved Windows device name: 'a/lpt0'"
|
||||
},
|
||||
{
|
||||
"path": "a/com0",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'com0' is a reserved Windows device name: 'a/com0'"
|
||||
},
|
||||
{
|
||||
"path": "a/comA",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/com10",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/con .txt",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/b\u0000c",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b\\x00c' holds a control character or one of <>:\"|?*: 'a/b\\x00c'"
|
||||
},
|
||||
{
|
||||
"path": "a/b\nc",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b\\nc' holds a control character or one of <>:\"|?*: 'a/b\\nc'"
|
||||
},
|
||||
{
|
||||
"path": "a/b\u001fc",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b\\x1fc' holds a control character or one of <>:\"|?*: 'a/b\\x1fc'"
|
||||
},
|
||||
{
|
||||
"path": "a/b\u007fc",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/b\u0085c",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/b\u2028c",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/b?c",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b?c' holds a control character or one of <>:\"|?*: 'a/b?c'"
|
||||
},
|
||||
{
|
||||
"path": "a/b*c",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b*c' holds a control character or one of <>:\"|?*: 'a/b*c'"
|
||||
},
|
||||
{
|
||||
"path": "a/b\"c",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b\"c' holds a control character or one of <>:\"|?*: 'a/b\"c'"
|
||||
},
|
||||
{
|
||||
"path": "a/b|c",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b|c' holds a control character or one of <>:\"|?*: 'a/b|c'"
|
||||
},
|
||||
{
|
||||
"path": "a/b<c",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b<c' holds a control character or one of <>:\"|?*: 'a/b<c'"
|
||||
},
|
||||
{
|
||||
"path": "a/b>c",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b>c' holds a control character or one of <>:\"|?*: 'a/b>c'"
|
||||
},
|
||||
{
|
||||
"path": "a/console.txt",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/auxiliary",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/.hidden",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/b c.txt",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": " a/b",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/ b",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/b.c.",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path part 'b.c.' ends with a dot or space: 'a/b.c.'"
|
||||
},
|
||||
{
|
||||
"path": "donn\u00e9es/\u00e9t\u00e9.png",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "\u4e2d\u6587/\u8def\u5f84.txt",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/\ud83d\ude00.png",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/b#c%20d",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/aux\u00a0",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/com\u0660",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"path": "a/\u212aon",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"app": {
|
||||
"files": [
|
||||
{
|
||||
"path": "Odian/Odian.exe",
|
||||
"sha256": "9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b",
|
||||
"size": 3
|
||||
},
|
||||
{
|
||||
"path": "Odian/assets/empty.conf",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 0
|
||||
},
|
||||
{
|
||||
"path": "Odian/donn\u00e9es/carte \u00e9t\u00e9.png",
|
||||
"sha256": "d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9",
|
||||
"size": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"db": {
|
||||
"files": [
|
||||
{
|
||||
"path": "db/20260915T101500Z/pre-data.dump",
|
||||
"section": "pre-data",
|
||||
"sha256": "148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940",
|
||||
"size": 1
|
||||
},
|
||||
{
|
||||
"path": "db/20260915T101500Z/data/public.t1.dump",
|
||||
"section": "data",
|
||||
"sha256": "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4",
|
||||
"size": 1,
|
||||
"table": "public.t1"
|
||||
}
|
||||
],
|
||||
"schema_version": 0,
|
||||
"snapshot": "20260915T101500Z"
|
||||
},
|
||||
"extra_note": "unknown keys are signed too: \"quoted\" \\ \u0001 \ud83d\ude00",
|
||||
"min_launcher": "0.1.0",
|
||||
"notes": "notes.md",
|
||||
"product": "odian",
|
||||
"released_at": "2026-09-15T12:00:00Z",
|
||||
"schema": 1,
|
||||
"signature": "ed25519:lgdqpswT6p5WOOy74T3v1JA7aWsJUMmRjc+MFDJSMw7W/GG2ec0BuWaeVL+YkMP+WtaF3tOS8Lh43TlD2Y7UDw==",
|
||||
"src": {
|
||||
"chunk_bytes": 10,
|
||||
"enabled": true,
|
||||
"files": [
|
||||
{
|
||||
"chunks": [
|
||||
"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3",
|
||||
"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982"
|
||||
],
|
||||
"chunks_sha256": "9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867",
|
||||
"path": "osm/x.sqlite",
|
||||
"size": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
"tiles": {
|
||||
"block_shift": 6,
|
||||
"sets": [
|
||||
{
|
||||
"blocks": 2,
|
||||
"bytes": 2437523419,
|
||||
"dataset": "ds",
|
||||
"files": 3,
|
||||
"index": "tiles/dark.ds.index.json",
|
||||
"index_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"journal_seq": 9,
|
||||
"style": "dark"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": "0.51.0"
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"app": {
|
||||
"files": [
|
||||
{
|
||||
"path": "Odian/Odian.exe",
|
||||
"sha256": "9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b",
|
||||
"size": 3
|
||||
},
|
||||
{
|
||||
"path": "Odian/assets/empty.conf",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 0
|
||||
},
|
||||
{
|
||||
"path": "Odian/donn\u00e9es/carte \u00e9t\u00e9.png",
|
||||
"sha256": "d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9",
|
||||
"size": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"db": {
|
||||
"files": [
|
||||
{
|
||||
"path": "db/20260915T101500Z/pre-data.dump",
|
||||
"section": "pre-data",
|
||||
"sha256": "148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940",
|
||||
"size": 1
|
||||
},
|
||||
{
|
||||
"path": "db/20260915T101500Z/data/public.t1.dump",
|
||||
"section": "data",
|
||||
"sha256": "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4",
|
||||
"size": 1,
|
||||
"table": "public.t1"
|
||||
}
|
||||
],
|
||||
"schema_version": 0,
|
||||
"snapshot": "20260915T101500Z"
|
||||
},
|
||||
"extra_note": "unknown keys are signed too: \"quoted\" \\ \u0001 \ud83d\ude00",
|
||||
"min_launcher": "0.1.0",
|
||||
"notes": "notes.md",
|
||||
"product": "odian",
|
||||
"released_at": "2026-09-15T12:00:00Z",
|
||||
"schema": 1,
|
||||
"signature": "ed25519:aQdqpswT6p5WOOy74T3v1JA7aWsJUMmRjc+MFDJSMw7W/GG2ec0BuWaeVL+YkMP+WtaF3tOS8Lh43TlD2Y7UDw==",
|
||||
"src": {
|
||||
"chunk_bytes": 10,
|
||||
"enabled": true,
|
||||
"files": [
|
||||
{
|
||||
"chunks": [
|
||||
"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3",
|
||||
"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982"
|
||||
],
|
||||
"chunks_sha256": "9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867",
|
||||
"path": "osm/x.sqlite",
|
||||
"size": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
"tiles": {
|
||||
"block_shift": 6,
|
||||
"sets": [
|
||||
{
|
||||
"blocks": 2,
|
||||
"bytes": 2437523419,
|
||||
"dataset": "ds",
|
||||
"files": 3,
|
||||
"index": "tiles/dark.ds.index.json",
|
||||
"index_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"journal_seq": 9,
|
||||
"style": "dark"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": "0.51.0"
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"app": {
|
||||
"files": [
|
||||
{
|
||||
"path": "Odian/Odian.exe",
|
||||
"sha256": "9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b",
|
||||
"size": 3
|
||||
},
|
||||
{
|
||||
"path": "Odian/assets/empty.conf",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 0
|
||||
},
|
||||
{
|
||||
"path": "Odian/données/carte été.png",
|
||||
"sha256": "d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9",
|
||||
"size": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"db": {
|
||||
"files": [
|
||||
{
|
||||
"path": "db/20260915T101500Z/pre-data.dump",
|
||||
"section": "pre-data",
|
||||
"sha256": "148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940",
|
||||
"size": 1
|
||||
},
|
||||
{
|
||||
"path": "db/20260915T101500Z/data/public.t1.dump",
|
||||
"section": "data",
|
||||
"sha256": "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4",
|
||||
"size": 1,
|
||||
"table": "public.t1"
|
||||
}
|
||||
],
|
||||
"schema_version": 0,
|
||||
"snapshot": "20260915T101500Z"
|
||||
},
|
||||
"extra_note": "unknown keys are signed too: \"quoted\" \\ \u0001 😀",
|
||||
"min_launcher": "0.1.0",
|
||||
"notes": "notes.md",
|
||||
"product": "odian",
|
||||
"released_at": "2026-09-15T12:00:00Z",
|
||||
"schema": 1,
|
||||
"signature": "ed25519:aQdqpswT6p5WOOy74T3v1JA7aWsJUMmRjc+MFDJSMw7W/GG2ec0BuWaeVL+YkMP+WtaF3tOS8Lh43TlD2Y7UDw==",
|
||||
"src": {
|
||||
"chunk_bytes": 10,
|
||||
"enabled": true,
|
||||
"files": [
|
||||
{
|
||||
"chunks": [
|
||||
"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3",
|
||||
"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982"
|
||||
],
|
||||
"chunks_sha256": "9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867",
|
||||
"path": "osm/x.sqlite",
|
||||
"size": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
"tiles": {
|
||||
"block_shift": 6,
|
||||
"sets": [
|
||||
{
|
||||
"blocks": 2,
|
||||
"bytes": 2437523419,
|
||||
"dataset": "ds",
|
||||
"files": 3,
|
||||
"index": "tiles/dark.ds.index.json",
|
||||
"index_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"journal_seq": 9,
|
||||
"style": "dark"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": "0.51.0"
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"private_key_raw_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
||||
"public_key": "ed25519:A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=",
|
||||
"other_public_key": "ed25519:ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=",
|
||||
"signed_with_other_key_verdict": "error",
|
||||
"signature": "ed25519:aQdqpswT6p5WOOy74T3v1JA7aWsJUMmRjc+MFDJSMw7W/GG2ec0BuWaeVL+YkMP+WtaF3tOS8Lh43TlD2Y7UDw==",
|
||||
"payload_hex": "7b22617070223a7b2266696c6573223a5b7b2270617468223a224f6469616e2f4f6469616e2e657865222c22736861323536223a2239303935626462383539333038623632616366303430333666666434616466653336366437663733376432373665623663343661653433346633383136633962222c2273697a65223a337d2c7b2270617468223a224f6469616e2f6173736574732f656d7074792e636f6e66222c22736861323536223a2265336230633434323938666331633134396166626634633839393666623932343237616534316534363439623933346361343935393931623738353262383535222c2273697a65223a307d2c7b2270617468223a224f6469616e2f646f6e6ec3a965732f636172746520c3a974c3a92e706e67222c22736861323536223a2264326331323432306239383135653037623239623132353666646436653339613934623639656431393230303966306335643761666666633939646534346539222c2273697a65223a357d5d7d2c226462223a7b2266696c6573223a5b7b2270617468223a2264622f3230323630393135543130313530305a2f7072652d646174612e64756d70222c2273656374696f6e223a227072652d64617461222c22736861323536223a2231343864653963356137613434643139653536636439616531613535346266363738343761666230633538663665313266613239616337646466636139393430222c2273697a65223a317d2c7b2270617468223a2264622f3230323630393135543130313530305a2f646174612f7075626c69632e74312e64756d70222c2273656374696f6e223a2264617461222c22736861323536223a2231386163336537333433663031363839306335313065393366393335323631313639643965336635363534333634323938333066616630393334663466386534222c2273697a65223a312c227461626c65223a227075626c69632e7431227d5d2c22736368656d615f76657273696f6e223a302c22736e617073686f74223a223230323630393135543130313530305a227d2c2265787472615f6e6f7465223a22756e6b6e6f776e206b65797320617265207369676e656420746f6f3a205c2271756f7465645c22205c5c205c753030303120f09f9880222c226d696e5f6c61756e63686572223a22302e312e30222c226e6f746573223a226e6f7465732e6d64222c2270726f64756374223a226f6469616e222c2272656c65617365645f6174223a22323032362d30392d31355431323a30303a30305a222c22736368656d61223a312c22737263223a7b226368756e6b5f6279746573223a31302c22656e61626c6564223a747275652c2266696c6573223a5b7b226368756e6b73223a5b2231323263353937303833626434333862376636643732616637356430323539343838393936343737313162383036626464326364383266613639373133646233222c2264306636333163613164646261386462336263666362396530353763646339386430333739663162656530306537356135343531343761323764616464393832225d2c226368756e6b735f736861323536223a2239636132623534313738616238653732613635303666313135633137633737623338636464356333323366363463653336653061316661343061613635383637222c2270617468223a226f736d2f782e73716c697465222c2273697a65223a31357d5d7d2c2274696c6573223a7b22626c6f636b5f7368696674223a362c2273657473223a5b7b22626c6f636b73223a322c226279746573223a323433373532333431392c2264617461736574223a226473222c2266696c6573223a332c22696e646578223a2274696c65732f6461726b2e64732e696e6465782e6a736f6e222c22696e6465785f736861323536223a2261616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161222c226a6f75726e616c5f736571223a392c227374796c65223a226461726b227d5d7d2c2276657273696f6e223a22302e35312e30227d",
|
||||
"cases": [
|
||||
{
|
||||
"file": "signed_manifest.utf8.json",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"file": "signed_manifest.ascii.json",
|
||||
"python": "ok",
|
||||
"python_error": ""
|
||||
},
|
||||
{
|
||||
"file": "tampered_manifest.json",
|
||||
"python": "error",
|
||||
"python_error": "SignatureError: signature does not match: manifest altered, or signed by another key"
|
||||
},
|
||||
{
|
||||
"file": "signature_flipped_manifest.json",
|
||||
"python": "error",
|
||||
"python_error": "SignatureError: signature does not match: manifest altered, or signed by another key"
|
||||
},
|
||||
{
|
||||
"file": "unsigned_manifest.json",
|
||||
"python": "error",
|
||||
"python_error": "SignatureError: manifest is not signed"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"app": {
|
||||
"files": [
|
||||
{
|
||||
"path": "Odian/Odian.exe",
|
||||
"sha256": "9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b",
|
||||
"size": 4
|
||||
},
|
||||
{
|
||||
"path": "Odian/assets/empty.conf",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 0
|
||||
},
|
||||
{
|
||||
"path": "Odian/donn\u00e9es/carte \u00e9t\u00e9.png",
|
||||
"sha256": "d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9",
|
||||
"size": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"db": {
|
||||
"files": [
|
||||
{
|
||||
"path": "db/20260915T101500Z/pre-data.dump",
|
||||
"section": "pre-data",
|
||||
"sha256": "148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940",
|
||||
"size": 1
|
||||
},
|
||||
{
|
||||
"path": "db/20260915T101500Z/data/public.t1.dump",
|
||||
"section": "data",
|
||||
"sha256": "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4",
|
||||
"size": 1,
|
||||
"table": "public.t1"
|
||||
}
|
||||
],
|
||||
"schema_version": 0,
|
||||
"snapshot": "20260915T101500Z"
|
||||
},
|
||||
"extra_note": "unknown keys are signed too: \"quoted\" \\ \u0001 \ud83d\ude00",
|
||||
"min_launcher": "0.1.0",
|
||||
"notes": "notes.md",
|
||||
"product": "odian",
|
||||
"released_at": "2026-09-15T12:00:00Z",
|
||||
"schema": 1,
|
||||
"signature": "ed25519:aQdqpswT6p5WOOy74T3v1JA7aWsJUMmRjc+MFDJSMw7W/GG2ec0BuWaeVL+YkMP+WtaF3tOS8Lh43TlD2Y7UDw==",
|
||||
"src": {
|
||||
"chunk_bytes": 10,
|
||||
"enabled": true,
|
||||
"files": [
|
||||
{
|
||||
"chunks": [
|
||||
"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3",
|
||||
"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982"
|
||||
],
|
||||
"chunks_sha256": "9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867",
|
||||
"path": "osm/x.sqlite",
|
||||
"size": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
"tiles": {
|
||||
"block_shift": 6,
|
||||
"sets": [
|
||||
{
|
||||
"blocks": 2,
|
||||
"bytes": 2437523419,
|
||||
"dataset": "ds",
|
||||
"files": 3,
|
||||
"index": "tiles/dark.ds.index.json",
|
||||
"index_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"journal_seq": 9,
|
||||
"style": "dark"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": "0.51.0"
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"app": {
|
||||
"files": [
|
||||
{
|
||||
"path": "Odian/Odian.exe",
|
||||
"sha256": "9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b",
|
||||
"size": 3
|
||||
},
|
||||
{
|
||||
"path": "Odian/assets/empty.conf",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 0
|
||||
},
|
||||
{
|
||||
"path": "Odian/donn\u00e9es/carte \u00e9t\u00e9.png",
|
||||
"sha256": "d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9",
|
||||
"size": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"db": {
|
||||
"files": [
|
||||
{
|
||||
"path": "db/20260915T101500Z/pre-data.dump",
|
||||
"section": "pre-data",
|
||||
"sha256": "148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940",
|
||||
"size": 1
|
||||
},
|
||||
{
|
||||
"path": "db/20260915T101500Z/data/public.t1.dump",
|
||||
"section": "data",
|
||||
"sha256": "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4",
|
||||
"size": 1,
|
||||
"table": "public.t1"
|
||||
}
|
||||
],
|
||||
"schema_version": 0,
|
||||
"snapshot": "20260915T101500Z"
|
||||
},
|
||||
"extra_note": "unknown keys are signed too: \"quoted\" \\ \u0001 \ud83d\ude00",
|
||||
"min_launcher": "0.1.0",
|
||||
"notes": "notes.md",
|
||||
"product": "odian",
|
||||
"released_at": "2026-09-15T12:00:00Z",
|
||||
"schema": 1,
|
||||
"src": {
|
||||
"chunk_bytes": 10,
|
||||
"enabled": true,
|
||||
"files": [
|
||||
{
|
||||
"chunks": [
|
||||
"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3",
|
||||
"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982"
|
||||
],
|
||||
"chunks_sha256": "9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867",
|
||||
"path": "osm/x.sqlite",
|
||||
"size": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
"tiles": {
|
||||
"block_shift": 6,
|
||||
"sets": [
|
||||
{
|
||||
"blocks": 2,
|
||||
"bytes": 2437523419,
|
||||
"dataset": "ds",
|
||||
"files": 3,
|
||||
"index": "tiles/dark.ds.index.json",
|
||||
"index_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"journal_seq": 9,
|
||||
"style": "dark"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": "0.51.0"
|
||||
}
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"name": "valid_full",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "ok",
|
||||
"python_error": "",
|
||||
"rust_expect": "ok",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "valid_minimal",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}]}}",
|
||||
"python": "ok",
|
||||
"python_error": "",
|
||||
"rust_expect": "ok",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "no_app_section",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "ok",
|
||||
"python_error": "",
|
||||
"rust_expect": "ok",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "app_null",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": null, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app must be an object",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "app_files_missing",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files must be a list",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "app_entry_not_object",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [null]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files[0] must be an object",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "db_entry_not_object",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [1]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: db.files[0] must be an object",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "tiles_set_not_object",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [1]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: tiles.sets[0] must be an object",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "src_file_not_object",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [\"x\"]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: src.files[0] must be an object",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "manifest_not_object",
|
||||
"manifest_json": "[1]",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: manifest must be a JSON object",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "schema_2",
|
||||
"manifest_json": "{\"schema\": 2, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: schema must be 1",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "schema_true",
|
||||
"manifest_json": "{\"schema\": true, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: schema must be 1",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "schema_float",
|
||||
"manifest_json": "{\"schema\": 1.0, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: manifest.schema: numbers must be integers, got 1.0",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "product_wrong",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"other\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: product must be 'odian'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "version_two_parts",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"1.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: version must be MAJOR.MINOR.PATCH, got '1.0'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "version_leading_zero",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"01.2.3\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: version must be MAJOR.MINOR.PATCH, got '01.2.3'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "version_int",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": 5, \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: version must be a MAJOR.MINOR.PATCH string, got 5",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "version_missing",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: version must be MAJOR.MINOR.PATCH, got ''",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "version_trailing_newline",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\\n\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: version must be MAJOR.MINOR.PATCH, got '0.51.0\\n'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "version_unicode_digit",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.\\u0663\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: version must be MAJOR.MINOR.PATCH, got '0.51.\u0663'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "released_at_int",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": 5, \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: released_at must be an ISO-8601 string",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "min_launcher_bad",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: version must be MAJOR.MINOR.PATCH, got '0.1'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "notes_traversal",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"../notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: '../notes.md'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "notes_int",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": 5, \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: empty path",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "sha_upper",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files[0]: sha256 must be 64 lowercase hex chars",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "sha_short",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files[0]: sha256 must be 64 lowercase hex chars",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "sha_trailing_newline",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\n\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files[0]: sha256 must be 64 lowercase hex chars",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "dup_app_path_case",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}, {\"path\": \"ODIAN/odian.EXE\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files[3]: duplicate path 'ODIAN/odian.EXE'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "dup_app_path_unicode_case",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}, {\"path\": \"Odian/\\u00c4.txt\", \"size\": 1, \"sha256\": \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"}, {\"path\": \"odian/\\u00e4.txt\", \"size\": 1, \"sha256\": \"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files[4]: duplicate path 'odian/\u00e4.txt'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "app_path_traversal",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"../evil.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: path must not contain empty, '.' or '..' parts: '../evil.exe'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "size_negative",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": -1, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files[0]: size must be a non-negative integer",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "size_bool",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": true, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files[0]: size must be a non-negative integer",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "size_float",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3.0, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: manifest.app.files[0].size: numbers must be integers, got 3.0",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "size_string",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": \"3\", \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: app.files[0]: size must be a non-negative integer",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "size_above_u64",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 18446744073709551616, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: manifest.app.files[0].size: 18446744073709551616 does not fit in 64 bits",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "db_empty_files",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": []}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: db.files must not be empty",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "db_bad_section",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"schema\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: db.files[0]: section must be one of ('pre-data', 'data', 'sequences', 'post-data')",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "db_data_without_table",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: db.files[1]: data dumps name their table",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "db_data_empty_table",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: db.files[1]: data dumps name their table",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "db_path_outside_snapshot",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260101T000000Z/x.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: db.files[0]: path must live under db/20260915T101500Z/",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "db_snapshot_bad",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"2026-09-15\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: db.snapshot must look like 20260915T101500Z",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "db_schema_version_bool",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": true, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: db.schema_version must be a non-negative integer",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "tiles_block_shift_7",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 7, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: tiles.block_shift must be 6",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "tiles_block_shift_float",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6.0, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: manifest.tiles.block_shift: numbers must be integers, got 6.0",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "tiles_bad_style",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"da rk\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: tiles.sets[0]: bad style",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "tiles_style_trailing_newline",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\\n\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark\\n.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: tiles.sets[0]: bad style",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "tiles_dup_set",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}, {\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: tiles.sets[1]: duplicate tile set dark/ds",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "tiles_wrong_index_name",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/other.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: tiles.sets[0]: index must be 'tiles/dark.ds.index.json'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "tiles_count_negative",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": -1, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: tiles.sets[0]: journal_seq must be a non-negative integer",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "src_enabled_int",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": 1, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: src.enabled must be true or false",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "src_chunk_bytes_zero",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 0, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: src.chunk_bytes must be a positive integer",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "src_chunk_count_wrong",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 25, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: src.files[0]: 2 chunks for 25 bytes at 10 per chunk",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "src_chunks_sha_wrong",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: src.files[0]: chunks_sha256 does not match chunks",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "src_empty_file",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}, {\"path\": \"e.bin\", \"size\": 0, \"chunks\": [], \"chunks_sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\"}",
|
||||
"python": "ok",
|
||||
"python_error": "",
|
||||
"rust_expect": "ok",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "signature_not_ed25519",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\", \"signature\": \"rsa:xx\"}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: signature must look like 'ed25519:<base64>'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "signature_int",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\", \"signature\": 5}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: signature must look like 'ed25519:<base64>'",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "unknown_keys",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\", \"future\": {\"x\": [1, \"y\"]}}",
|
||||
"python": "ok",
|
||||
"python_error": "",
|
||||
"rust_expect": "ok",
|
||||
"quirk": null
|
||||
},
|
||||
{
|
||||
"name": "unknown_float_key",
|
||||
"manifest_json": "{\"schema\": 1, \"product\": \"odian\", \"version\": \"0.51.0\", \"released_at\": \"2026-09-15T12:00:00Z\", \"min_launcher\": \"0.1.0\", \"notes\": \"notes.md\", \"app\": {\"files\": [{\"path\": \"Odian/Odian.exe\", \"size\": 3, \"sha256\": \"9095bdb859308b62acf04036ffd4adfe366d7f737d276eb6c46ae434f3816c9b\"}, {\"path\": \"Odian/assets/empty.conf\", \"size\": 0, \"sha256\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}, {\"path\": \"Odian/donn\\u00e9es/carte \\u00e9t\\u00e9.png\", \"size\": 5, \"sha256\": \"d2c12420b9815e07b29b1256fdd6e39a94b69ed192009f0c5d7afffc99de44e9\"}]}, \"db\": {\"schema_version\": 0, \"snapshot\": \"20260915T101500Z\", \"files\": [{\"section\": \"pre-data\", \"path\": \"db/20260915T101500Z/pre-data.dump\", \"size\": 1, \"sha256\": \"148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940\"}, {\"section\": \"data\", \"table\": \"public.t1\", \"path\": \"db/20260915T101500Z/data/public.t1.dump\", \"size\": 1, \"sha256\": \"18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4\"}]}, \"tiles\": {\"block_shift\": 6, \"sets\": [{\"style\": \"dark\", \"dataset\": \"ds\", \"journal_seq\": 9, \"blocks\": 2, \"files\": 3, \"bytes\": 2437523419, \"index\": \"tiles/dark.ds.index.json\", \"index_sha256\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}]}, \"src\": {\"enabled\": true, \"chunk_bytes\": 10, \"files\": [{\"path\": \"osm/x.sqlite\", \"size\": 15, \"chunks\": [\"122c597083bd438b7f6d72af75d025948899647711b806bdd2cd82fa69713db3\", \"d0f631ca1ddba8db3bcfcb9e057cdc98d0379f1bee00e75a545147a27dadd982\"], \"chunks_sha256\": \"9ca2b54178ab8e72a6506f115c17c77b38cdd5c323f64ce36e0a1fa40aa65867\"}]}, \"extra_note\": \"unknown keys are signed too: \\\"quoted\\\" \\\\ \\u0001 \\ud83d\\ude00\", \"ratio\": 0.5}",
|
||||
"python": "error",
|
||||
"python_error": "ManifestError: manifest.ratio: numbers must be integers, got 0.5",
|
||||
"rust_expect": "error",
|
||||
"quirk": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
"""Local stub of an ODIAN site for the odian-packs Rust end-to-end tests.
|
||||
|
||||
PYTHONPATH=<ODIAN_MCP_Server>/src python stub_site.py --root DIR [--no-empty-src]
|
||||
|
||||
Builds a small signed pack store under DIR with odian_packs itself. The layout is the
|
||||
site store layout, which is also the ODIAN drive layout:
|
||||
|
||||
DIR/packs/versions/CURRENT
|
||||
DIR/packs/versions/0.51.0/{manifest.json, notes.md, tiles/dark.test_ds.index.json}
|
||||
DIR/packs/app/<sha256> DIR/packs/db/<snapshot>/...
|
||||
DIR/tiles/dark/test_ds/<z>/<x>/<y><ext>, DIR/tiles/tiles.db
|
||||
DIR/src/...
|
||||
|
||||
It then serves DIR on 127.0.0.1 (a free port) with ``odian_update.api.create_app`` and a
|
||||
stub token validator (token ``good``), plus nginx-like Range routes for /packs/app,
|
||||
/packs/db and /src that check the same Bearer token. It prints one JSON line (port, test
|
||||
public key, token, layout) and serves until its stdin closes.
|
||||
|
||||
Test-only control routes (no token):
|
||||
GET /_ctl/log every request seen: path, range
|
||||
POST /_ctl/reset clear the log and all rules
|
||||
POST /_ctl/fail?prefix=P&status=503&count=N&retry_after=0
|
||||
POST /_ctl/cut?prefix=P&after=BYTES&count=N send BYTES of the body, then drop the connection
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import urllib.parse
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
VERSION = "0.51.0"
|
||||
STYLE = "dark"
|
||||
DATASET = "test_ds"
|
||||
SNAPSHOT = "20260915T101500Z"
|
||||
TOKEN = "good"
|
||||
CHUNK = 1024 * 1024
|
||||
TEST_KEY_RAW = bytes(range(32))
|
||||
|
||||
APP_FILES = {
|
||||
"Odian/Odian.exe": 200 * 1024,
|
||||
"Odian/assets/empty.conf": 0,
|
||||
"Odian/data/big.bin": 3 * CHUNK + 17,
|
||||
"Odian/données/carte été.png": 1000,
|
||||
"api/odian_api.exe": 50 * 1024,
|
||||
}
|
||||
DB_FILES = [
|
||||
("pre-data", None, "pre-data.dump", 5000),
|
||||
("data", "public.t1", "data/public.t1.dump", 300 * 1024),
|
||||
("post-data", None, "post-data.dump", 2048),
|
||||
]
|
||||
# (z, x, y, ext, size). Blocks (0,0,0), (3,0,0), (7,0,0), (7,1,0). The 8-byte tile is
|
||||
# below MIN_TILE_BYTES and never journaled.
|
||||
TILES = [
|
||||
(0, 0, 0, ".png", 5000),
|
||||
(3, 1, 2, ".png", 40),
|
||||
(7, 1, 2, ".png", 700),
|
||||
(7, 3, 2, ".jpg", 512),
|
||||
(7, 3, 2, ".png", 9),
|
||||
(7, 63, 63, ".pbf", 300 * 1024),
|
||||
(7, 2, 2, ".png", 8),
|
||||
(7, 64, 5, ".png", 20),
|
||||
(7, 127, 63, ".png", 1000),
|
||||
]
|
||||
# Journaled AFTER the index is frozen, inside block (7,0,0): no install may contain it.
|
||||
LATE_TILE = (7, 5, 5, ".png", 64)
|
||||
SRC_FILES = {"osm_source/world_aeroway_v1.sqlite": 2 * CHUNK + 123, "osm_source/small.bin": 100}
|
||||
EMPTY_SRC = "osm_source/empty.bin"
|
||||
|
||||
|
||||
def blob(label, size):
|
||||
return hashlib.shake_256(label.encode("utf-8")).digest(size) if size else b""
|
||||
|
||||
|
||||
def write(path, data):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
def write_json(path, obj):
|
||||
"""Like odian_packs.cli._write_json: pretty, sorted, ASCII, trailing newline."""
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||
json.dump(obj, f, indent=2, sort_keys=True)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def join(root, rel):
|
||||
return os.path.join(root, *rel.split("/"))
|
||||
|
||||
|
||||
def build(root, empty_src):
|
||||
from odian_packs import appstore, hashing, signing, tiles
|
||||
from odian_packs import manifest as mf
|
||||
|
||||
packs = os.path.join(root, "packs")
|
||||
tiles_root = os.path.join(root, "tiles")
|
||||
src_root = os.path.join(root, "src")
|
||||
stage = os.path.join(root, "stage")
|
||||
|
||||
for rel, size in APP_FILES.items():
|
||||
write(join(os.path.join(stage, "app"), rel), blob("app:" + rel, size))
|
||||
app = appstore.store_tree(os.path.join(stage, "app"), packs, workers=2)
|
||||
shutil.rmtree(stage)
|
||||
|
||||
db_files = []
|
||||
for section, table, rel, size in DB_FILES:
|
||||
path_rel = f"db/{SNAPSHOT}/{rel}"
|
||||
full = join(packs, path_rel)
|
||||
write(full, blob("db:" + rel, size))
|
||||
got_size, got_sha = hashing.sha256_file(full)
|
||||
entry = {"section": section, "path": path_rel, "size": got_size, "sha256": got_sha}
|
||||
if table:
|
||||
entry["table"] = table
|
||||
db_files.append(entry)
|
||||
|
||||
set_root = os.path.join(tiles_root, STYLE, DATASET)
|
||||
journal = os.path.join(tiles_root, "tiles.db")
|
||||
for z, x, y, ext, size in TILES:
|
||||
write(os.path.join(set_root, str(z), str(x), f"{y}{ext}"), blob(f"tile:{z}/{x}/{y}{ext}", size))
|
||||
tiles.import_tree(set_root, STYLE, DATASET, journal, workers=2)
|
||||
index = tiles.build_index(journal, STYLE, DATASET)
|
||||
version_dir = os.path.join(packs, "versions", VERSION)
|
||||
index_name = mf.tile_index_name(STYLE, DATASET)
|
||||
write_json(join(version_dir, index_name), index)
|
||||
z, x, y, ext, size = LATE_TILE
|
||||
write(os.path.join(set_root, str(z), str(x), f"{y}{ext}"), blob(f"tile:{z}/{x}/{y}{ext}", size))
|
||||
tiles.import_tree(set_root, STYLE, DATASET, journal, workers=2)
|
||||
|
||||
src_items = dict(SRC_FILES)
|
||||
if empty_src:
|
||||
src_items[EMPTY_SRC] = 0
|
||||
src_files = []
|
||||
for rel, size in src_items.items():
|
||||
full = join(src_root, rel)
|
||||
write(full, blob("src:" + rel, size))
|
||||
hashed = hashing.hash_chunks(full, CHUNK)
|
||||
src_files.append({"path": rel, "size": hashed["size"], "chunks": hashed["chunks"],
|
||||
"chunks_sha256": hashed["chunks_sha256"]})
|
||||
|
||||
manifest = {
|
||||
"schema": 1, "product": "odian", "version": VERSION, "released_at": "2026-09-15T12:00:00Z",
|
||||
"min_launcher": "0.1.0", "notes": "notes.md",
|
||||
"app": app,
|
||||
"db": {"schema_version": 0, "snapshot": SNAPSHOT, "files": db_files},
|
||||
"tiles": {"block_shift": 6, "sets": [{
|
||||
"style": STYLE, "dataset": DATASET, "journal_seq": index["journal_seq"],
|
||||
"blocks": len(index["blocks"]), "files": index["files"], "bytes": index["bytes"],
|
||||
"index": index_name, "index_sha256": tiles.index_sha256(index)}]},
|
||||
"src": {"enabled": True, "chunk_bytes": CHUNK, "files": src_files},
|
||||
}
|
||||
signed = signing.sign(mf.validate(manifest), signing.private_key_from_raw(TEST_KEY_RAW))
|
||||
write_json(os.path.join(version_dir, "manifest.json"), signed)
|
||||
write(os.path.join(version_dir, "notes.md"), "# ODIAN 0.51.0\n\nPremier pack signé.\n".encode("utf-8"))
|
||||
write(os.path.join(packs, "versions", "CURRENT"), f"{VERSION}\n".encode("ascii"))
|
||||
|
||||
|
||||
def serve(root, info):
|
||||
import uvicorn
|
||||
from fastapi import Request
|
||||
from fastapi.responses import Response
|
||||
|
||||
from odian_packs.manifest import ManifestError, safe_relpath
|
||||
from odian_update.api import create_app
|
||||
from odian_update.auth import AuthInvalid, Caller, bearer_token
|
||||
from odian_update.config import Settings
|
||||
|
||||
class StubValidator:
|
||||
async def validate(self, token):
|
||||
if token != TOKEN:
|
||||
raise AuthInvalid("invalid token")
|
||||
return Caller(username="stub-operator", expires_at=0)
|
||||
|
||||
packs = os.path.join(root, "packs")
|
||||
settings = Settings(packs_dir=packs, tiles_dir=os.path.join(root, "tiles"), max_streams=8)
|
||||
app = create_app(settings, validator=StubValidator())
|
||||
|
||||
def serve_file(request: Request, base: str, rel: str) -> Response:
|
||||
"""nginx `alias` + `auth_request`: token check, then the file, honouring one Range."""
|
||||
if bearer_token(request.headers.get("authorization")) != TOKEN:
|
||||
return Response(status_code=401, headers={"WWW-Authenticate": "Bearer"})
|
||||
try:
|
||||
safe_relpath(rel)
|
||||
except ManifestError:
|
||||
return Response(status_code=404)
|
||||
path = join(base, rel)
|
||||
if not os.path.isfile(path):
|
||||
return Response(status_code=404)
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
size = len(data)
|
||||
header = request.headers.get("range")
|
||||
if not header:
|
||||
return Response(data, media_type="application/octet-stream", headers={"Accept-Ranges": "bytes"})
|
||||
match = re.fullmatch(r"bytes=(\d*)-(\d*)", header.strip())
|
||||
unsatisfiable = Response(status_code=416, headers={"Content-Range": f"bytes */{size}"})
|
||||
if not match or (not match.group(1) and not match.group(2)):
|
||||
return unsatisfiable
|
||||
if not match.group(1):
|
||||
start, end = max(0, size - int(match.group(2))), size - 1
|
||||
else:
|
||||
start = int(match.group(1))
|
||||
end = min(int(match.group(2)), size - 1) if match.group(2) else size - 1
|
||||
if start >= size or start > end:
|
||||
return unsatisfiable
|
||||
return Response(data[start:end + 1], status_code=206, media_type="application/octet-stream",
|
||||
headers={"Content-Range": f"bytes {start}-{end}/{size}", "Accept-Ranges": "bytes"})
|
||||
|
||||
@app.get("/packs/app/{sha}")
|
||||
def app_file(sha: str, request: Request):
|
||||
return serve_file(request, os.path.join(packs, "app"), sha)
|
||||
|
||||
@app.get("/packs/db/{rel:path}")
|
||||
def db_file(rel: str, request: Request):
|
||||
return serve_file(request, os.path.join(packs, "db"), rel)
|
||||
|
||||
@app.get("/src/{rel:path}")
|
||||
def src_file(rel: str, request: Request):
|
||||
return serve_file(request, os.path.join(root, "src"), rel)
|
||||
|
||||
lock = threading.Lock()
|
||||
state = {"log": [], "fail": [], "cut": []}
|
||||
|
||||
async def reply(send, status, obj, headers=()):
|
||||
body = json.dumps(obj).encode("utf-8")
|
||||
await send({"type": "http.response.start", "status": status,
|
||||
"headers": [(b"content-type", b"application/json"),
|
||||
(b"content-length", str(len(body)).encode("ascii")), *headers]})
|
||||
await send({"type": "http.response.body", "body": body})
|
||||
|
||||
async def control(scope, receive, send):
|
||||
path = scope["path"]
|
||||
query = {k: v[0] for k, v in urllib.parse.parse_qs(scope["query_string"].decode("latin-1")).items()}
|
||||
with lock:
|
||||
if path == "/_ctl/log":
|
||||
return await reply(send, 200, {"requests": list(state["log"])})
|
||||
if path == "/_ctl/reset":
|
||||
state["log"].clear()
|
||||
state["fail"].clear()
|
||||
state["cut"].clear()
|
||||
elif path == "/_ctl/fail":
|
||||
state["fail"].append({"prefix": query["prefix"], "status": int(query.get("status", "503")),
|
||||
"count": int(query.get("count", "1")), "retry_after": query.get("retry_after")})
|
||||
elif path == "/_ctl/cut":
|
||||
state["cut"].append({"prefix": query["prefix"], "after": int(query["after"]),
|
||||
"count": int(query.get("count", "1"))})
|
||||
else:
|
||||
return await reply(send, 404, {"error": "unknown control"})
|
||||
await reply(send, 200, {"ok": True})
|
||||
|
||||
def take_rule(kind, path):
|
||||
for rule in state[kind]:
|
||||
if rule["count"] > 0 and path.startswith(rule["prefix"]):
|
||||
rule["count"] -= 1
|
||||
return rule
|
||||
return None
|
||||
|
||||
async def asgi(scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
return await app(scope, receive, send)
|
||||
path = scope["path"]
|
||||
if path.startswith("/_ctl/"):
|
||||
return await control(scope, receive, send)
|
||||
headers = {k.decode("latin-1").lower(): v.decode("latin-1") for k, v in scope["headers"]}
|
||||
with lock:
|
||||
state["log"].append({"path": path, "range": headers.get("range")})
|
||||
fail = take_rule("fail", path)
|
||||
cut = None if fail else take_rule("cut", path)
|
||||
if fail:
|
||||
extra = [(b"retry-after", fail["retry_after"].encode("ascii"))] if fail["retry_after"] is not None else []
|
||||
return await reply(send, fail["status"], {"error": "stub failure"}, extra)
|
||||
if not cut:
|
||||
return await app(scope, receive, send)
|
||||
|
||||
sent = 0
|
||||
|
||||
async def cutting_send(message):
|
||||
nonlocal sent
|
||||
if message["type"] != "http.response.body":
|
||||
return await send(message)
|
||||
body = message.get("body", b"")
|
||||
room = cut["after"] - sent
|
||||
if len(body) < room:
|
||||
sent += len(body)
|
||||
return await send(message)
|
||||
await send({"type": "http.response.body", "body": body[:room], "more_body": True})
|
||||
raise ConnectionAbortedError("stub cut the connection")
|
||||
|
||||
return await app(scope, receive, cutting_send)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
info.update(port=port, base_url=f"http://127.0.0.1:{port}")
|
||||
print(json.dumps(info), flush=True)
|
||||
|
||||
def exit_when_stdin_closes():
|
||||
try:
|
||||
sys.stdin.buffer.read()
|
||||
finally:
|
||||
os._exit(0)
|
||||
|
||||
threading.Thread(target=exit_when_stdin_closes, daemon=True).start()
|
||||
server = uvicorn.Server(uvicorn.Config(asgi, log_level="warning", lifespan="off", access_log=False))
|
||||
server.run(sockets=[sock])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", required=True)
|
||||
parser.add_argument("--no-empty-src", action="store_true", help="leave out the zero-byte source file")
|
||||
parser.add_argument("--build-only", action="store_true")
|
||||
args = parser.parse_args()
|
||||
root = os.path.abspath(args.root)
|
||||
if not os.path.isfile(os.path.join(root, "packs", "versions", "CURRENT")):
|
||||
build(root, empty_src=not args.no_empty_src)
|
||||
|
||||
from odian_packs import signing
|
||||
|
||||
info = {
|
||||
"root": root, "token": TOKEN, "version": VERSION, "style": STYLE, "dataset": DATASET, "snapshot": SNAPSHOT,
|
||||
"public_key": signing.public_key_text(signing.private_key_from_raw(TEST_KEY_RAW).public_key()),
|
||||
}
|
||||
if args.build_only:
|
||||
print(json.dumps(info), flush=True)
|
||||
return
|
||||
serve(root, info)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user