diff --git a/crates/lanspread-peer/src/download/manifest.rs b/crates/lanspread-peer/src/download/manifest.rs index fecc0dc..8d304b2 100644 --- a/crates/lanspread-peer/src/download/manifest.rs +++ b/crates/lanspread-peer/src/download/manifest.rs @@ -7,6 +7,8 @@ use std::{ use eyre::WrapErr; use lanspread_db::db::{GameCatalog, GameFileDescription}; +use crate::game_paths::{VERSION_INI, is_download_protected_root_name, portable_name_key}; + /// A remote manifest may describe at most this many filesystem entries. pub(crate) const MAX_DOWNLOAD_MANIFEST_ENTRIES: usize = 100_000; /// A single remotely described file may be at most one tebibyte. @@ -21,8 +23,6 @@ pub(crate) const MAX_DOWNLOAD_COMPONENT_BYTES: usize = 255; pub(crate) const MAX_DOWNLOAD_RELATIVE_PATH_BYTES: usize = 900; const MAX_DOWNLOAD_DESTINATION_UNITS: usize = 1_000; -const VERSION_INI: &str = "version.ini"; - /// One entry whose path and shape were validated as part of a complete manifest. #[derive(Clone, Debug)] pub(crate) struct ValidatedDownloadEntry { @@ -259,7 +259,7 @@ impl<'a> ProtocolV7ManifestBuilder<'a> { if windows_alias_component(root_component)? == self.game_alias { eyre::bail!("download path contains a doubled game prefix: {display_path}"); } - if is_protected_root_component(root_component) { + if is_download_protected_root_name(root_component) { eyre::bail!("download path targets install or recovery state: {display_path}"); } Ok(()) @@ -337,7 +337,7 @@ fn validate_game_id(game_id: &str) -> eyre::Result<()> { eyre::bail!("catalog game ID must be one path component: {game_id}"); } validate_component(game_id)?; - if is_protected_root_component(game_id) { + if is_download_protected_root_name(game_id) { eyre::bail!("catalog game ID is reserved for application state: {game_id}"); } Ok(()) @@ -447,7 +447,7 @@ fn validate_component(component: &str) -> eyre::Result<()> { fn windows_alias_component(component: &str) -> eyre::Result { validate_component(component)?; - Ok(component.to_uppercase()) + Ok(portable_name_key(component)) } fn is_windows_device_name(stem: &str) -> bool { @@ -472,25 +472,6 @@ fn looks_like_dos_short_name(component: &str) -> bool { }) } -fn is_protected_root_component(component: &str) -> bool { - let alias = component.to_uppercase(); - alias == "LOCAL" - || alias.starts_with(".LOCAL.") - || alias.starts_with(".VERSION.INI.") - || matches!( - alias.as_str(), - ".SYNC" - | ".LANSPREAD" - | ".LANSPREAD.JSON" - | ".LANSPREAD.JSON.TMP" - | ".LANSPREAD_OWNED" - | ".SOFTLAN_FIRST_START_DONE" - | ".SOFTLAN_GAME_INSTALLED" - | "INSTALL_INTENT.JSON" - | "INSTALL_INTENT.JSON.TMP" - ) -} - fn validate_entry_shape( shapes: &mut BTreeMap, alias_path: &str, diff --git a/crates/lanspread-peer/src/download/storage.rs b/crates/lanspread-peer/src/download/storage.rs index d8c7d70..8c0930d 100644 --- a/crates/lanspread-peer/src/download/storage.rs +++ b/crates/lanspread-peer/src/download/storage.rs @@ -3,9 +3,7 @@ use std::{io::ErrorKind, path::Path}; use tokio::fs::OpenOptions; use super::manifest::ValidatedDownloadManifest; -use crate::local_games::is_local_dir_name; - -const SYNC_DIR: &str = ".sync"; +use crate::game_paths::is_preserved_on_download_discard; /// Prepares storage for game files by creating directories and pre-allocating files. pub(super) async fn prepare_game_storage(manifest: &ValidatedDownloadManifest) -> eyre::Result<()> { @@ -90,7 +88,7 @@ pub(super) async fn discard_cancelled_download( } fn should_preserve_on_download_discard(name: &str) -> bool { - is_local_dir_name(name) || name.starts_with(".local.") || name == SYNC_DIR + is_preserved_on_download_discard(name) } async fn remove_entry(path: &Path) -> eyre::Result<()> { diff --git a/crates/lanspread-peer/src/download/version_ini.rs b/crates/lanspread-peer/src/download/version_ini.rs index 6d703f8..b4861f0 100644 --- a/crates/lanspread-peer/src/download/version_ini.rs +++ b/crates/lanspread-peer/src/download/version_ini.rs @@ -3,6 +3,8 @@ use std::path::Path; use lanspread_db::db::GameFileDescription; use tokio::{io::AsyncWriteExt, sync::Mutex}; +use crate::game_paths::{VERSION_DISCARDED_FILE, VERSION_INI, VERSION_TMP_FILE}; + #[derive(Debug)] pub(super) struct VersionIniBuffer { relative_path: String, @@ -48,27 +50,27 @@ impl VersionIniBuffer { pub(super) async fn begin_version_ini_transaction(game_root: &Path) -> eyre::Result<()> { tokio::fs::create_dir_all(game_root).await?; - remove_file_if_exists(&game_root.join(".version.ini.tmp")).await?; - remove_file_if_exists(&game_root.join(".version.ini.discarded")).await?; + remove_file_if_exists(&game_root.join(VERSION_TMP_FILE)).await?; + remove_file_if_exists(&game_root.join(VERSION_DISCARDED_FILE)).await?; - let version_path = game_root.join("version.ini"); + let version_path = game_root.join(VERSION_INI); if tokio::fs::metadata(&version_path) .await .is_ok_and(|metadata| metadata.is_file()) { - tokio::fs::rename(version_path, game_root.join(".version.ini.discarded")).await?; + tokio::fs::rename(version_path, game_root.join(VERSION_DISCARDED_FILE)).await?; } Ok(()) } pub(super) async fn rollback_version_ini_transaction(game_root: &Path) { - if let Err(err) = remove_file_if_exists(&game_root.join(".version.ini.tmp")).await { + if let Err(err) = remove_file_if_exists(&game_root.join(VERSION_TMP_FILE)).await { log::warn!( "Failed to sweep partial version.ini tmp in {}: {err}", game_root.display() ); } - if let Err(err) = remove_file_if_exists(&game_root.join(".version.ini.discarded")).await { + if let Err(err) = remove_file_if_exists(&game_root.join(VERSION_DISCARDED_FILE)).await { log::warn!( "Failed to sweep discarded version.ini in {}: {err}", game_root.display() @@ -80,8 +82,8 @@ pub(super) async fn commit_version_ini_buffer( game_root: &Path, buffer: &VersionIniBuffer, ) -> eyre::Result<()> { - let tmp_path = game_root.join(".version.ini.tmp"); - let version_path = game_root.join("version.ini"); + let tmp_path = game_root.join(VERSION_TMP_FILE); + let version_path = game_root.join(VERSION_INI); let bytes = buffer.snapshot().await; let mut file = tokio::fs::File::create(&tmp_path).await?; @@ -91,7 +93,7 @@ pub(super) async fn commit_version_ini_buffer( tokio::fs::rename(&tmp_path, &version_path).await?; sync_parent_dir(&version_path)?; - remove_file_if_exists(&game_root.join(".version.ini.discarded")).await?; + remove_file_if_exists(&game_root.join(VERSION_DISCARDED_FILE)).await?; Ok(()) } diff --git a/crates/lanspread-peer/src/game_paths.rs b/crates/lanspread-peer/src/game_paths.rs new file mode 100644 index 0000000..fda8a3f --- /dev/null +++ b/crates/lanspread-peer/src/game_paths.rs @@ -0,0 +1,125 @@ +//! Shared names and ownership policy for entries below a game root. + +pub(crate) const LOCAL_DIR: &str = "local"; +pub(crate) const INSTALLING_DIR: &str = ".local.installing"; +pub(crate) const BACKUP_DIR: &str = ".local.backup"; +pub(crate) const INSTALL_OWNED_MARKER: &str = ".lanspread_owned"; +pub(crate) const VERSION_INI: &str = "version.ini"; +pub(crate) const VERSION_TMP_FILE: &str = ".version.ini.tmp"; +pub(crate) const VERSION_DISCARDED_FILE: &str = ".version.ini.discarded"; +pub(crate) const LEGACY_LIBRARY_INDEX_DIR: &str = ".lanspread"; +pub(crate) const LEGACY_INTENT_FILE: &str = ".lanspread.json"; +pub(crate) const LEGACY_INTENT_TMP_FILE: &str = ".lanspread.json.tmp"; +pub(crate) const LEGACY_FIRST_START_DONE_FILE: &str = ".softlan_first_start_done"; +pub(crate) const LEGACY_SOFTLAN_INSTALL_MARKER: &str = ".softlan_game_installed"; +pub(crate) const INSTALL_INTENT_FILE: &str = "install_intent.json"; +pub(crate) const INSTALL_INTENT_TMP_FILE: &str = "install_intent.json.tmp"; + +/// Returns the conservative cross-platform comparison key used for reserved names. +pub(crate) fn portable_name_key(name: &str) -> String { + name.to_uppercase() +} + +/// Matches the committed install directory according to the host filesystem. +#[cfg(target_os = "windows")] +pub(crate) fn is_local_dir_name(name: &str) -> bool { + name.eq_ignore_ascii_case(LOCAL_DIR) +} + +/// Matches the committed install directory according to the host filesystem. +#[cfg(not(target_os = "windows"))] +pub(crate) fn is_local_dir_name(name: &str) -> bool { + name == LOCAL_DIR +} + +/// Returns whether a top-level entry belongs to install, recovery, or legacy state. +/// +/// This deliberately uses a conservative platform-independent comparison because +/// a manifest accepted on one peer may be materialized on another operating system. +pub(crate) fn is_download_protected_root_name(name: &str) -> bool { + let key = portable_name_key(name); + key == "LOCAL" + || key.starts_with(".LOCAL.") + || key.starts_with(".VERSION.INI.") + || matches!( + key.as_str(), + ".SYNC" + | ".LANSPREAD" + | ".LANSPREAD.JSON" + | ".LANSPREAD.JSON.TMP" + | ".LANSPREAD_OWNED" + | ".SOFTLAN_FIRST_START_DONE" + | ".SOFTLAN_GAME_INSTALLED" + | "INSTALL_INTENT.JSON" + | "INSTALL_INTENT.JSON.TMP" + ) +} + +/// Returns whether cancellation cleanup must preserve an application-owned entry. +/// +/// Version transaction scratch files are intentionally excluded: they belong to +/// the cancelled download and are safe to sweep. Install and migration state is +/// independent of that download and must survive it. +pub(crate) fn is_preserved_on_download_discard(name: &str) -> bool { + let key = portable_name_key(name); + key == "LOCAL" + || key.starts_with(".LOCAL.") + || matches!( + key.as_str(), + ".SYNC" + | ".LANSPREAD" + | ".LANSPREAD.JSON" + | ".LANSPREAD.JSON.TMP" + | ".LANSPREAD_OWNED" + | ".SOFTLAN_FIRST_START_DONE" + | ".SOFTLAN_GAME_INSTALLED" + | "INSTALL_INTENT.JSON" + | "INSTALL_INTENT.JSON.TMP" + ) +} + +/// Returns whether an entry in the configured games directory is application state. +pub(crate) fn is_ignored_games_root_name(name: &str) -> bool { + portable_name_key(name) == ".LANSPREAD" +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn protected_policy_covers_current_and_legacy_state() { + for name in [ + LOCAL_DIR, + "LOCAL", + INSTALLING_DIR, + BACKUP_DIR, + VERSION_TMP_FILE, + VERSION_DISCARDED_FILE, + ".sync", + LEGACY_LIBRARY_INDEX_DIR, + LEGACY_INTENT_FILE, + LEGACY_INTENT_TMP_FILE, + INSTALL_OWNED_MARKER, + LEGACY_FIRST_START_DONE_FILE, + LEGACY_SOFTLAN_INSTALL_MARKER, + INSTALL_INTENT_FILE, + INSTALL_INTENT_TMP_FILE, + ".ſync", + ] { + assert!(is_download_protected_root_name(name), "missed {name}"); + } + assert!(!is_download_protected_root_name(VERSION_INI)); + assert!(!is_download_protected_root_name("archive.eti")); + } + + #[test] + fn cancellation_policy_sweeps_only_download_transaction_state() { + assert!(is_preserved_on_download_discard(LOCAL_DIR)); + assert!(is_preserved_on_download_discard(BACKUP_DIR)); + assert!(is_preserved_on_download_discard(LEGACY_INTENT_FILE)); + assert!(!is_preserved_on_download_discard(VERSION_INI)); + assert!(!is_preserved_on_download_discard(VERSION_TMP_FILE)); + assert!(!is_preserved_on_download_discard("archive.eti")); + } +} diff --git a/crates/lanspread-peer/src/install/intent.rs b/crates/lanspread-peer/src/install/intent.rs index 77d281d..cfabfc7 100644 --- a/crates/lanspread-peer/src/install/intent.rs +++ b/crates/lanspread-peer/src/install/intent.rs @@ -6,11 +6,12 @@ use std::{ use serde::{Deserialize, Serialize}; use tokio::io::AsyncWriteExt; +use crate::game_paths::{ + INSTALL_INTENT_FILE as INTENT_FILE, + INSTALL_INTENT_TMP_FILE as INTENT_TMP_FILE, +}; + const INTENT_SCHEMA_VERSION: u32 = 1; -pub(crate) const LEGACY_INTENT_FILE: &str = ".lanspread.json"; -pub(crate) const LEGACY_INTENT_TMP_FILE: &str = ".lanspread.json.tmp"; -const INTENT_FILE: &str = "install_intent.json"; -const INTENT_TMP_FILE: &str = "install_intent.json.tmp"; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum InstallIntentState { diff --git a/crates/lanspread-peer/src/install/remove.rs b/crates/lanspread-peer/src/install/remove.rs index 927ee70..14b11f2 100644 --- a/crates/lanspread-peer/src/install/remove.rs +++ b/crates/lanspread-peer/src/install/remove.rs @@ -6,10 +6,7 @@ use std::{ use eyre::{WrapErr, bail}; -const LOCAL_DIR: &str = "local"; -const INSTALLING_DIR: &str = ".local.installing"; -const BACKUP_DIR: &str = ".local.backup"; -const VERSION_INI: &str = "version.ini"; +use crate::game_paths::{BACKUP_DIR, INSTALLING_DIR, LOCAL_DIR, VERSION_INI}; /// Remove the downloaded files for an uninstalled game root. /// diff --git a/crates/lanspread-peer/src/install/transaction.rs b/crates/lanspread-peer/src/install/transaction.rs index eb9fc66..e6d5719 100644 --- a/crates/lanspread-peer/src/install/transaction.rs +++ b/crates/lanspread-peer/src/install/transaction.rs @@ -11,14 +11,18 @@ use super::{ intent::{InstallIntent, InstallIntentState, read_intent, write_intent}, unpack::Unpacker, }; -use crate::{local_games::version_ini_is_regular_file, state_paths::launch_settings_applied_path}; - -const LOCAL_DIR: &str = "local"; -const INSTALLING_DIR: &str = ".local.installing"; -const BACKUP_DIR: &str = ".local.backup"; -const OWNED_MARKER: &str = ".lanspread_owned"; -const VERSION_TMP_FILE: &str = ".version.ini.tmp"; -const VERSION_DISCARDED_FILE: &str = ".version.ini.discarded"; +use crate::{ + game_paths::{ + BACKUP_DIR, + INSTALL_OWNED_MARKER, + INSTALLING_DIR, + LOCAL_DIR, + VERSION_DISCARDED_FILE, + VERSION_TMP_FILE, + }, + local_games::version_ini_is_regular_file, + state_paths::launch_settings_applied_path, +}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum FsEntryState { @@ -672,7 +676,7 @@ fn backup_dir(game_root: &Path) -> PathBuf { } fn owned_marker(path: &Path) -> PathBuf { - path.join(OWNED_MARKER) + path.join(INSTALL_OWNED_MARKER) } impl From for FsEntryState { diff --git a/crates/lanspread-peer/src/launch_settings.rs b/crates/lanspread-peer/src/launch_settings.rs index 85dd83c..07e1f2f 100644 --- a/crates/lanspread-peer/src/launch_settings.rs +++ b/crates/lanspread-peer/src/launch_settings.rs @@ -20,9 +20,8 @@ use std::{ use eyre::WrapErr; -use crate::state_paths::launch_settings_applied_path; +use crate::{game_paths::LOCAL_DIR, state_paths::launch_settings_applied_path}; -const LOCAL_DIR: &str = "local"; const ACCOUNT_NAME_FILE: &str = "account_name.txt"; const LANGUAGE_FILE: &str = "language.txt"; const SMART_STEAM_EMU_INI: &str = "SmartSteamEmu.ini"; diff --git a/crates/lanspread-peer/src/lib.rs b/crates/lanspread-peer/src/lib.rs index d510632..8d1747f 100644 --- a/crates/lanspread-peer/src/lib.rs +++ b/crates/lanspread-peer/src/lib.rs @@ -18,6 +18,7 @@ mod context; mod download; mod error; mod events; +mod game_paths; mod handlers; mod identity; mod install; diff --git a/crates/lanspread-peer/src/local_games.rs b/crates/lanspread-peer/src/local_games.rs index 31d8c0c..fbdaf10 100644 --- a/crates/lanspread-peer/src/local_games.rs +++ b/crates/lanspread-peer/src/local_games.rs @@ -14,25 +14,25 @@ use lanspread_proto::{Availability, GameSummary}; use serde::{Deserialize, Serialize}; use tokio::{io::AsyncWriteExt, sync::Mutex}; -use crate::{context::OperationKind, error::PeerError}; +use crate::{ + context::OperationKind, + error::PeerError, + game_paths::{ + LEGACY_LIBRARY_INDEX_DIR, + LOCAL_DIR, + VERSION_INI, + is_download_protected_root_name, + is_ignored_games_root_name, + }, +}; // ============================================================================= // Local directory helpers // ============================================================================= -#[cfg(target_os = "windows")] -pub fn is_local_dir_name(name: &str) -> bool { - name.eq_ignore_ascii_case("local") -} - -#[cfg(not(target_os = "windows"))] -pub fn is_local_dir_name(name: &str) -> bool { - name == "local" -} - /// Checks if `local/` is a committed install directory. pub async fn local_dir_is_directory(path: &Path) -> bool { - let local_dir = path.join("local"); + let local_dir = path.join(LOCAL_DIR); tokio::fs::metadata(&local_dir) .await .is_ok_and(|metadata| metadata.is_dir()) @@ -40,7 +40,7 @@ pub async fn local_dir_is_directory(path: &Path) -> bool { /// Checks if the root-level `version.ini` sentinel exists as a regular file. pub async fn version_ini_is_regular_file(game_path: &Path) -> bool { - let version_path = game_path.join("version.ini"); + let version_path = game_path.join(VERSION_INI); tokio::fs::metadata(&version_path) .await .is_ok_and(|metadata| metadata.is_file()) @@ -105,11 +105,7 @@ pub async fn local_download_matches_catalog( // Local library index and scanning // ============================================================================= -const LEGACY_LIBRARY_INDEX_DIR: &str = ".lanspread"; const LIBRARY_INDEX_FILE: &str = "library_index.json"; -const INTENT_LOG_FILE: &str = ".lanspread.json"; -const VERSION_TMP_FILE: &str = ".version.ini.tmp"; -const VERSION_DISCARDED_FILE: &str = ".version.ini.discarded"; static LIBRARY_INDEX_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); @@ -289,7 +285,7 @@ async fn root_eti_fingerprints(game_path: &Path) -> eyre::Result eyre::Result { let eti_files = root_eti_fingerprints(game_path).await?; - let version_path = game_path.join("version.ini"); + let version_path = game_path.join(VERSION_INI); let (version_mtime, version_contents) = match tokio::fs::metadata(&version_path).await { Ok(metadata) if metadata.is_file() => { let contents = match tokio::fs::read_to_string(&version_path).await { @@ -317,40 +313,15 @@ async fn fingerprint_game_dir(game_path: &Path) -> eyre::Result }) } -pub fn is_ignored_game_root_name(name: &str) -> bool { - name == LEGACY_LIBRARY_INDEX_DIR -} - -fn is_reserved_transient_name(name: &str) -> bool { - name.starts_with(".local.") - || name == VERSION_TMP_FILE - || name == VERSION_DISCARDED_FILE - || name == INTENT_LOG_FILE - || name == LEGACY_LIBRARY_INDEX_DIR -} - fn should_skip_root_entry(entry: &walkdir::DirEntry) -> bool { if entry.depth() != 1 { return false; } - if entry.file_type().is_dir() && entry.file_name().to_str().is_some_and(is_local_dir_name) { - return true; - } - - if let Some(name) = entry.file_name().to_str() { - if is_reserved_transient_name(name) { - return true; - } - if entry.file_type().is_dir() && name == ".sync" { - return true; - } - if entry.file_type().is_file() && name == ".softlan_game_installed" { - return true; - } - } - - false + entry + .file_name() + .to_str() + .is_some_and(is_download_protected_root_name) } fn canonical_protocol_path(path: &Path) -> eyre::Result { @@ -659,7 +630,7 @@ pub async fn scan_local_library( let Some(game_id) = path.file_name().and_then(|n| n.to_str()) else { continue; }; - if is_ignored_game_root_name(game_id) { + if is_ignored_games_root_name(game_id) { continue; } diff --git a/crates/lanspread-peer/src/migration.rs b/crates/lanspread-peer/src/migration.rs index a23b8c4..019a5a3 100644 --- a/crates/lanspread-peer/src/migration.rs +++ b/crates/lanspread-peer/src/migration.rs @@ -8,20 +8,19 @@ use futures::{StreamExt as _, stream}; use tokio::io::AsyncWriteExt as _; use crate::{ - install::intent::{ - InstallIntent, + game_paths::{ + LEGACY_FIRST_START_DONE_FILE, LEGACY_INTENT_FILE, LEGACY_INTENT_TMP_FILE, - intent_path, - write_intent, + LEGACY_LIBRARY_INDEX_DIR, + LEGACY_SOFTLAN_INSTALL_MARKER, + is_ignored_games_root_name, }, - local_games::{is_ignored_game_root_name, legacy_library_index_path}, + install::intent::{InstallIntent, intent_path, write_intent}, + local_games::legacy_library_index_path, state_paths::{local_library_index_path, setup_done_path}, }; -const LEGACY_LIBRARY_INDEX_DIR: &str = ".lanspread"; -const LEGACY_FIRST_START_DONE_FILE: &str = ".softlan_first_start_done"; -const LEGACY_SOFTLAN_INSTALL_MARKER: &str = ".softlan_game_installed"; const MIGRATION_CONCURRENCY: usize = 16; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize)] @@ -97,7 +96,7 @@ async fn collect_game_roots(game_dir: &Path) -> std::io::Result eyre::Result> { let Some(name) = entry.file_name().to_str().map(ToOwned::to_owned) else { continue; }; - if is_ignored_game_root_name(&name) { + if is_ignored_games_root_name(&name) { continue; } roots.push(entry.path()); @@ -303,7 +299,7 @@ fn game_id_from_event_path(game_dir: &Path, path: &Path) -> Option { let relative = path.strip_prefix(game_dir).ok()?; let mut components = relative.components(); let game_id = component_name(components.next()?)?; - if is_ignored_game_root_name(game_id) { + if is_ignored_games_root_name(game_id) { return None; } @@ -324,13 +320,7 @@ fn component_name(component: Component<'_>) -> Option<&str> { } fn should_ignore_game_child(name: &str) -> bool { - is_local_dir_name(name) - || name.starts_with(".local.") - || name.starts_with(".version.ini.") - || name == ".lanspread" - || name == ".lanspread.json" - || name == ".sync" - || name == ".softlan_game_installed" + is_download_protected_root_name(name) } #[cfg(test)] diff --git a/crates/lanspread-peer/src/services/stream.rs b/crates/lanspread-peer/src/services/stream.rs index 240cf4f..5255f10 100644 --- a/crates/lanspread-peer/src/services/stream.rs +++ b/crates/lanspread-peer/src/services/stream.rs @@ -12,7 +12,8 @@ use crate::{ context::PeerCtx, error::PeerError, events, - local_games::{get_game_file_descriptions, is_local_dir_name, local_download_matches_catalog}, + game_paths::is_local_dir_name, + local_games::{get_game_file_descriptions, local_download_matches_catalog}, peer::{send_game_file_chunk, send_game_file_data}, services::handshake::{HandshakeCtx, accept_inbound_hello, spawn_library_resync}, stream_install::{send_game_install_stream, send_stream_install_error},