diff --git a/crates/lanspread-compat/src/catalog_publisher/mod.rs b/crates/lanspread-compat/src/catalog_publisher/mod.rs index 6aa9749..55031d3 100644 --- a/crates/lanspread-compat/src/catalog_publisher/mod.rs +++ b/crates/lanspread-compat/src/catalog_publisher/mod.rs @@ -122,7 +122,7 @@ pub async fn generate_catalog_manifests( reject_incomplete_publication(&options.manifests_dir)?; let selected = options.selection.select(&catalog)?; for game in &selected { - package::validate_package_version( + package::preflight_package_version( &options.packages_dir.join(&game.game_id), &game.game_version, ) @@ -820,6 +820,51 @@ mod tests { ); } + #[cfg(unix)] + #[tokio::test] + async fn symlink_package_root_is_rejected_during_full_selection_preflight() { + use std::os::unix::fs::symlink; + + let temp = TempDir::new(); + let catalog_db = temp.0.join("game.db"); + create_catalog(&catalog_db, &[("a", "20240101"), ("b", "20240102")]).await; + 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"); + let outside = temp.0.join("outside-b"); + fs::create_dir(&outside).expect("outside package should be created"); + fs::write(outside.join("version.ini"), "20240102") + .expect("outside version should be written"); + symlink(&outside, temp.0.join("packages/b")) + .expect("package root symlink should be created"); + let manifests_dir = temp.0.join("manifests"); + fs::create_dir(&manifests_dir).expect("manifest directory should be created"); + let existing = manifests_dir.join("a.json"); + fs::write(&existing, b"existing artifact").expect("existing artifact should be seeded"); + + let error = generate_catalog_manifests(&GenerateOptions { + catalog_db, + packages_dir: temp.0.join("packages"), + manifests_dir: manifests_dir.clone(), + unrar: PathBuf::from("missing-unrar"), + selection: CatalogSelection::All, + }) + .await + .expect_err("package root links must fail during version preflight"); + let rendered = format!("{error:#}"); + + assert!(rendered.contains("failed to preflight package version for b")); + assert!(rendered.contains("expected a regular non-link directory")); + assert_eq!( + fs::read(existing).expect("existing artifact should remain readable"), + b"existing artifact" + ); + assert!( + !manifests_dir.join(CATALOG_PUBLICATION_MARKER_NAME).exists(), + "root validation must fail before archive work or publication" + ); + } + #[tokio::test] async fn incremental_generation_requires_an_existing_complete_indexed_corpus() { let temp = TempDir::new(); @@ -894,15 +939,23 @@ mod tests { #[test] fn package_version_mismatch_is_rejected() { + const OBSERVED: &str = "package-controlled-secret-20240101"; + let temp = TempDir::new(); - let root = simple_package(&temp, "g", "20240101"); + let root = simple_package(&temp, "g", OBSERVED); let game = CatalogGame { game_id: "g".to_owned(), game_version: "20240102".to_owned(), }; let error = build_manifest_from_package(&game, &root, Path::new("missing-unrar")) .expect_err("version skew should fail"); - assert!(error.to_string().contains("package version mismatch")); + let rendered = format!("{error:#}"); + assert!(rendered.contains("package version mismatch")); + assert!(rendered.contains("game.db expects 20240102")); + assert!( + !rendered.contains(OBSERVED), + "package-controlled version contents must not enter diagnostics" + ); } #[test] diff --git a/crates/lanspread-compat/src/catalog_publisher/package.rs b/crates/lanspread-compat/src/catalog_publisher/package.rs index 85a4211..09828ed 100644 --- a/crates/lanspread-compat/src/catalog_publisher/package.rs +++ b/crates/lanspread-compat/src/catalog_publisher/package.rs @@ -122,6 +122,14 @@ pub(super) fn validate_manifest_destination(path: &Path) -> eyre::Result<()> { Ok(()) } +pub(super) fn preflight_package_version( + package_root: &Path, + expected: &str, +) -> eyre::Result<()> { + validate_regular_directory(package_root)?; + validate_package_version(package_root, expected) +} + 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)?; @@ -129,9 +137,7 @@ pub(super) fn validate_package_version(package_root: &Path, expected: &str) -> e .wrap_err("version.ini is not valid UTF-8")? .trim(); if version != expected { - eyre::bail!( - "package version mismatch: game.db expects {expected}, version.ini contains {version:?}" - ); + eyre::bail!("package version mismatch: game.db expects {expected}"); } Ok(()) }