From 169183392c7d5e79f17dcdbbc6d0681a297819ee Mon Sep 17 00:00:00 2001 From: Andrei Hasna <28298713+andrei-hasna@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:46:24 +0300 Subject: [PATCH 1/6] feat(state): add managed worktree path key codec --- codex-rs/Cargo.lock | 1 + codex-rs/Cargo.toml | 1 + codex-rs/state/Cargo.toml | 1 + .../state/src/runtime/managed_worktrees.rs | 11 +- .../runtime/managed_worktrees/path_keys.rs | 253 ++++++++++++++++++ 5 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 codex-rs/state/src/runtime/managed_worktrees/path_keys.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 5105a8f32e..a72601893c 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3737,6 +3737,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "unicode-normalization", "uuid", ] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 52b1a3d015..45e129b62e 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -431,6 +431,7 @@ ts-rs = "11" tungstenite = { version = "0.27.0", features = ["deflate", "proxy"] } uds_windows = "1.1.0" unicode-segmentation = "1.12.0" +unicode-normalization = "0.1.25" unicode-width = "0.2" url = "2" urlencoding = "2.1" diff --git a/codex-rs/state/Cargo.toml b/codex-rs/state/Cargo.toml index fa9d5a01f7..3d8b3a7fbd 100644 --- a/codex-rs/state/Cargo.toml +++ b/codex-rs/state/Cargo.toml @@ -22,6 +22,7 @@ strum = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } +unicode-normalization = { workspace = true } uuid = { workspace = true } [dev-dependencies] diff --git a/codex-rs/state/src/runtime/managed_worktrees.rs b/codex-rs/state/src/runtime/managed_worktrees.rs index b0a7b0e72a..a9e36d3f62 100644 --- a/codex-rs/state/src/runtime/managed_worktrees.rs +++ b/codex-rs/state/src/runtime/managed_worktrees.rs @@ -6,6 +6,10 @@ use std::path::Path; use std::path::PathBuf; use uuid::Uuid; +mod path_keys; + +pub(crate) use path_keys::managed_worktree_path_key_from_display; + pub const DEFAULT_MANAGED_WORKTREE_LIST_LIMIT: u32 = 50; pub const MAX_MANAGED_WORKTREE_LIST_LIMIT: u32 = 200; @@ -1520,7 +1524,12 @@ WHERE owner_agent_run_id = ? } pub(crate) fn path_to_db_string(path: &Path) -> String { - path_to_string(&normalize_path_for_db(path)) + let display_path = path_to_string(&normalize_path_for_db(path)); + debug_assert!( + !managed_worktree_path_key_from_display(&display_path).is_empty(), + "managed worktree display paths always have an identity key" + ); + display_path } fn normalize_path_for_db(path: &Path) -> PathBuf { diff --git a/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs new file mode 100644 index 0000000000..33fad0bf81 --- /dev/null +++ b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs @@ -0,0 +1,253 @@ +//! Lexical identity keys for managed-worktree display paths. +//! +//! This module intentionally does not read the filesystem. The managed-worktree +//! store preserves its display path separately; this codec derives a stable key +//! for future identity and collision checks from that display string alone. + +#[cfg(any(target_os = "macos", test))] +use unicode_normalization::UnicodeNormalization; + +/// Derives the platform identity key for an already-persisted display path. +/// +/// This is key-only normalization: it does not alter the persisted display +/// path, resolve symlinks, or canonicalize any existing ancestor. Windows +/// folds case with a locale-independent Unicode upper-then-lower mapping. +/// macOS additionally normalizes to NFD to reflect the default APFS alias +/// behavior. Other Unix platforms preserve case and Unicode spelling. +pub(crate) fn managed_worktree_path_key_from_display(display_path: &str) -> String { + #[cfg(windows)] + { + windows_path_key(display_path) + } + + #[cfg(target_os = "macos")] + { + macos_path_key(display_path) + } + + #[cfg(not(any(windows, target_os = "macos")))] + { + unix_path_key(display_path) + } +} + +#[cfg(any(not(any(windows, target_os = "macos")), test))] +fn unix_path_key(display_path: &str) -> String { + normalize_slash_path(display_path) +} + +#[cfg(any(target_os = "macos", test))] +fn macos_path_key(display_path: &str) -> String { + let decomposed = display_path.nfd().collect::(); + fold_case(normalize_slash_path(&decomposed)).nfd().collect() +} + +#[cfg(any(not(windows), test))] +fn normalize_slash_path(path: &str) -> String { + let rooted = path.starts_with('/'); + let components = normalize_components(path.split('/'), /*root_floor*/ 0, rooted); + format_path(components, "/", rooted, "") +} + +#[cfg(any(windows, test))] +fn windows_path_key(display_path: &str) -> String { + let path = strip_windows_verbatim_prefix(display_path).replace('/', "\\"); + let key = if let Some(path) = path.strip_prefix(r"\\") { + normalize_windows_unc(path) + } else if has_windows_drive_prefix(&path) { + normalize_windows_drive(&path) + } else { + normalize_windows_relative(&path) + }; + fold_case(key) +} + +#[cfg(any(windows, test))] +fn strip_windows_verbatim_prefix(path: &str) -> String { + if path + .get(..8) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case(r"\\?\UNC\")) + { + return format!(r"\\{}", path.get(8..).unwrap_or_default()); + } + if path + .get(..4) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case(r"\\?\")) + { + return path.get(4..).unwrap_or_default().to_string(); + } + path.to_string() +} + +#[cfg(any(windows, test))] +fn has_windows_drive_prefix(path: &str) -> bool { + path.as_bytes() + .get(1) + .is_some_and(|character| *character == b':') + && path.as_bytes().first().is_some_and(u8::is_ascii_alphabetic) +} + +#[cfg(any(windows, test))] +fn normalize_windows_unc(path: &str) -> String { + let components = normalize_components(path.split('\\'), /*root_floor*/ 2, true); + let joined = components.join("\\"); + if joined.is_empty() { + r"\\".to_string() + } else { + format!(r"\\{joined}") + } +} + +#[cfg(any(windows, test))] +fn normalize_windows_drive(path: &str) -> String { + let (prefix, suffix) = path.split_at(2); + let rooted = suffix.starts_with('\\'); + let components = normalize_components(suffix.split('\\'), /*root_floor*/ 0, rooted); + format_path(components, "\\", rooted, prefix) +} + +#[cfg(any(windows, test))] +fn normalize_windows_relative(path: &str) -> String { + let rooted = path.starts_with('\\'); + let components = normalize_components(path.split('\\'), /*root_floor*/ 0, rooted); + format_path(components, "\\", rooted, "") +} + +fn normalize_components<'a>( + components: impl Iterator, + root_floor: usize, + rooted: bool, +) -> Vec<&'a str> { + let mut normalized = Vec::new(); + for component in components { + match component { + "" | "." => {} + ".." if normalized.len() > root_floor => { + normalized.pop(); + } + ".." if !rooted => normalized.push(component), + ".." => {} + _ => normalized.push(component), + } + } + normalized +} + +fn format_path(components: Vec<&str>, separator: &str, rooted: bool, prefix: &str) -> String { + let joined = components.join(separator); + match (prefix, rooted, joined.is_empty()) { + ("", false, true) => ".".to_string(), + ("", false, false) => joined, + ("", true, true) => separator.to_string(), + ("", true, false) => format!("{separator}{joined}"), + (_, false, true) => prefix.to_string(), + (_, false, false) => format!("{prefix}{joined}"), + (_, true, true) => format!("{prefix}{separator}"), + (_, true, false) => format!("{prefix}{separator}{joined}"), + } +} + +#[cfg(any(windows, target_os = "macos", test))] +fn fold_case(path: String) -> String { + path.chars() + .flat_map(char::to_uppercase) + .flat_map(char::to_lowercase) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn unix_keys_normalize_lexically_without_changing_the_display_path() { + let display_path = "./worktrees//run-a/../run-b"; + + assert_eq!("worktrees/run-b", unix_path_key(display_path)); + assert_eq!("./worktrees//run-a/../run-b", display_path); + assert_eq!("/run-b", unix_path_key("/worktrees/../run-b")); + assert_eq!("../run-b", unix_path_key("worktrees/../../run-b")); + assert_eq!(".", unix_path_key("worktrees/..")); + } + + #[test] + fn unix_keys_preserve_case_and_unicode_spelling() { + assert_ne!( + unix_path_key("/worktrees/RunA"), + unix_path_key("/worktrees/runa") + ); + assert_ne!( + unix_path_key("/worktrees/caf\u{00e9}"), + unix_path_key("/worktrees/cafe\u{0301}") + ); + } + + #[test] + fn macos_keys_fold_case_and_canonical_unicode_aliases() { + assert_eq!( + macos_path_key("/worktrees/CAF\u{00c9}/run"), + macos_path_key("/worktrees/cafe\u{0301}/RUN") + ); + } + + #[test] + fn windows_keys_normalize_separators_drive_roots_and_parents() { + assert_eq!( + r"c:\managed-worktrees\run-a", + windows_path_key(r"C:/Managed-Worktrees\\Run-A\\child\\..") + ); + assert_eq!(r"c:\", windows_path_key(r"C:\.")); + assert_eq!(r"c:run-a", windows_path_key(r"C:.\Run-A")); + assert_ne!(windows_path_key(r"C:RunA"), windows_path_key(r"C:\RunA")); + } + + #[test] + fn windows_keys_collapse_unc_and_verbatim_aliases() { + assert_eq!( + windows_path_key(r"\\server\share\run-a"), + windows_path_key(r"\\?\UNC\SERVER\Share\worktrees\..\run-a") + ); + assert_eq!( + windows_path_key(r"C:\worktrees\run-a"), + windows_path_key(r"\\?\c:\WORKTREES\Run-A") + ); + assert_eq!( + r"\\server\share", + windows_path_key(r"\\server\share\worktrees\..") + ); + } + + #[test] + fn windows_case_fold_handles_long_s_without_unicode_normalizing_names() { + assert_eq!( + windows_path_key(r"C:\worktrees\RunS"), + windows_path_key("c:\\worktrees\\run\u{017f}") + ); + assert_ne!( + windows_path_key("C:\\worktrees\\caf\u{00e9}"), + windows_path_key("C:\\worktrees\\cafe\u{0301}") + ); + } + + #[test] + fn current_platform_key_uses_its_documented_policy() { + #[cfg(windows)] + assert_eq!( + windows_path_key(r"C:\worktrees\RunA"), + managed_worktree_path_key_from_display(r"C:\worktrees\RunA") + ); + + #[cfg(target_os = "macos")] + assert_eq!( + macos_path_key("/worktrees/caf\u{00e9}"), + managed_worktree_path_key_from_display("/worktrees/caf\u{00e9}") + ); + + #[cfg(not(any(windows, target_os = "macos")))] + assert_eq!( + unix_path_key("/worktrees/RunA"), + managed_worktree_path_key_from_display("/worktrees/RunA") + ); + } +} From 46be9eeea5aef8ff525f6c59a1645b255f9fc8a4 Mon Sep 17 00:00:00 2001 From: Andrei Hasna <28298713+andrei-hasna@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:50:24 +0300 Subject: [PATCH 2/6] test(state): avoid codespell in path key fixtures --- .../src/runtime/managed_worktrees/path_keys.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs index 33fad0bf81..48597d9c57 100644 --- a/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs +++ b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs @@ -178,16 +178,16 @@ mod tests { unix_path_key("/worktrees/runa") ); assert_ne!( - unix_path_key("/worktrees/caf\u{00e9}"), - unix_path_key("/worktrees/cafe\u{0301}") + unix_path_key("/worktrees/x\u{00e5}"), + unix_path_key("/worktrees/xa\u{030a}") ); } #[test] fn macos_keys_fold_case_and_canonical_unicode_aliases() { assert_eq!( - macos_path_key("/worktrees/CAF\u{00c9}/run"), - macos_path_key("/worktrees/cafe\u{0301}/RUN") + macos_path_key("/worktrees/X\u{00c5}/run"), + macos_path_key("/worktrees/xa\u{030a}/RUN") ); } @@ -225,8 +225,8 @@ mod tests { windows_path_key("c:\\worktrees\\run\u{017f}") ); assert_ne!( - windows_path_key("C:\\worktrees\\caf\u{00e9}"), - windows_path_key("C:\\worktrees\\cafe\u{0301}") + windows_path_key("C:\\worktrees\\x\u{00e5}"), + windows_path_key("C:\\worktrees\\xa\u{030a}") ); } @@ -240,8 +240,8 @@ mod tests { #[cfg(target_os = "macos")] assert_eq!( - macos_path_key("/worktrees/caf\u{00e9}"), - managed_worktree_path_key_from_display("/worktrees/caf\u{00e9}") + macos_path_key("/worktrees/x\u{00e5}"), + managed_worktree_path_key_from_display("/worktrees/x\u{00e5}") ); #[cfg(not(any(windows, target_os = "macos")))] From bbb2dd5fcf1766229f4f2912aae068786e64ce21 Mon Sep 17 00:00:00 2001 From: Andrei Hasna <28298713+andrei-hasna@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:14:09 +0300 Subject: [PATCH 3/6] fix(state): preserve native path key semantics --- codex-rs/Cargo.lock | 1 - codex-rs/Cargo.toml | 1 - codex-rs/state/Cargo.toml | 1 - .../state/src/runtime/managed_worktrees.rs | 10 +- .../runtime/managed_worktrees/path_keys.rs | 483 ++++++++++++------ 5 files changed, 333 insertions(+), 163 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index a72601893c..5105a8f32e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3737,7 +3737,6 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "unicode-normalization", "uuid", ] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 45e129b62e..52b1a3d015 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -431,7 +431,6 @@ ts-rs = "11" tungstenite = { version = "0.27.0", features = ["deflate", "proxy"] } uds_windows = "1.1.0" unicode-segmentation = "1.12.0" -unicode-normalization = "0.1.25" unicode-width = "0.2" url = "2" urlencoding = "2.1" diff --git a/codex-rs/state/Cargo.toml b/codex-rs/state/Cargo.toml index 3d8b3a7fbd..fa9d5a01f7 100644 --- a/codex-rs/state/Cargo.toml +++ b/codex-rs/state/Cargo.toml @@ -22,7 +22,6 @@ strum = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } -unicode-normalization = { workspace = true } uuid = { workspace = true } [dev-dependencies] diff --git a/codex-rs/state/src/runtime/managed_worktrees.rs b/codex-rs/state/src/runtime/managed_worktrees.rs index a9e36d3f62..c88bb2d66d 100644 --- a/codex-rs/state/src/runtime/managed_worktrees.rs +++ b/codex-rs/state/src/runtime/managed_worktrees.rs @@ -8,7 +8,7 @@ use uuid::Uuid; mod path_keys; -pub(crate) use path_keys::managed_worktree_path_key_from_display; +pub(crate) use path_keys::managed_worktree_path_key; pub const DEFAULT_MANAGED_WORKTREE_LIST_LIMIT: u32 = 50; pub const MAX_MANAGED_WORKTREE_LIST_LIMIT: u32 = 200; @@ -1524,12 +1524,12 @@ WHERE owner_agent_run_id = ? } pub(crate) fn path_to_db_string(path: &Path) -> String { - let display_path = path_to_string(&normalize_path_for_db(path)); debug_assert!( - !managed_worktree_path_key_from_display(&display_path).is_empty(), - "managed worktree display paths always have an identity key" + !managed_worktree_path_key(path).is_empty(), + "managed worktree paths always have an identity key" ); - display_path + + path_to_string(&normalize_path_for_db(path)) } fn normalize_path_for_db(path: &Path) -> PathBuf { diff --git a/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs index 48597d9c57..aca247fcbd 100644 --- a/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs +++ b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs @@ -1,158 +1,279 @@ -//! Lexical identity keys for managed-worktree display paths. +//! Conservative lexical identity keys for managed-worktree paths. //! -//! This module intentionally does not read the filesystem. The managed-worktree -//! store preserves its display path separately; this codec derives a stable key -//! for future identity and collision checks from that display string alone. +//! This module intentionally does not read the filesystem. Keys are opaque +//! byte sequences derived from native path data, so they never rely on lossy +//! display text. They normalize only lexical syntax that is unambiguous across +//! filesystem policies. Case and Unicode alias checks require verified +//! filesystem policy and belong to later admission code. -#[cfg(any(target_os = "macos", test))] -use unicode_normalization::UnicodeNormalization; +use std::path::Path; -/// Derives the platform identity key for an already-persisted display path. +#[cfg(unix)] +use std::ffi::OsStr; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(windows)] +use std::os::windows::ffi::OsStrExt; + +const KEY_VERSION: &[u8] = b"managed-worktree-path-key-v1"; + +/// Derives an opaque, byte-safe identity key from a native path. /// -/// This is key-only normalization: it does not alter the persisted display -/// path, resolve symlinks, or canonicalize any existing ancestor. Windows -/// folds case with a locale-independent Unicode upper-then-lower mapping. -/// macOS additionally normalizes to NFD to reflect the default APFS alias -/// behavior. Other Unix platforms preserve case and Unicode spelling. -pub(crate) fn managed_worktree_path_key_from_display(display_path: &str) -> String { - #[cfg(windows)] - { - windows_path_key(display_path) - } +/// The key does not alter the display path, resolve symlinks, or infer a +/// filesystem's case or Unicode policy. Unix keys preserve `OsStr` bytes; +/// Windows keys preserve UTF-16 units. In particular, verbatim and device +/// Windows namespaces are encoded without Win32 normalization. +#[cfg(unix)] +pub(crate) fn managed_worktree_path_key(path: &Path) -> Vec { + unix_path_key(path.as_os_str()) +} - #[cfg(target_os = "macos")] - { - macos_path_key(display_path) - } +/// Derives an opaque, byte-safe identity key from a native Windows path. +#[cfg(windows)] +pub(crate) fn managed_worktree_path_key(path: &Path) -> Vec { + let units = path.as_os_str().encode_wide().collect::>(); + windows_path_key(&units) +} - #[cfg(not(any(windows, target_os = "macos")))] - { - unix_path_key(display_path) +#[cfg(unix)] +fn unix_path_key(path: &OsStr) -> Vec { + let path = path.as_bytes(); + let rooted = path.starts_with(b"/"); + let components = normalize_byte_components(path.split(|byte| *byte == b'/'), rooted); + encode_byte_key( + if rooted { + b"unix-absolute" + } else { + b"unix-relative" + }, + components, + ) +} + +#[cfg(any(windows, test))] +fn windows_path_key(path: &[u16]) -> Vec { + if has_windows_verbatim_prefix(path) { + return encode_wide_key(verbatim_kind(path), path, Vec::new()); } + if has_windows_device_prefix(path) { + return encode_wide_key(b"windows-device", path, Vec::new()); + } + if has_windows_unc_prefix(path) { + let components = normalize_wide_components( + split_windows_components(&path[2..]), + /*rooted*/ true, + /*root_floor*/ 2, + ); + return encode_wide_key(b"windows-unc", &[], components); + } + if has_windows_drive_prefix(path) { + let rooted = path.get(2).is_some_and(|unit| is_windows_separator(*unit)); + let components = normalize_wide_components( + split_windows_components(&path[2..]), + rooted, + /*root_floor*/ 0, + ); + return encode_wide_key( + if rooted { + b"windows-drive-absolute" + } else { + b"windows-drive-relative" + }, + &path[..2], + components, + ); + } + + let rooted = path.first().is_some_and(|unit| is_windows_separator(*unit)); + let components = normalize_wide_components( + split_windows_components(path), + rooted, + /*root_floor*/ 0, + ); + encode_wide_key( + if rooted { + b"windows-rooted" + } else { + b"windows-relative" + }, + &[], + components, + ) } -#[cfg(any(not(any(windows, target_os = "macos")), test))] -fn unix_path_key(display_path: &str) -> String { - normalize_slash_path(display_path) +#[cfg(any(windows, test))] +fn has_windows_verbatim_prefix(path: &[u16]) -> bool { + path.starts_with(&[ + u16::from(b'\\'), + u16::from(b'\\'), + u16::from(b'?'), + u16::from(b'\\'), + ]) } -#[cfg(any(target_os = "macos", test))] -fn macos_path_key(display_path: &str) -> String { - let decomposed = display_path.nfd().collect::(); - fold_case(normalize_slash_path(&decomposed)).nfd().collect() +#[cfg(any(windows, test))] +fn has_windows_device_prefix(path: &[u16]) -> bool { + path.starts_with(&[ + u16::from(b'\\'), + u16::from(b'\\'), + u16::from(b'.'), + u16::from(b'\\'), + ]) } -#[cfg(any(not(windows), test))] -fn normalize_slash_path(path: &str) -> String { - let rooted = path.starts_with('/'); - let components = normalize_components(path.split('/'), /*root_floor*/ 0, rooted); - format_path(components, "/", rooted, "") +#[cfg(any(windows, test))] +fn has_windows_unc_prefix(path: &[u16]) -> bool { + matches!(path, [first, second, ..] if is_windows_separator(*first) && is_windows_separator(*second)) } #[cfg(any(windows, test))] -fn windows_path_key(display_path: &str) -> String { - let path = strip_windows_verbatim_prefix(display_path).replace('/', "\\"); - let key = if let Some(path) = path.strip_prefix(r"\\") { - normalize_windows_unc(path) - } else if has_windows_drive_prefix(&path) { - normalize_windows_drive(&path) - } else { - normalize_windows_relative(&path) - }; - fold_case(key) +fn has_windows_drive_prefix(path: &[u16]) -> bool { + path.first().is_some_and(|drive| is_ascii_alpha(*drive)) + && path.get(1).is_some_and(|unit| *unit == u16::from(b':')) } #[cfg(any(windows, test))] -fn strip_windows_verbatim_prefix(path: &str) -> String { - if path - .get(..8) - .is_some_and(|prefix| prefix.eq_ignore_ascii_case(r"\\?\UNC\")) +fn verbatim_kind(path: &[u16]) -> &'static [u8] { + let namespace = &path[4..]; + if has_ascii_prefix_ignore_case(namespace, b"UNC") + && namespace + .get(3) + .is_some_and(|unit| is_windows_separator(*unit)) { - return format!(r"\\{}", path.get(8..).unwrap_or_default()); - } - if path - .get(..4) - .is_some_and(|prefix| prefix.eq_ignore_ascii_case(r"\\?\")) - { - return path.get(4..).unwrap_or_default().to_string(); + b"windows-verbatim-unc" + } else if has_windows_drive_prefix(namespace) { + b"windows-verbatim-drive" + } else if has_ascii_prefix_ignore_case(namespace, b"Volume{") { + b"windows-verbatim-volume" + } else { + b"windows-verbatim" } - path.to_string() } #[cfg(any(windows, test))] -fn has_windows_drive_prefix(path: &str) -> bool { - path.as_bytes() - .get(1) - .is_some_and(|character| *character == b':') - && path.as_bytes().first().is_some_and(u8::is_ascii_alphabetic) +fn has_ascii_prefix_ignore_case(path: &[u16], prefix: &[u8]) -> bool { + path.get(..prefix.len()).is_some_and(|units| { + units.iter().zip(prefix).all(|(unit, expected)| { + u8::try_from(*unit).is_ok_and(|unit| unit.eq_ignore_ascii_case(expected)) + }) + }) } #[cfg(any(windows, test))] -fn normalize_windows_unc(path: &str) -> String { - let components = normalize_components(path.split('\\'), /*root_floor*/ 2, true); - let joined = components.join("\\"); - if joined.is_empty() { - r"\\".to_string() - } else { - format!(r"\\{joined}") - } +fn is_ascii_alpha(unit: u16) -> bool { + u8::try_from(unit).is_ok_and(|unit| unit.is_ascii_alphabetic()) } #[cfg(any(windows, test))] -fn normalize_windows_drive(path: &str) -> String { - let (prefix, suffix) = path.split_at(2); - let rooted = suffix.starts_with('\\'); - let components = normalize_components(suffix.split('\\'), /*root_floor*/ 0, rooted); - format_path(components, "\\", rooted, prefix) +fn is_windows_separator(unit: u16) -> bool { + unit == u16::from(b'\\') || unit == u16::from(b'/') } #[cfg(any(windows, test))] -fn normalize_windows_relative(path: &str) -> String { - let rooted = path.starts_with('\\'); - let components = normalize_components(path.split('\\'), /*root_floor*/ 0, rooted); - format_path(components, "\\", rooted, "") +fn split_windows_components(path: &[u16]) -> impl Iterator { + path.split(|unit| is_windows_separator(*unit)) } -fn normalize_components<'a>( - components: impl Iterator, +#[cfg(any(windows, test))] +fn normalize_wide_components<'a>( + components: impl Iterator, + rooted: bool, root_floor: usize, +) -> Vec<&'a [u16]> { + normalize_components( + components, + rooted, + root_floor, + |component| component.is_empty() || component == [b'.' as u16], + |component| component == [b'.' as u16, b'.' as u16], + ) +} + +#[cfg(unix)] +fn normalize_byte_components<'a>( + components: impl Iterator, + rooted: bool, +) -> Vec<&'a [u8]> { + normalize_components( + components, + rooted, + /*root_floor*/ 0, + |component| component.is_empty() || component == b".", + |component| component == b"..", + ) +} + +fn normalize_components<'a, T>( + components: impl Iterator, rooted: bool, -) -> Vec<&'a str> { + root_floor: usize, + is_ignored: impl Fn(&[T]) -> bool, + is_parent: impl Fn(&[T]) -> bool, +) -> Vec<&'a [T]> { let mut normalized = Vec::new(); for component in components { - match component { - "" | "." => {} - ".." if normalized.len() > root_floor => { + if is_ignored(component) { + continue; + } + if is_parent(component) { + if normalized.len() > root_floor { normalized.pop(); + } else if !rooted { + normalized.push(component); } - ".." if !rooted => normalized.push(component), - ".." => {} - _ => normalized.push(component), + } else { + normalized.push(component); } } normalized } -fn format_path(components: Vec<&str>, separator: &str, rooted: bool, prefix: &str) -> String { - let joined = components.join(separator); - match (prefix, rooted, joined.is_empty()) { - ("", false, true) => ".".to_string(), - ("", false, false) => joined, - ("", true, true) => separator.to_string(), - ("", true, false) => format!("{separator}{joined}"), - (_, false, true) => prefix.to_string(), - (_, false, false) => format!("{prefix}{joined}"), - (_, true, true) => format!("{prefix}{separator}"), - (_, true, false) => format!("{prefix}{separator}{joined}"), +fn encode_byte_key(kind: &[u8], components: Vec<&[u8]>) -> Vec { + let mut key = key_prefix(kind); + push_length(&mut key, components.len()); + for component in components { + push_bytes(&mut key, component); } + key } -#[cfg(any(windows, target_os = "macos", test))] -fn fold_case(path: String) -> String { - path.chars() - .flat_map(char::to_uppercase) - .flat_map(char::to_lowercase) - .collect() +#[cfg(any(windows, test))] +fn encode_wide_key(kind: &[u8], prefix: &[u16], components: Vec<&[u16]>) -> Vec { + let mut key = key_prefix(kind); + push_wide_units(&mut key, prefix); + push_length(&mut key, components.len()); + for component in components { + push_wide_units(&mut key, component); + } + key +} + +fn key_prefix(kind: &[u8]) -> Vec { + let mut key = Vec::with_capacity(KEY_VERSION.len() + kind.len() + 16); + push_bytes(&mut key, KEY_VERSION); + push_bytes(&mut key, kind); + key +} + +fn push_bytes(key: &mut Vec, value: &[u8]) { + push_length(key, value.len()); + key.extend_from_slice(value); +} + +#[cfg(any(windows, test))] +fn push_wide_units(key: &mut Vec, value: &[u16]) { + push_length(key, value.len()); + for unit in value { + key.extend_from_slice(&unit.to_be_bytes()); + } +} + +fn push_length(key: &mut Vec, length: usize) { + let length = match u32::try_from(length) { + Ok(length) => length, + Err(_) => panic!("managed worktree path key fields must fit in u32"), + }; + key.extend_from_slice(&length.to_be_bytes()); } #[cfg(test)] @@ -160,94 +281,146 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - #[test] - fn unix_keys_normalize_lexically_without_changing_the_display_path() { - let display_path = "./worktrees//run-a/../run-b"; + #[cfg(unix)] + use std::os::unix::ffi::OsStrExt; - assert_eq!("worktrees/run-b", unix_path_key(display_path)); - assert_eq!("./worktrees//run-a/../run-b", display_path); - assert_eq!("/run-b", unix_path_key("/worktrees/../run-b")); - assert_eq!("../run-b", unix_path_key("worktrees/../../run-b")); - assert_eq!(".", unix_path_key("worktrees/..")); + #[cfg(any(windows, test))] + fn wide(path: &str) -> Vec { + path.encode_utf16().collect() } + #[cfg(unix)] #[test] - fn unix_keys_preserve_case_and_unicode_spelling() { - assert_ne!( - unix_path_key("/worktrees/RunA"), - unix_path_key("/worktrees/runa") + fn unix_keys_normalize_only_safe_lexical_syntax() { + assert_eq!( + unix_path_key(OsStr::new("worktrees/run-b")), + unix_path_key(OsStr::new("./worktrees//run-a/../run-b")) + ); + assert_eq!( + unix_path_key(OsStr::new("/run-b")), + unix_path_key(OsStr::new("/worktrees/../run-b")) + ); + assert_eq!( + unix_path_key(OsStr::new("../run-b")), + unix_path_key(OsStr::new("worktrees/../../run-b")) ); assert_ne!( - unix_path_key("/worktrees/x\u{00e5}"), - unix_path_key("/worktrees/xa\u{030a}") + unix_path_key(OsStr::new("run-b")), + unix_path_key(OsStr::new("/run-b")) ); } + #[cfg(unix)] #[test] - fn macos_keys_fold_case_and_canonical_unicode_aliases() { - assert_eq!( - macos_path_key("/worktrees/X\u{00c5}/run"), - macos_path_key("/worktrees/xa\u{030a}/RUN") + fn unix_keys_preserve_case_and_unicode_spelling() { + assert_ne!( + unix_path_key(OsStr::new("/worktrees/RunA")), + unix_path_key(OsStr::new("/worktrees/runa")) + ); + assert_ne!( + unix_path_key(OsStr::new("/worktrees/x\u{00e5}")), + unix_path_key(OsStr::new("/worktrees/xa\u{030a}")) ); } + #[cfg(unix)] #[test] - fn windows_keys_normalize_separators_drive_roots_and_parents() { + fn unix_keys_preserve_non_utf8_os_string_bytes() { + let invalid = OsStr::from_bytes(b"/worktrees/\xff"); + let replacement = OsStr::new("/worktrees/\u{fffd}"); + + assert_ne!(unix_path_key(invalid), unix_path_key(replacement)); assert_eq!( - r"c:\managed-worktrees\run-a", - windows_path_key(r"C:/Managed-Worktrees\\Run-A\\child\\..") + unix_path_key(invalid), + managed_worktree_path_key(Path::new(invalid)) ); - assert_eq!(r"c:\", windows_path_key(r"C:\.")); - assert_eq!(r"c:run-a", windows_path_key(r"C:.\Run-A")); - assert_ne!(windows_path_key(r"C:RunA"), windows_path_key(r"C:\RunA")); } #[test] - fn windows_keys_collapse_unc_and_verbatim_aliases() { + fn ordinary_windows_keys_normalize_separators_and_dot_segments_only() { assert_eq!( - windows_path_key(r"\\server\share\run-a"), - windows_path_key(r"\\?\UNC\SERVER\Share\worktrees\..\run-a") + windows_path_key(&wide(r"C:\managed-worktrees\run-a")), + windows_path_key(&wide(r"C:/managed-worktrees\\run-a\\child\\..")) ); assert_eq!( - windows_path_key(r"C:\worktrees\run-a"), - windows_path_key(r"\\?\c:\WORKTREES\Run-A") + windows_path_key(&wide(r"\\server\share")), + windows_path_key(&wide(r"\\server\share\worktrees\..")) ); - assert_eq!( - r"\\server\share", - windows_path_key(r"\\server\share\worktrees\..") + assert_ne!( + windows_path_key(&wide(r"C:run-a")), + windows_path_key(&wide(r"C:\run-a")) + ); + assert_ne!( + windows_path_key(&wide(r"C:\RunA")), + windows_path_key(&wide(r"C:\runa")) ); } #[test] - fn windows_case_fold_handles_long_s_without_unicode_normalizing_names() { - assert_eq!( - windows_path_key(r"C:\worktrees\RunS"), - windows_path_key("c:\\worktrees\\run\u{017f}") + fn windows_verbatim_namespaces_preserve_raw_drive_unc_and_volume_paths() { + assert_ne!( + windows_path_key(&wide(r"\\?\C:\run. ")), + windows_path_key(&wide(r"C:\run. ")) + ); + assert_ne!( + windows_path_key(&wide(r"\\?\C:\run. ")), + windows_path_key(&wide(r"\\?\C:\run")) + ); + assert_ne!( + windows_path_key(&wide(r"\\?\C:\dir.\..\run. ")), + windows_path_key(&wide(r"\\?\C:\run. ")) + ); + assert_ne!( + windows_path_key(&wide(r"\\?\UNC\server\share\run. ")), + windows_path_key(&wide(r"\\server\share\run. ")) ); assert_ne!( - windows_path_key("C:\\worktrees\\x\u{00e5}"), - windows_path_key("C:\\worktrees\\xa\u{030a}") + windows_path_key(&wide( + r"\\?\Volume{01234567-89ab-cdef-0123-456789abcdef}\run. " + )), + windows_path_key(&wide(r"\\?\C:\run. ")) ); } #[test] - fn current_platform_key_uses_its_documented_policy() { - #[cfg(windows)] - assert_eq!( - windows_path_key(r"C:\worktrees\RunA"), - managed_worktree_path_key_from_display(r"C:\worktrees\RunA") + fn windows_keys_do_not_assume_case_or_expanding_unicode_aliases() { + assert_ne!( + windows_path_key(&wide(r"C:\worktrees\RunA")), + windows_path_key(&wide(r"C:\worktrees\runa")) ); + assert_ne!( + windows_path_key(&wide(r"C:\worktrees\Straße")), + windows_path_key(&wide(r"C:\worktrees\Strasse")) + ); + assert_ne!( + windows_path_key(&wide("C:\\worktrees\\x\u{00e5}")), + windows_path_key(&wide("C:\\worktrees\\xa\u{030a}")) + ); + } + + #[test] + fn windows_keys_preserve_non_unicode_native_units() { + let lone_surrogate = vec![b'C' as u16, b':' as u16, b'\\' as u16, 0xd800]; + let other_surrogate = vec![b'C' as u16, b':' as u16, b'\\' as u16, 0xd801]; - #[cfg(target_os = "macos")] assert_eq!( - macos_path_key("/worktrees/x\u{00e5}"), - managed_worktree_path_key_from_display("/worktrees/x\u{00e5}") + windows_path_key(&lone_surrogate), + windows_path_key(&lone_surrogate) ); + assert_ne!( + windows_path_key(&lone_surrogate), + windows_path_key(&other_surrogate) + ); + } + + #[cfg(unix)] + #[test] + fn current_platform_key_uses_native_path_bytes() { + let path = Path::new("/worktrees/RunA"); - #[cfg(not(any(windows, target_os = "macos")))] assert_eq!( - unix_path_key("/worktrees/RunA"), - managed_worktree_path_key_from_display("/worktrees/RunA") + unix_path_key(path.as_os_str()), + managed_worktree_path_key(path) ); } } From dec364e4e4992131311364469eb82a90761d4a2e Mon Sep 17 00:00:00 2001 From: Andrei Hasna <28298713+andrei-hasna@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:33:06 +0300 Subject: [PATCH 4/6] Fix managed worktree path key framing --- .../runtime/managed_worktrees/path_keys.rs | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs index aca247fcbd..531582c814 100644 --- a/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs +++ b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs @@ -41,7 +41,9 @@ fn unix_path_key(path: &OsStr) -> Vec { let rooted = path.starts_with(b"/"); let components = normalize_byte_components(path.split(|byte| *byte == b'/'), rooted); encode_byte_key( - if rooted { + if path.starts_with(b"//") && !path.starts_with(b"///") { + b"unix-double-slash-absolute" + } else if rooted { b"unix-absolute" } else { b"unix-relative" @@ -228,9 +230,10 @@ fn normalize_components<'a, T>( normalized } +#[cfg(unix)] fn encode_byte_key(kind: &[u8], components: Vec<&[u8]>) -> Vec { let mut key = key_prefix(kind); - push_length(&mut key, components.len()); + push_length(&mut key, components.len() as u64); for component in components { push_bytes(&mut key, component); } @@ -241,7 +244,7 @@ fn encode_byte_key(kind: &[u8], components: Vec<&[u8]>) -> Vec { fn encode_wide_key(kind: &[u8], prefix: &[u16], components: Vec<&[u16]>) -> Vec { let mut key = key_prefix(kind); push_wide_units(&mut key, prefix); - push_length(&mut key, components.len()); + push_length(&mut key, components.len() as u64); for component in components { push_wide_units(&mut key, component); } @@ -256,23 +259,19 @@ fn key_prefix(kind: &[u8]) -> Vec { } fn push_bytes(key: &mut Vec, value: &[u8]) { - push_length(key, value.len()); + push_length(key, value.len() as u64); key.extend_from_slice(value); } #[cfg(any(windows, test))] fn push_wide_units(key: &mut Vec, value: &[u16]) { - push_length(key, value.len()); + push_length(key, value.len() as u64); for unit in value { key.extend_from_slice(&unit.to_be_bytes()); } } -fn push_length(key: &mut Vec, length: usize) { - let length = match u32::try_from(length) { - Ok(length) => length, - Err(_) => panic!("managed worktree path key fields must fit in u32"), - }; +fn push_length(key: &mut Vec, length: u64) { key.extend_from_slice(&length.to_be_bytes()); } @@ -310,6 +309,19 @@ mod tests { ); } + #[cfg(unix)] + #[test] + fn unix_keys_preserve_exactly_two_leading_slashes() { + let one_slash = unix_path_key(OsStr::new("/host/share")); + let exactly_two_slashes = unix_path_key(OsStr::new("//host/share")); + let three_slashes = unix_path_key(OsStr::new("///host/share")); + let four_slashes = unix_path_key(OsStr::new("////host/share")); + + assert_ne!(one_slash, exactly_two_slashes); + assert_eq!(one_slash, three_slashes); + assert_eq!(three_slashes, four_slashes); + } + #[cfg(unix)] #[test] fn unix_keys_preserve_case_and_unicode_spelling() { @@ -423,4 +435,17 @@ mod tests { managed_worktree_path_key(path) ); } + + #[test] + fn length_framing_preserves_values_beyond_u32() { + let mut u32_max = Vec::new(); + let mut next_value = Vec::new(); + + push_length(&mut u32_max, u64::from(u32::MAX)); + push_length(&mut next_value, u64::from(u32::MAX) + 1); + + assert_eq!(u32_max, u64::from(u32::MAX).to_be_bytes()); + assert_eq!(next_value, (u64::from(u32::MAX) + 1).to_be_bytes()); + assert_ne!(u32_max, next_value); + } } From 941934a6c008d990890c83fef366b00d4e68564a Mon Sep 17 00:00:00 2001 From: Andrei Hasna <28298713+andrei-hasna@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:45:22 +0300 Subject: [PATCH 5/6] fix(state): keep path codec standalone --- codex-rs/state/src/runtime/managed_worktrees.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/codex-rs/state/src/runtime/managed_worktrees.rs b/codex-rs/state/src/runtime/managed_worktrees.rs index c88bb2d66d..b497878f11 100644 --- a/codex-rs/state/src/runtime/managed_worktrees.rs +++ b/codex-rs/state/src/runtime/managed_worktrees.rs @@ -6,10 +6,9 @@ use std::path::Path; use std::path::PathBuf; use uuid::Uuid; +#[allow(dead_code)] mod path_keys; -pub(crate) use path_keys::managed_worktree_path_key; - pub const DEFAULT_MANAGED_WORKTREE_LIST_LIMIT: u32 = 50; pub const MAX_MANAGED_WORKTREE_LIST_LIMIT: u32 = 200; @@ -1524,11 +1523,6 @@ WHERE owner_agent_run_id = ? } pub(crate) fn path_to_db_string(path: &Path) -> String { - debug_assert!( - !managed_worktree_path_key(path).is_empty(), - "managed worktree paths always have an identity key" - ); - path_to_string(&normalize_path_for_db(path)) } From 62f8ef9b9e7e8e88690413d810bd43e97cc3a56e Mon Sep 17 00:00:00 2001 From: Andrei Hasna <28298713+andrei-hasna@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:57:17 +0300 Subject: [PATCH 6/6] fix(state): preserve symlink-sensitive path parents --- .../state/src/runtime/managed_worktrees.rs | 4 +- .../runtime/managed_worktrees/path_keys.rs | 94 +++++++++---------- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/codex-rs/state/src/runtime/managed_worktrees.rs b/codex-rs/state/src/runtime/managed_worktrees.rs index b497878f11..421a0d5123 100644 --- a/codex-rs/state/src/runtime/managed_worktrees.rs +++ b/codex-rs/state/src/runtime/managed_worktrees.rs @@ -6,7 +6,9 @@ use std::path::Path; use std::path::PathBuf; use uuid::Uuid; -#[allow(dead_code)] +// The codec is exercised as a test-only staging specification until a later +// persistence and admission stage owns production identity keys. +#[cfg(test)] mod path_keys; pub const DEFAULT_MANAGED_WORKTREE_LIST_LIMIT: u32 = 50; diff --git a/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs index 531582c814..b9bedf41f9 100644 --- a/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs +++ b/codex-rs/state/src/runtime/managed_worktrees/path_keys.rs @@ -1,40 +1,20 @@ -//! Conservative lexical identity keys for managed-worktree paths. +//! Conservative lexical identity key staging specification. //! -//! This module intentionally does not read the filesystem. Keys are opaque -//! byte sequences derived from native path data, so they never rely on lossy -//! display text. They normalize only lexical syntax that is unambiguous across +//! This test-only module intentionally does not read the filesystem. It proves +//! the byte-safe key format for a later persistence and admission stage without +//! pretending that the codec has production callers today. Keys are opaque byte +//! sequences derived from native path data, so they never rely on lossy display +//! text. They normalize only lexical syntax that is unambiguous across //! filesystem policies. Case and Unicode alias checks require verified //! filesystem policy and belong to later admission code. -use std::path::Path; - #[cfg(unix)] use std::ffi::OsStr; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; -#[cfg(windows)] -use std::os::windows::ffi::OsStrExt; const KEY_VERSION: &[u8] = b"managed-worktree-path-key-v1"; -/// Derives an opaque, byte-safe identity key from a native path. -/// -/// The key does not alter the display path, resolve symlinks, or infer a -/// filesystem's case or Unicode policy. Unix keys preserve `OsStr` bytes; -/// Windows keys preserve UTF-16 units. In particular, verbatim and device -/// Windows namespaces are encoded without Win32 normalization. -#[cfg(unix)] -pub(crate) fn managed_worktree_path_key(path: &Path) -> Vec { - unix_path_key(path.as_os_str()) -} - -/// Derives an opaque, byte-safe identity key from a native Windows path. -#[cfg(windows)] -pub(crate) fn managed_worktree_path_key(path: &Path) -> Vec { - let units = path.as_os_str().encode_wide().collect::>(); - windows_path_key(&units) -} - #[cfg(unix)] fn unix_path_key(path: &OsStr) -> Vec { let path = path.as_bytes(); @@ -218,9 +198,10 @@ fn normalize_components<'a, T>( continue; } if is_parent(component) { - if normalized.len() > root_floor { - normalized.pop(); - } else if !rooted { + // `component/..` may traverse through a symlink, so it cannot be + // erased without consulting the filesystem. Only a parent already + // at a syntactic root is safe to discard. + if !rooted || normalized.len() != root_floor { normalized.push(component); } } else { @@ -293,13 +274,21 @@ mod tests { fn unix_keys_normalize_only_safe_lexical_syntax() { assert_eq!( unix_path_key(OsStr::new("worktrees/run-b")), - unix_path_key(OsStr::new("./worktrees//run-a/../run-b")) + unix_path_key(OsStr::new("./worktrees//run-b")) ); assert_eq!( unix_path_key(OsStr::new("/run-b")), - unix_path_key(OsStr::new("/worktrees/../run-b")) + unix_path_key(OsStr::new("/../run-b")) ); - assert_eq!( + assert_ne!( + unix_path_key(OsStr::new("run-b")), + unix_path_key(OsStr::new("link/../run-b")) + ); + assert_ne!( + unix_path_key(OsStr::new("/run-b")), + unix_path_key(OsStr::new("/link/../run-b")) + ); + assert_ne!( unix_path_key(OsStr::new("../run-b")), unix_path_key(OsStr::new("worktrees/../../run-b")) ); @@ -342,21 +331,37 @@ mod tests { let replacement = OsStr::new("/worktrees/\u{fffd}"); assert_ne!(unix_path_key(invalid), unix_path_key(replacement)); - assert_eq!( - unix_path_key(invalid), - managed_worktree_path_key(Path::new(invalid)) - ); } #[test] - fn ordinary_windows_keys_normalize_separators_and_dot_segments_only() { + fn ordinary_windows_keys_normalize_separators_and_safe_root_parents_only() { assert_eq!( windows_path_key(&wide(r"C:\managed-worktrees\run-a")), - windows_path_key(&wide(r"C:/managed-worktrees\\run-a\\child\\..")) + windows_path_key(&wide(r"C:/managed-worktrees\\.\\run-a")) ); assert_eq!( windows_path_key(&wide(r"\\server\share")), - windows_path_key(&wide(r"\\server\share\worktrees\..")) + windows_path_key(&wide(r"\\server\share\..")) + ); + assert_eq!( + windows_path_key(&wide(r"C:\run-b")), + windows_path_key(&wide(r"C:\..\run-b")) + ); + assert_eq!( + windows_path_key(&wide(r"\run-b")), + windows_path_key(&wide(r"\..\run-b")) + ); + assert_ne!( + windows_path_key(&wide(r"C:\run-b")), + windows_path_key(&wide(r"C:\link\..\run-b")) + ); + assert_ne!( + windows_path_key(&wide(r"C:run-b")), + windows_path_key(&wide(r"C:link\..\run-b")) + ); + assert_ne!( + windows_path_key(&wide(r"\\server\share\run-b")), + windows_path_key(&wide(r"\\server\share\link\..\run-b")) ); assert_ne!( windows_path_key(&wide(r"C:run-a")), @@ -425,17 +430,6 @@ mod tests { ); } - #[cfg(unix)] - #[test] - fn current_platform_key_uses_native_path_bytes() { - let path = Path::new("/worktrees/RunA"); - - assert_eq!( - unix_path_key(path.as_os_str()), - managed_worktree_path_key(path) - ); - } - #[test] fn length_framing_preserves_values_beyond_u32() { let mut u32_max = Vec::new();