fix(peer): sanitize legacy library index migration

The selected game root could supply an unbounded legacy library index whose
revision was copied verbatim into app-owned state. A revision of `u64::MAX`
made every later checked revision advance fail even after the original root was
removed.

Read only a bounded regular, non-link file whose identity stays stable across
the read. Deserialize it before publication, preserve its cached game data, and
reset its stale revision authority to zero. Existing app-owned state still wins
without reading the legacy source, and invalid input remains in place for
recovery.

Test Plan:
- `just test` -- passed outside the sandbox; 494 peer tests and all workspace
  targets passed.
- The initial sandboxed `just test` reached an unrelated Unix-socket permission
  denial, then passed unchanged with the required socket permission.
- `git diff --cached --check` -- passed.
This commit is contained in:
ddidderr committed 2026-09-12 12:19:50 +02:00
1 parent 0ed04f64e9
commit 7de373afb6
2 files changed
+174 -10

No files matched your search

+31
View File
@@ -191,6 +191,14 @@ struct LibraryIndex {
games: HashMap<String, GameIndexEntry>,
}
/// Converts a legacy selected-root cache into app-owned state without carrying
/// its stale revision authority across the trust boundary.
pub(crate) fn normalize_migrated_library_index(bytes: &[u8]) -> serde_json::Result<Vec<u8>> {
let mut index: LibraryIndex = serde_json::from_slice(bytes)?;
index.revision = 0;
serde_json::to_vec(&index)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct GameIndexEntry {
summary: LocalGameSummary,
@@ -1043,6 +1051,29 @@ mod tests {
}
}
#[test]
fn migrated_library_index_preserves_games_but_resets_revision_authority() {
let original = test_library_index(u64::MAX, "game", 77);
let bytes = serde_json::to_vec(&original).expect("legacy index should serialize");
let normalized =
normalize_migrated_library_index(&bytes).expect("legacy index should normalize");
let migrated: LibraryIndex =
serde_json::from_slice(&normalized).expect("normalized index should deserialize");
assert_eq!(migrated.revision, 0);
assert_eq!(migrated.games.len(), 1);
assert_eq!(
migrated
.games
.get("game")
.expect("game should be preserved")
.summary
.size,
77,
);
}
#[test]
fn legacy_fingerprint_defaults_download_recovery_to_false() {
let fingerprint: GameFingerprint = serde_json::from_value(serde_json::json!({