@@ -144,17 +144,84 @@ path."#
144144fn  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