1pub(crate) mod env;
4pub use env::is_wsl;
5
6use codex_utils_absolute_path::AbsolutePathBuf;
7use std::collections::HashSet;
8use std::io;
9use std::path::Path;
10use std::path::PathBuf;
11use tempfile::NamedTempFile;
12
13pub fn normalize_for_path_comparison(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
14 let canonical = path.as_ref().canonicalize()?;
15 Ok(normalize_for_wsl(canonical))
16}
17
18pub fn paths_match_after_normalization(left: impl AsRef<Path>, right: impl AsRef<Path>) -> bool {
22 if let (Ok(left), Ok(right)) = (
23 normalize_for_path_comparison(left.as_ref()),
24 normalize_for_path_comparison(right.as_ref()),
25 ) {
26 return left == right;
27 }
28 left.as_ref() == right.as_ref()
29}
30
31pub fn normalize_for_native_workdir(path: impl AsRef<Path>) -> PathBuf {
32 normalize_for_native_workdir_with_flag(path.as_ref().to_path_buf(), cfg!(windows))
33}
34
35pub struct SymlinkWritePaths {
36 pub read_path: Option<PathBuf>,
37 pub write_path: PathBuf,
38}
39
40pub fn resolve_symlink_write_paths(path: &Path) -> io::Result<SymlinkWritePaths> {
47 let root = AbsolutePathBuf::from_absolute_path(path)
48 .map(AbsolutePathBuf::into_path_buf)
49 .unwrap_or_else(|_| path.to_path_buf());
50 let mut current = root.clone();
51 let mut visited = HashSet::new();
52
53 loop {
55 let meta = match std::fs::symlink_metadata(¤t) {
56 Ok(meta) => meta,
57 Err(err) if err.kind() == io::ErrorKind::NotFound => {
58 return Ok(SymlinkWritePaths {
59 read_path: Some(current.clone()),
60 write_path: current,
61 });
62 }
63 Err(_) => {
64 return Ok(SymlinkWritePaths {
65 read_path: None,
66 write_path: root,
67 });
68 }
69 };
70
71 if !meta.file_type().is_symlink() {
72 return Ok(SymlinkWritePaths {
73 read_path: Some(current.clone()),
74 write_path: current,
75 });
76 }
77
78 if !visited.insert(current.clone()) {
80 return Ok(SymlinkWritePaths {
81 read_path: None,
82 write_path: root,
83 });
84 }
85
86 let target = match std::fs::read_link(¤t) {
87 Ok(target) => target,
88 Err(_) => {
89 return Ok(SymlinkWritePaths {
90 read_path: None,
91 write_path: root,
92 });
93 }
94 };
95
96 let next = if target.is_absolute() {
97 AbsolutePathBuf::from_absolute_path(&target)
98 } else if let Some(parent) = current.parent() {
99 Ok(AbsolutePathBuf::resolve_path_against_base(&target, parent))
100 } else {
101 return Ok(SymlinkWritePaths {
102 read_path: None,
103 write_path: root,
104 });
105 };
106
107 let next = match next {
108 Ok(path) => path.into_path_buf(),
109 Err(_) => {
110 return Ok(SymlinkWritePaths {
111 read_path: None,
112 write_path: root,
113 });
114 }
115 };
116
117 current = next;
118 }
119}
120
121pub fn write_atomically(write_path: &Path, contents: &str) -> io::Result<()> {
122 let parent = write_path.parent().ok_or_else(|| {
123 io::Error::new(
124 io::ErrorKind::InvalidInput,
125 format!("path {} has no parent directory", write_path.display()),
126 )
127 })?;
128 std::fs::create_dir_all(parent)?;
129 let tmp = NamedTempFile::new_in(parent)?;
130 std::fs::write(tmp.path(), contents)?;
131 tmp.persist(write_path)?;
132 Ok(())
133}
134
135fn normalize_for_wsl(path: PathBuf) -> PathBuf {
136 normalize_for_wsl_with_flag(path, env::is_wsl())
137}
138
139fn normalize_for_native_workdir_with_flag(path: PathBuf, is_windows: bool) -> PathBuf {
140 if is_windows {
141 dunce::simplified(&path).to_path_buf()
142 } else {
143 path
144 }
145}
146
147fn normalize_for_wsl_with_flag(path: PathBuf, is_wsl: bool) -> PathBuf {
148 if !is_wsl {
149 return path;
150 }
151
152 if !is_wsl_case_insensitive_path(&path) {
153 return path;
154 }
155
156 lower_ascii_path(path)
157}
158
159fn is_wsl_case_insensitive_path(path: &Path) -> bool {
160 #[cfg(target_os = "linux")]
161 {
162 use std::os::unix::ffi::OsStrExt;
163 use std::path::Component;
164
165 let mut components = path.components();
166 let Some(Component::RootDir) = components.next() else {
167 return false;
168 };
169 let Some(Component::Normal(mnt)) = components.next() else {
170 return false;
171 };
172 if !ascii_eq_ignore_case(mnt.as_bytes(), b"mnt") {
173 return false;
174 }
175 let Some(Component::Normal(drive)) = components.next() else {
176 return false;
177 };
178 let drive_bytes = drive.as_bytes();
179 drive_bytes.len() == 1 && drive_bytes[0].is_ascii_alphabetic()
180 }
181 #[cfg(not(target_os = "linux"))]
182 {
183 let _ = path;
184 false
185 }
186}
187
188#[cfg(target_os = "linux")]
189fn ascii_eq_ignore_case(left: &[u8], right: &[u8]) -> bool {
190 left.len() == right.len()
191 && left
192 .iter()
193 .zip(right)
194 .all(|(lhs, rhs)| lhs.to_ascii_lowercase() == *rhs)
195}
196
197#[cfg(target_os = "linux")]
198fn lower_ascii_path(path: PathBuf) -> PathBuf {
199 use std::ffi::OsString;
200 use std::os::unix::ffi::OsStrExt;
201 use std::os::unix::ffi::OsStringExt;
202
203 let bytes = path.as_os_str().as_bytes();
205 let mut lowered = Vec::with_capacity(bytes.len());
206 for byte in bytes {
207 lowered.push(byte.to_ascii_lowercase());
208 }
209 PathBuf::from(OsString::from_vec(lowered))
210}
211
212#[cfg(not(target_os = "linux"))]
213fn lower_ascii_path(path: PathBuf) -> PathBuf {
214 path
215}
216
217#[cfg(test)]
218#[path = "path_utils_tests.rs"]
219mod tests;