From a977585f6dc6733e37b049f0a2b5258501628a4f Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 13 Aug 2026 07:13:00 +0200 Subject: [PATCH] fix(catalog): preflight package versions Full catalog generation previously prepared each game sequentially, so a version mismatch late in the catalog could surface only after earlier packages had already been extracted and hashed. Validate every selected version.ini against game.db before starting manifest preparation, while retaining the per-package validation during preparation to detect later input changes. Document the ordering and cover it with a regression where an earlier archive would fail if archive processing began before a later version mismatch. Test Plan: - `just fmt` -- passed - `just test` -- passed - `just clippy` -- passed - `git diff --cached --check` -- passed --- README.md | 11 +++++----- .../src/catalog_publisher/mod.rs | 22 +++++++++++++++---- .../src/catalog_publisher/package.rs | 2 +- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f38c045..dad0758 100644 --- a/README.md +++ b/README.md @@ -111,11 +111,12 @@ Generate the complete production manifest set: just catalog-generate-production /srv/lanspread/packages ``` -The publisher independently reads and hashes each selected package twice before -publication. Under a durable publication marker, it atomically replaces each -manifest and writes the complete content index last. Interruption leaves the -marker in place so later checks and builds fail closed instead of accepting a -mixed generation. +The publisher first checks every selected package's `version.ini` against +`game.db`, then independently reads and hashes each selected package twice +before publication. Under a durable publication marker, it atomically replaces +each manifest and writes the complete content index last. Interruption leaves +the marker in place so later checks and builds fail closed instead of accepting +a mixed generation. Verify the published database/index/manifest set without rereading the package corpus: diff --git a/crates/lanspread-compat/src/catalog_publisher/mod.rs b/crates/lanspread-compat/src/catalog_publisher/mod.rs index e106f4d..9592d45 100644 --- a/crates/lanspread-compat/src/catalog_publisher/mod.rs +++ b/crates/lanspread-compat/src/catalog_publisher/mod.rs @@ -120,6 +120,13 @@ pub async fn generate_catalog_manifests( let versions = catalog_versions(&catalog); reject_incomplete_publication(&options.manifests_dir)?; let selected = options.selection.select(&catalog)?; + for game in &selected { + package::validate_package_version( + &options.packages_dir.join(&game.game_id), + &game.game_version, + ) + .wrap_err_with(|| format!("failed to preflight package version for {}", game.game_id))?; + } let prepared = selected .into_iter() .map(|game| prepare_manifest(game, options)) @@ -766,11 +773,13 @@ mod tests { } #[tokio::test] - async fn entire_selection_is_prepared_before_any_manifest_is_published() { + async fn entire_selection_versions_are_preflighted_before_archive_work() { let temp = TempDir::new(); let catalog_db = temp.0.join("game.db"); create_catalog(&catalog_db, &[("a", "20240101"), ("b", "20240102")]).await; - simple_package(&temp, "a", "20240101"); + let a_package = simple_package(&temp, "a", "20240101"); + fs::write(a_package.join("a.eti"), b"not an archive") + .expect("archive-shaped input should be written"); simple_package(&temp, "b", "wrong-version"); let manifests_dir = temp.0.join("manifests"); fs::create_dir(&manifests_dir).expect("manifest directory should be created"); @@ -785,12 +794,17 @@ mod tests { selection: CatalogSelection::All, }) .await - .expect_err("late package failure should reject the whole selection"); + .expect_err("all versions should be checked before archive processing"); assert!( error .to_string() - .contains("failed to generate manifest for b") + .contains("failed to preflight package version for b"), + "unexpected error: {error:#}" + ); + assert!( + format!("{error:#}").contains("package version mismatch"), + "unexpected error: {error:#}" ); assert_eq!( fs::read(existing).expect("existing artifact should remain readable"), diff --git a/crates/lanspread-compat/src/catalog_publisher/package.rs b/crates/lanspread-compat/src/catalog_publisher/package.rs index 43c1535..1ea4608 100644 --- a/crates/lanspread-compat/src/catalog_publisher/package.rs +++ b/crates/lanspread-compat/src/catalog_publisher/package.rs @@ -121,7 +121,7 @@ pub(super) fn validate_manifest_destination(path: &Path) -> eyre::Result<()> { Ok(()) } -fn validate_package_version(package_root: &Path, expected: &str) -> eyre::Result<()> { +pub(super) fn validate_package_version(package_root: &Path, expected: &str) -> eyre::Result<()> { let path = package_root.join("version.ini"); let bytes = read_bounded_regular_file(&path, MAX_VERSION_INI_BYTES)?; let version = std::str::from_utf8(&bytes)