feat(peer): validate manifests before download mutation

Why:
- Remote and UI-echoed file descriptions could reach transaction and storage
  code one entry at a time, so a hostile late path could mutate earlier files.
- Per-file consensus also accepted malformed peer lists and let duplicate rows
  inflate a source's vote.

What:
- Add a complete protocol-7 manifest adapter with catalog-root confinement,
  portable path and alias rules, reserved-path protection, shape and size caps,
  symlink/reparse inspection, and zero-mutation tests.
- Keep download selection in the peer core, validate every peer manifest before
  consensus, and pass only the validated manifest into storage/orchestration.
- Canonicalize locally advertised paths, cap exact chunk receives, and preserve
  the local-only install fast path.
- Record the chosen safety limits and follow-up ownership/catalog decisions.

Test Plan:
- just clippy
- just test
- just frontend-test
- just build
- just fmt (Rust/TOML/Prettier completed; rumdl reports 39 pre-existing issues)
- git diff --cached --check
This commit is contained in:
2026-08-09 17:51:11 +02:00
parent 9268de2371
commit a6ed60a538
12 changed files with 1376 additions and 137 deletions
@@ -14,7 +14,7 @@ use std::{
use eyre::bail;
use lanspread_compat::eti::get_games;
use lanspread_db::db::{Availability, Game, GameCatalog, GameDB, GameFileDescription};
use lanspread_db::db::{Availability, Game, GameCatalog, GameDB};
use lanspread_peer::{
ActiveOperation,
ActiveOperationKind,
@@ -2249,9 +2249,9 @@ async fn handle_peer_event(app_handle: &AppHandle, event: PeerEvent) {
}
PeerEvent::GotGameFiles {
id,
file_descriptions,
file_descriptions: _,
} => {
handle_got_game_files(app_handle, id, file_descriptions).await;
handle_got_game_files(app_handle, id).await;
}
PeerEvent::NoPeersHaveGame { id } => {
log::warn!("PeerEvent::NoPeersHaveGame received for {id}");
@@ -2382,20 +2382,13 @@ async fn handle_peer_event(app_handle: &AppHandle, event: PeerEvent) {
}
}
async fn handle_got_game_files(
app_handle: &AppHandle,
id: String,
file_descriptions: Vec<GameFileDescription>,
) {
async fn handle_got_game_files(app_handle: &AppHandle, id: String) {
log::info!("PeerEvent::GotGameFiles received");
let state = app_handle.state::<LanSpreadState>();
let peer_ctrl = state.peer_ctrl.read().await.clone();
if let Some(peer_ctrl) = peer_ctrl
&& let Err(e) = peer_ctrl.send(PeerCommand::DownloadGameFiles {
id,
file_descriptions,
})
&& let Err(e) = peer_ctrl.send(PeerCommand::DownloadGameFiles { id })
{
log::error!("Failed to continue queued game transfer: {e}");
}