fix(catalog): validate package roots before preflight

Require each selected package root to be a regular non-link directory before reading its bounded version.ini, and omit package-controlled observed contents from mismatch diagnostics. Full-selection preflight still completes before archive work or publication.

Test Plan:
- just test
- just clippy
- symlink-root and diagnostic-redaction regressions
- git diff --check
This commit is contained in:
2026-09-12 13:22:13 +02:00
parent 8f8072ffb6
commit 0e72de4e7a
2 changed files with 65 additions and 6 deletions
@@ -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]
@@ -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(())
}