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:
12 files changed
+1376
-137
No files matched your search
@@ -1,15 +1,11 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
|
||||
|
||||
use lanspread_db::db::GameFileDescription;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::{
|
||||
manifest::ValidatedDownloadManifest,
|
||||
planning::{ChunkDownloadResult, DownloadChunk, build_peer_plans, extract_version_descriptor},
|
||||
progress::{DownloadProgressTracker, sample_download_progress},
|
||||
retry::{RetryContext, retry_failed_chunks},
|
||||
@@ -26,15 +22,15 @@ use crate::{PeerEvent, config::MAX_RETRY_COUNT};
|
||||
|
||||
/// Downloads all game files from available peers.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn download_game_files(
|
||||
game_id: &str,
|
||||
game_file_descs: Vec<GameFileDescription>,
|
||||
games_folder: PathBuf,
|
||||
pub(crate) async fn download_game_files(
|
||||
manifest: ValidatedDownloadManifest,
|
||||
peers: Vec<SocketAddr>,
|
||||
file_peer_map: HashMap<String, Vec<SocketAddr>>,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> eyre::Result<()> {
|
||||
let game_id = manifest.game_id().to_owned();
|
||||
let games_folder = manifest.games_folder().to_path_buf();
|
||||
if peers.is_empty() {
|
||||
eyre::bail!("no peers available for game {game_id}");
|
||||
}
|
||||
@@ -43,40 +39,41 @@ pub async fn download_game_files(
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
let (version_desc, transfer_descs) = extract_version_descriptor(game_id, game_file_descs)?;
|
||||
let game_file_descs = manifest.protocol_descriptions();
|
||||
let (version_desc, transfer_descs) = extract_version_descriptor(&game_id, game_file_descs)?;
|
||||
let version_buffer = match VersionIniBuffer::new(&version_desc) {
|
||||
Ok(buffer) => Arc::new(buffer),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let game_root = games_folder.join(game_id);
|
||||
let game_root = manifest.game_root().to_path_buf();
|
||||
|
||||
begin_version_ini_transaction(&game_root).await?;
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, game_id).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
if let Err(err) = prepare_game_storage(&games_folder, &transfer_descs).await {
|
||||
if let Err(err) = prepare_game_storage(&manifest).await {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
if cancel_token.is_cancelled() {
|
||||
discard_cancelled_download_best_effort(&games_folder, game_id).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, game_id).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin {
|
||||
id: game_id.to_string(),
|
||||
id: game_id.clone(),
|
||||
})?;
|
||||
|
||||
let progress_tracker = DownloadProgressTracker::new(total_download_bytes(&transfer_descs));
|
||||
let transfer_ctx = TransferContext {
|
||||
game_id,
|
||||
game_id: &game_id,
|
||||
games_folder: &games_folder,
|
||||
peers: &peers,
|
||||
file_peer_map: &file_peer_map,
|
||||
@@ -86,7 +83,7 @@ pub async fn download_game_files(
|
||||
progress_tracker: progress_tracker.clone(),
|
||||
};
|
||||
let transfer_result = sample_download_progress(
|
||||
game_id,
|
||||
&game_id,
|
||||
progress_tracker,
|
||||
tx_notify_ui.clone(),
|
||||
download_transfer_chunks(&transfer_ctx, &transfer_descs),
|
||||
@@ -96,14 +93,14 @@ pub async fn download_game_files(
|
||||
if let Err(err) = transfer_result {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
if cancel_token.is_cancelled() {
|
||||
discard_cancelled_download_best_effort(&games_folder, game_id).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, game_id).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user