Files
lanspread/crates/lanspread-db/src/content_manifest/bundle.rs
T
ddidderr 60fd7ba0c2 feat(peer)!: cut over to authenticated catalog sharing
Replace address-only trust and pushed peer state with installation identities,
SPKI-pinned QUIC, candidate-only discovery, and bounded responder-owned
protocol-8 pulls. The runtime now owns each network generation and all admitted
work through shutdown.

Add exact bundled content identities, reproducible manifest publishing,
capability-confined downloads, streaming BLAKE3 verification, quarantine and
retry, and crash-recoverable download and install transactions. Ship generated
fixture catalogs and fail closed when production manifests are absent.

The Tauri backend exposes durable sharing policy, redacted identity state, and
attempt-keyed transfer snapshots. Frontend consumption follows in the next
commit. Repository-wide test certificates and protocol-7 paths are removed.

BREAKING CHANGE: peers must use protocol 8 and exact catalog content artifacts;
protocol-7 frames and shared-certificate identities are no longer accepted.

Test Plan:
- `just test` -- passed on the completed stack (708 workspace tests)
- `just clippy` -- passed on the completed stack
- `just build` -- passed with fixture catalogs on the completed stack
- `just catalog-check-production` -- failed closed because the external
  production manifest corpus is absent
- `git diff --cached --check` -- passed
2026-08-10 13:59:18 +02:00

335 lines
11 KiB
Rust

