From 76eec55103c13f3b07ead0230af4f6c64ecdd57d Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 20 Aug 2026 08:38:27 +0200 Subject: [PATCH] fix(paths): accept literal tilde-digit filenames Catalog generation and peer install/download validation rejected any path component containing a tilde followed by digits, even when the component was a valid long filename such as Bosons TD Gold~1.w3m. Remove the heuristic from all three validators and retain the existing device-name and portable-alias checks. Add a regression test for the literal filename so catalog publication and later path validation agree. Test Plan: - just clippy (passed) - just test (passed) - git diff --check (passed) --- .../lanspread-db/src/content_manifest/path.rs | 19 +++++-------------- .../lanspread-peer/src/download/manifest.rs | 13 ------------- .../src/install/mutation_root.rs | 13 ------------- 3 files changed, 5 insertions(+), 40 deletions(-) diff --git a/crates/lanspread-db/src/content_manifest/path.rs b/crates/lanspread-db/src/content_manifest/path.rs index 1d314ff..2b5bd32 100644 --- a/crates/lanspread-db/src/content_manifest/path.rs +++ b/crates/lanspread-db/src/content_manifest/path.rs @@ -184,9 +184,6 @@ fn validate_component(component: &str) -> eyre::Result<()> { if is_windows_device_name(device_stem) { eyre::bail!("catalog path uses a Windows device name: {component}"); } - if looks_like_dos_short_name(component) { - eyre::bail!("catalog path resembles a Windows short-name alias: {component}"); - } Ok(()) } @@ -206,16 +203,6 @@ fn is_windows_device_name(stem: &str) -> bool { }) } -fn looks_like_dos_short_name(component: &str) -> bool { - let stem = component.split('.').next().unwrap_or_default(); - stem.rsplit_once('~').is_some_and(|(prefix, suffix)| { - !prefix.is_empty() - && !suffix.is_empty() - && suffix.len() <= 6 - && suffix.bytes().all(|byte| byte.is_ascii_digit()) - }) -} - #[cfg(test)] mod tests { use super::*; @@ -242,7 +229,6 @@ mod tests { "NUL.txt", "com1", "LPT¹.log", - "LOCAL~1/file", "Donne\u{301}es/file", ] { assert!( @@ -252,6 +238,11 @@ mod tests { } } + #[test] + fn accepts_literal_tilde_digit_names() { + assert!(CanonicalCatalogPath::new("Bosons TD Gold~1.w3m").is_ok()); + } + #[test] fn aliases_are_conservative_across_platforms() { let left = CanonicalCatalogPath::new("Straße/FILE").expect("path should validate"); diff --git a/crates/lanspread-peer/src/download/manifest.rs b/crates/lanspread-peer/src/download/manifest.rs index 9c8c718..dd034df 100644 --- a/crates/lanspread-peer/src/download/manifest.rs +++ b/crates/lanspread-peer/src/download/manifest.rs @@ -370,9 +370,6 @@ fn validate_component(component: &str) -> eyre::Result<()> { if is_windows_device_name(device_stem) { eyre::bail!("download path uses a Windows device name: {component}"); } - if looks_like_dos_short_name(component) { - eyre::bail!("download path resembles a Windows short-name alias: {component}"); - } Ok(()) } @@ -393,16 +390,6 @@ fn is_windows_device_name(stem: &str) -> bool { }) } -fn looks_like_dos_short_name(component: &str) -> bool { - let stem = component.split('.').next().unwrap_or_default(); - stem.rsplit_once('~').is_some_and(|(prefix, suffix)| { - !prefix.is_empty() - && !suffix.is_empty() - && suffix.len() <= 6 - && suffix.bytes().all(|byte| byte.is_ascii_digit()) - }) -} - fn validate_existing_destination( game_root: &Path, components: &[&str], diff --git a/crates/lanspread-peer/src/install/mutation_root.rs b/crates/lanspread-peer/src/install/mutation_root.rs index c782b72..0e302a6 100644 --- a/crates/lanspread-peer/src/install/mutation_root.rs +++ b/crates/lanspread-peer/src/install/mutation_root.rs @@ -374,9 +374,6 @@ pub(super) fn validate_game_id(game_id: &str) -> eyre::Result<()> { if is_windows_device_name(device_stem) { eyre::bail!("game ID uses a Windows device name: {game_id}"); } - if looks_like_dos_short_name(game_id) { - eyre::bail!("game ID resembles a Windows short-name alias: {game_id}"); - } if is_download_protected_root_name(game_id) { eyre::bail!("game ID is reserved for application state: {game_id}"); } @@ -395,16 +392,6 @@ fn is_windows_device_name(stem: &str) -> bool { }) } -fn looks_like_dos_short_name(component: &str) -> bool { - let stem = component.split('.').next().unwrap_or_default(); - stem.rsplit_once('~').is_some_and(|(prefix, suffix)| { - !prefix.is_empty() - && !suffix.is_empty() - && suffix.len() <= 6 - && suffix.bytes().all(|byte| byte.is_ascii_digit()) - }) -} - fn open_ambient_directory_nofollow(path: &Path) -> eyre::Result { let directory = fs::open_ambient(path, &directory_options(), ambient_authority())?; validate_directory_handle(&directory, &path.display().to_string())?;