fix(peer): reject aliased ownership generations
Validate portable aliases across committed and pending ownership sets when loading persisted records. Malformed state can no longer make recovery delete the pending file through an older case-only spelling. Test Plan: - just clippy - just test - just fmt (Rust, TOML, and Prettier completed; rumdl still reports 39 pre-existing issues)
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user