use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
use super::{
CatalogContentIdentity,
CatalogContentManifest,
CatalogManifestStore,
reject_incomplete_catalog_publication,
};
use crate::db::GameCatalog;
/// Immutable catalog authority pairing exact game versions with their
/// on-demand content manifests.
#[derive(Debug)]
pub struct CatalogBundle {
catalog: GameCatalog,
manifests: CatalogManifestStore,
manifests_root: Option<PathBuf>,
}
impl CatalogBundle {
/// Constructs one exact catalog authority and validates artifact coverage
/// without eagerly parsing manifest bodies.
///
/// # Errors
///
/// Returns an error for invalid catalog identities, a non-regular manifest
/// root, or missing, unexpected, linked, or non-file manifest artifacts.
pub fn new(
manifests_root: impl Into<PathBuf>,
expected_versions: BTreeMap<String, String>,
) -> eyre::Result<Self> {
let manifests_root = manifests_root.into();
reject_incomplete_catalog_publication(&manifests_root)?;
let manifests = CatalogManifestStore::new(&manifests_root, expected_versions.clone())?;
manifests.validate_coverage()?;
reject_incomplete_catalog_publication(&manifests_root)?;
let mut catalog = GameCatalog::empty();
for (game_id, version) in expected_versions {
catalog.insert(game_id, Some(version));
}
Ok(Self {
catalog,
manifests,
manifests_root: Some(manifests_root),
})
}
/// Constructs an immutable authority from a complete in-memory manifest
/// set.
///
/// # Errors
///
/// Returns an error if any sealed manifest is invalid, if game IDs collide
/// exactly or under portable case-folding, or if catalog limits are
/// exceeded.
pub fn from_manifests(
manifests: impl IntoIterator<Item = CatalogContentManifest>,
) -> eyre::Result<Self> {
let manifests = CatalogManifestStore::from_manifests(manifests)?;
let mut catalog = GameCatalog::empty();
for (game_id, version) in manifests.expected_versions() {
catalog.insert(game_id.clone(), Some(version.clone()));
}
Ok(Self {
catalog,
manifests,
manifests_root: None,
})
}
/// Returns the exact ID/version catalog used by peer policy.
#[must_use]
pub const fn catalog(&self) -> &GameCatalog {
&self.catalog
}
/// Returns one catalog-owned content identity without filesystem access.
#[must_use]
pub fn content_identity(&self, game_id: &str) -> Option<CatalogContentIdentity> {
self.manifests.content_identity(game_id)
}
/// Loads and fully validates one manifest on demand.
///
/// # Errors
///
/// Returns an error if the ID is unknown or its artifact is invalid.
pub fn manifest(&self, game_id: &str) -> eyre::Result<Arc<CatalogContentManifest>> {
self.reject_incomplete_disk_publication()?;
let result = self.manifests.load(game_id);
self.reject_incomplete_disk_publication()?;
result
}
/// Returns a previously validated manifest without performing filesystem
/// I/O or parsing.
///
/// # Errors
///
/// Returns an error for an unknown or not-yet-loaded manifest.
pub fn cached_manifest(&self, game_id: &str) -> eyre::Result<Arc<CatalogContentManifest>> {
self.manifests.load_cached(game_id)
}
/// Eagerly validates exact coverage and every body against the compact
/// index, rejecting an overlapping or interrupted disk publication.
///
/// # Errors
///
/// Returns an error for a publication marker, invalid coverage, an invalid
/// body, or an index/body identity mismatch.
pub fn validate_all(&self) -> eyre::Result<()> {
self.reject_incomplete_disk_publication()?;
let result = self.manifests.validate_all();
self.reject_incomplete_disk_publication()?;
result
}
fn reject_incomplete_disk_publication(&self) -> eyre::Result<()> {
if let Some(root) = &self.manifests_root {
reject_incomplete_catalog_publication(root)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{
fs,
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
use super::*;
use crate::content_manifest::{
Blake3Digest,
CATALOG_CONTENT_INDEX_NAME,
CatalogContentIndex,
CatalogContentManifestBody,
CatalogFileEntry,
write_canonical_content_index_atomic,
write_canonical_manifest_atomic,
};
fn manifest(id: &str) -> CatalogContentManifest {
let version = "20240101";
let digest = Blake3Digest::hash(version.as_bytes());
CatalogContentManifest::seal(
CatalogContentManifestBody::new(
id,
version,
vec![
CatalogFileEntry::file(
"version.ini",
u64::try_from(version.len()).expect("version length should fit u64"),
digest,
vec![digest],
)
.expect("version.ini entry should be valid"),
],
Vec::new(),
)
.expect("test body should be valid"),
)
.expect("test manifest should seal")
}
static TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
struct TestDir(PathBuf);
impl TestDir {
fn new() -> Self {
let sequence = TEST_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should follow epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!(
"lanspread-catalog-authority-{}-{nanos}-{sequence}",
std::process::id()
));
fs::create_dir(&path).expect("test directory should be created");
Self(path)
}
}
impl Drop for TestDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn bundle_pairs_exact_versions_without_eager_manifest_parsing() {
let root = TestDir::new();
let valid_manifest = manifest("g");
let index = CatalogContentIndex::from_manifests([&valid_manifest])
.expect("test index should validate");
write_canonical_content_index_atomic(&root.0.join(CATALOG_CONTENT_INDEX_NAME), &index)
.expect("test index should publish");
fs::write(root.0.join("g.json"), b"not JSON\n").expect("opaque artifact should write");
let bundle = CatalogBundle::new(
&root.0,
BTreeMap::from([("g".to_owned(), "20240101".to_owned())]),
)
.expect("bundle construction should only validate coverage");
assert!(bundle.catalog().contains("g"));
assert_eq!(bundle.catalog().expected_version("g"), Some("20240101"));
assert_eq!(
bundle.content_identity("g"),
Some(CatalogContentIdentity::from_manifest(&valid_manifest))
);
assert!(bundle.cached_manifest("g").is_err());
assert!(bundle.manifest("g").is_err());
assert!(bundle.cached_manifest("g").is_err());
}
#[test]
fn bundle_rejects_missing_or_non_directory_manifest_root() {
let root = TestDir::new();
let expected = BTreeMap::from([("g".to_owned(), "20240101".to_owned())]);
assert!(CatalogBundle::new(root.0.join("missing"), expected.clone()).is_err());
let file = root.0.join("file");
fs::write(&file, b"not a directory\n").expect("file should write");
assert!(CatalogBundle::new(file, expected).is_err());
}
#[test]
fn bundle_rejects_incomplete_publication_without_parsing_manifests() {
let root = TestDir::new();
fs::write(root.0.join("g.json"), b"not JSON\n").expect("opaque artifact should write");
fs::write(
root.0.join(super::super::CATALOG_PUBLICATION_MARKER_NAME),
b"lanspread catalog publication v1\n",
)
.expect("publication marker should write");
let error = CatalogBundle::new(
&root.0,
BTreeMap::from([("g".to_owned(), "20240101".to_owned())]),
)
.expect_err("runtime authority must reject an interrupted publication");
assert!(error.to_string().contains("publication is incomplete"));
}
#[test]
fn disk_bundle_rechecks_publication_marker_without_invalidating_cached_snapshot() {
let root = TestDir::new();
let valid_manifest = manifest("g");
write_canonical_manifest_atomic(&root.0.join("g.json"), &valid_manifest)
.expect("test manifest should publish");
let index = CatalogContentIndex::from_manifests([&valid_manifest])
.expect("test index should validate");
write_canonical_content_index_atomic(&root.0.join(CATALOG_CONTENT_INDEX_NAME), &index)
.expect("test index should publish");
let bundle = CatalogBundle::new(
&root.0,
BTreeMap::from([("g".to_owned(), "20240101".to_owned())]),
)
.expect("complete disk bundle should construct");
let loaded = bundle.manifest("g").expect("manifest should preload");
fs::write(
root.0.join(super::super::CATALOG_PUBLICATION_MARKER_NAME),
b"lanspread catalog publication v1\n",
)
.expect("publication marker should write");
assert!(bundle.manifest("g").is_err());
assert!(bundle.validate_all().is_err());
assert!(Arc::ptr_eq(
&loaded,
&bundle
.cached_manifest("g")
.expect("explicit cache-only snapshot should remain immutable")
));
}
#[cfg(unix)]
#[test]
fn bundle_rejects_symlink_manifest_root() {
use std::os::unix::fs::symlink;
let root = TestDir::new();
let real = root.0.join("real");
fs::create_dir(&real).expect("real root should be created");
fs::write(real.join("g.json"), b"opaque\n").expect("artifact should write");
let linked = root.0.join("linked");
symlink(&real, &linked).expect("root symlink should be created");
assert!(
CatalogBundle::new(
linked,
BTreeMap::from([("g".to_owned(), "20240101".to_owned())])
)
.is_err()
);
}
#[test]
fn in_memory_bundle_is_exact_and_loadable() {
let manifest = manifest("g");
let bundle = CatalogBundle::from_manifests([manifest.clone()])
.expect("complete in-memory authority should load");
assert_eq!(bundle.catalog().expected_version("g"), Some("20240101"));
assert_eq!(
bundle.content_identity("g"),
Some(CatalogContentIdentity::from_manifest(&manifest))
);
assert_eq!(
bundle
.manifest("g")
.expect("known in-memory manifest should load")
.as_ref(),
&manifest
);
assert!(bundle.manifest("missing").is_err());
}
#[test]
fn in_memory_bundle_rejects_portable_aliases() {
assert!(CatalogBundle::from_manifests([manifest("game"), manifest("GAME")]).is_err());
}
}