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)
This commit is contained in:
2026-08-20 08:38:36 +02:00
parent bb5547f1b3
commit 76eec55103
3 changed files with 5 additions and 40 deletions
@@ -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");
@@ -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],
@@ -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<File> {
let directory = fs::open_ambient(path, &directory_options(), ambient_authority())?;
validate_directory_handle(&directory, &path.display().to_string())?;