Skip to content

Commit 9f4c3a1

Browse files
authored
Fix path relative-to for case-insensitive filesystems (#16310)
fixes #16205 # Description 1. **Adds fallback**: On case-insensitive filesystems (Windows, macOS), falls back to case-insensitive comparison when the standard comparison fails 2. **Maintains filesystem semantics**: Only uses case-insensitive comparison on platforms where it's appropriate ## Before: ```console $> "/etc" | path relative-to "/Etc" Error: nu::shell::cant_convert × Can't convert to prefix not found. ╭─[entry #33:1:1] 1 │ "/etc" | path relative-to "/Etc" · ───┬── · ╰── can't convert string to prefix not found ╰──── ``` ## After: For Windows and macOS: ```console $> "/etc" | path relative-to "/Etc" | debug -v "" ```
1 parent 4f9c077 commit 9f4c3a1

File tree

1 file changed

+160
-8
lines changed

1 file changed

+160
-8
lines changed

crates/nu-command/src/path/relative_to.rs

Lines changed: 160 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,17 +144,84 @@ path."#
144144
fn relative_to(path: &Path, span: Span, args: &Arguments) -> Value {
145145
let lhs = expand_to_real_path(path);
146146
let rhs = expand_to_real_path(&args.path.item);
147+
147148
match lhs.strip_prefix(&rhs) {
148149
Ok(p) => Value::string(p.to_string_lossy(), span),
149-
Err(e) => Value::error(
150-
ShellError::CantConvert {
151-
to_type: e.to_string(),
152-
from_type: "string".into(),
150+
Err(e) => {
151+
// On case-insensitive filesystems, try case-insensitive comparison
152+
if is_case_insensitive_filesystem() {
153+
if let Some(relative_path) = try_case_insensitive_strip_prefix(&lhs, &rhs) {
154+
return Value::string(relative_path.to_string_lossy(), span);
155+
}
156+
}
157+
158+
Value::error(
159+
ShellError::CantConvert {
160+
to_type: e.to_string(),
161+
from_type: "string".into(),
162+
span,
163+
help: None,
164+
},
153165
span,
154-
help: None,
155-
},
156-
span,
157-
),
166+
)
167+
}
168+
}
169+
}
170+
171+
/// Check if the current filesystem is typically case-insensitive
172+
fn is_case_insensitive_filesystem() -> bool {
173+
// Windows and macOS typically have case-insensitive filesystems
174+
cfg!(any(target_os = "windows", target_os = "macos"))
175+
}
176+
177+
/// Try to strip prefix in a case-insensitive manner
178+
fn try_case_insensitive_strip_prefix(lhs: &Path, rhs: &Path) -> Option<std::path::PathBuf> {
179+
let mut lhs_components = lhs.components();
180+
let mut rhs_components = rhs.components();
181+
182+
// Compare components case-insensitively
183+
loop {
184+
match (lhs_components.next(), rhs_components.next()) {
185+
(Some(lhs_comp), Some(rhs_comp)) => {
186+
match (lhs_comp, rhs_comp) {
187+
(
188+
std::path::Component::Normal(lhs_name),
189+
std::path::Component::Normal(rhs_name),
190+
) => {
191+
if lhs_name.to_string_lossy().to_lowercase()
192+
!= rhs_name.to_string_lossy().to_lowercase()
193+
{
194+
return None;
195+
}
196+
}
197+
// Non-Normal components must match exactly
198+
_ if lhs_comp != rhs_comp => {
199+
return None;
200+
}
201+
_ => {}
202+
}
203+
}
204+
(Some(lhs_comp), None) => {
205+
// rhs is fully consumed, but lhs has more components
206+
// This means rhs is a prefix of lhs, collect remaining lhs components
207+
let mut result = std::path::PathBuf::new();
208+
// Add the current lhs component that wasn't matched
209+
result.push(lhs_comp);
210+
// Add all remaining lhs components
211+
for component in lhs_components {
212+
result.push(component);
213+
}
214+
return Some(result);
215+
}
216+
(None, Some(_)) => {
217+
// lhs is shorter than rhs, so rhs cannot be a prefix of lhs
218+
return None;
219+
}
220+
(None, None) => {
221+
// Both paths have the same components, relative path is empty
222+
return Some(std::path::PathBuf::new());
223+
}
224+
}
158225
}
159226
}
160227

@@ -168,4 +235,89 @@ mod tests {
168235

169236
test_examples(PathRelativeTo {})
170237
}
238+
239+
#[test]
240+
fn test_case_insensitive_filesystem() {
241+
use nu_protocol::{Span, Value};
242+
use std::path::Path;
243+
244+
let args = Arguments {
245+
path: Spanned {
246+
item: "/Etc".to_string(),
247+
span: Span::test_data(),
248+
},
249+
};
250+
251+
let result = relative_to(Path::new("/etc"), Span::test_data(), &args);
252+
253+
// On case-insensitive filesystems (Windows, macOS), this should work
254+
// On case-sensitive filesystems (Linux, FreeBSD), this should fail
255+
if is_case_insensitive_filesystem() {
256+
match result {
257+
Value::String { val, .. } => {
258+
assert_eq!(val, "");
259+
}
260+
_ => panic!("Expected string result on case-insensitive filesystem"),
261+
}
262+
} else {
263+
match result {
264+
Value::Error { .. } => {
265+
// Expected on case-sensitive filesystems
266+
}
267+
_ => panic!("Expected error on case-sensitive filesystem"),
268+
}
269+
}
270+
}
271+
272+
#[test]
273+
fn test_case_insensitive_with_subpath() {
274+
use nu_protocol::{Span, Value};
275+
use std::path::Path;
276+
277+
let args = Arguments {
278+
path: Spanned {
279+
item: "/Home/User".to_string(),
280+
span: Span::test_data(),
281+
},
282+
};
283+
284+
let result = relative_to(Path::new("/home/user/documents"), Span::test_data(), &args);
285+
286+
if is_case_insensitive_filesystem() {
287+
match result {
288+
Value::String { val, .. } => {
289+
assert_eq!(val, "documents");
290+
}
291+
_ => panic!("Expected string result on case-insensitive filesystem"),
292+
}
293+
} else {
294+
match result {
295+
Value::Error { .. } => {
296+
// Expected on case-sensitive filesystems
297+
}
298+
_ => panic!("Expected error on case-sensitive filesystem"),
299+
}
300+
}
301+
}
302+
303+
#[test]
304+
fn test_truly_different_paths() {
305+
use nu_protocol::{Span, Value};
306+
use std::path::Path;
307+
308+
let args = Arguments {
309+
path: Spanned {
310+
item: "/Different/Path".to_string(),
311+
span: Span::test_data(),
312+
},
313+
};
314+
315+
let result = relative_to(Path::new("/home/user"), Span::test_data(), &args);
316+
317+
// This should fail on all filesystems since paths are truly different
318+
match result {
319+
Value::Error { .. } => {}
320+
_ => panic!("Expected error for truly different paths"),
321+
}
322+
}
171323
}

0 commit comments

Comments
 (0)