From 4efc02e768ae8d8cbf5eb5f7ec654ed898c38943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 13 Oct 2026 07:18:03 +0500 Subject: [PATCH] fix(paths): recognize long Windows rehearsal paths inside server --- changelog.d/20261013-long-rehearsal-paths.md | 7 ++++ crates/coa-core/src/fsx.rs | 42 ++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 changelog.d/20261013-long-rehearsal-paths.md diff --git a/changelog.d/20261013-long-rehearsal-paths.md b/changelog.d/20261013-long-rehearsal-paths.md new file mode 100644 index 0000000..aaa5593 --- /dev/null +++ b/changelog.d/20261013-long-rehearsal-paths.md @@ -0,0 +1,7 @@ +--- +area: manager +type: fixed +audience: admins +title: Server updates accept long Windows paths inside the private test server +--- +Long SQL file paths no longer fail the folder safety check during update rehearsals. Files outside the server folder remain blocked. diff --git a/crates/coa-core/src/fsx.rs b/crates/coa-core/src/fsx.rs index 47925a1..57fd070 100644 --- a/crates/coa-core/src/fsx.rs +++ b/crates/coa-core/src/fsx.rs @@ -28,6 +28,19 @@ pub fn normalize_lexical(path: &Path) -> PathBuf { fn key(path: &Path) -> Vec { path.components() .map(|c| { + #[cfg(windows)] + if let Component::Prefix(prefix) = c { + use std::path::Prefix; + match prefix.kind() { + Prefix::Disk(drive) | Prefix::VerbatimDisk(drive) => { + return format!("{}:", char::from(drive).to_ascii_lowercase()); + } + Prefix::UNC(server, share) | Prefix::VerbatimUNC(server, share) => { + return format!("\\\\{}\\{}", server.to_string_lossy(), share.to_string_lossy()).to_lowercase(); + } + _ => {} + } + } let c = c.as_os_str().to_string_lossy(); if cfg!(windows) { c.to_lowercase() } else { c.into_owned() } }) @@ -262,6 +275,35 @@ mod tests { } } + #[cfg(windows)] + #[test] + fn containment_matches_verbatim_drive_and_unc_prefixes() { + assert!(starts_with_ci(Path::new(r"\\?\C:\server\Extras\file.sql"), Path::new(r"C:\server"))); + assert!(starts_with_ci(Path::new(r"C:\server\Extras\file.sql"), Path::new(r"\\?\c:\server"))); + assert!(starts_with_ci(Path::new(r"\\?\UNC\host\share\server\file.sql"), Path::new(r"\\host\share\server"))); + assert!(!starts_with_ci(Path::new(r"\\?\D:\server\file.sql"), Path::new(r"C:\server"))); + assert!(!starts_with_ci(Path::new(r"\\?\C:\server-other\file.sql"), Path::new(r"C:\server"))); + assert!(!starts_with_ci(Path::new(r"\\?\UNC\host\other\server\file.sql"), Path::new(r"\\host\share\server"))); + } + + #[cfg(windows)] + #[test] + fn ensure_within_accepts_long_rehearsal_sql_path() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path().join("server"); + fs::create_dir_all(&root).unwrap(); + let mut candidate = root.clone(); + for _ in 0..5 { + candidate.push("long-private-rehearsal-directory-name-20261013"); + } + fs::create_dir_all(&candidate).unwrap(); + candidate.push("2026_06_13_00_ai_playerbot_missing_group_buff_reagent.sql"); + fs::write(&candidate, b"SELECT 1;").unwrap(); + assert!(candidate.as_os_str().len() > 260); + assert!(ensure_within(&root, &candidate).is_ok()); + assert!(ensure_within(&root, &std::fs::canonicalize(candidate).unwrap()).is_ok()); + } + #[test] fn ensure_within_blocks_junction_escape() { if !cfg!(windows) {