diff --git a/crates/lanspread-peer/src/download/ownership.rs b/crates/lanspread-peer/src/download/ownership.rs index 6db95ae..f04d2e1 100644 --- a/crates/lanspread-peer/src/download/ownership.rs +++ b/crates/lanspread-peer/src/download/ownership.rs @@ -79,6 +79,10 @@ impl DownloadOwnershipRecord { validate_file_set(&self.committed_files)?; if let Some(pending) = &self.pending_files { validate_file_set(pending)?; + validate_generation_aliases( + &self.committed_files.iter().cloned().collect(), + &pending.iter().cloned().collect(), + )?; } Ok(self) } @@ -852,6 +856,12 @@ mod tests { let mut wrong_schema = valid.clone(); wrong_schema.schema_version += 1; assert!(wrong_schema.validate("game", "root").is_err()); + + let mut cross_generation_alias = valid.clone(); + cross_generation_alias.committed_files = vec!["Archive.eti".to_owned()]; + cross_generation_alias.pending_files = Some(vec!["archive.eti".to_owned()]); + assert!(cross_generation_alias.validate("game", "root").is_err()); + assert!(valid.clone().validate("other", "root").is_err()); assert!(valid.validate("game", "other-root").is_err()); } @@ -1263,6 +1273,32 @@ mod tests { assert!(!root.join("archive.eti").exists()); } + #[tokio::test] + async fn recovery_never_trusts_cross_generation_aliases() { + let games = TempDir::new("lanspread-ownership-alias-recovery-games"); + let state = TempDir::new("lanspread-ownership-alias-recovery-state"); + let root = games.game_root(); + write_file(&root.join(VERSION_INI), b"20240101"); + write_file(&root.join("archive.eti"), b"must-survive"); + seed_record( + state.path(), + games.path(), + &["Archive.eti"], + Some(&["archive.eti"]), + ) + .await; + + recover_incomplete_download(&root, state.path(), "game") + .await + .expect("invalid ownership must fail closed"); + + assert_eq!( + std::fs::read(root.join("archive.eti")).expect("payload must be preserved"), + b"must-survive" + ); + assert!(root.join(VERSION_INI).is_file()); + } + #[tokio::test] async fn committed_path_ownership_survives_external_root_recreation() { let games = TempDir::new("lanspread-ownership-recreated-root-games");