@@ -295,7 +295,49 @@ public string GetRelativePath(string relativeTo, string path)
295
295
relativeTo = fileSystem . Execute . Path . GetFullPath ( relativeTo ) ;
296
296
path = fileSystem . Execute . Path . GetFullPath ( path ) ;
297
297
298
- return System . IO . Path . GetRelativePath ( relativeTo , path ) ;
298
+ // Need to check if the roots are different- if they are we need to return the "to" path.
299
+ if ( ! AreRootsEqual ( relativeTo , path , fileSystem . Execute . StringComparisonMode ) )
300
+ {
301
+ return path ;
302
+ }
303
+
304
+ Func < char , char , bool > charComparer = ( c1 , c2 ) => c1 == c2 ;
305
+ if ( fileSystem . Execute . StringComparisonMode == StringComparison . OrdinalIgnoreCase )
306
+ {
307
+ charComparer = ( c1 , c2 ) => char . ToUpperInvariant ( c1 ) == char . ToUpperInvariant ( c2 ) ;
308
+ }
309
+
310
+ int commonLength = GetCommonPathLength ( relativeTo , path , charComparer ) ;
311
+
312
+ // If there is nothing in common they can't share the same root, return the "to" path as is.
313
+ if ( commonLength == 0 )
314
+ {
315
+ return path ;
316
+ }
317
+
318
+ // Trailing separators aren't significant for comparison
319
+ int relativeToLength = relativeTo . Length ;
320
+ if ( IsDirectorySeparator ( relativeTo [ relativeToLength - 1 ] ) )
321
+ {
322
+ relativeToLength -- ;
323
+ }
324
+
325
+ int pathLength = path . Length ;
326
+ bool pathEndsInSeparator = IsDirectorySeparator ( path [ pathLength - 1 ] ) ;
327
+ if ( pathEndsInSeparator )
328
+ {
329
+ pathLength -- ;
330
+ }
331
+
332
+ // If we have effectively the same path, return "."
333
+ if ( relativeToLength == pathLength && commonLength >= relativeToLength )
334
+ {
335
+ return "." ;
336
+ }
337
+
338
+ return CreateRelativePath ( relativeTo , path ,
339
+ commonLength , relativeToLength , pathLength ,
340
+ pathEndsInSeparator ) ;
299
341
}
300
342
#endif
301
343
@@ -324,7 +366,7 @@ public bool HasExtension([NotNullWhen(true)] string? path)
324
366
return false ;
325
367
}
326
368
327
- return TryGetExtensionIndex ( path , out var dotIndex )
369
+ return TryGetExtensionIndex ( path , out int ? dotIndex )
328
370
&& dotIndex < path . Length - 1 ;
329
371
}
330
372
@@ -463,6 +505,24 @@ public bool TryJoin(ReadOnlySpan<char> path1,
463
505
464
506
#endregion
465
507
508
+ /// <summary>
509
+ /// Returns true if the two paths have the same root
510
+ /// </summary>
511
+ private bool AreRootsEqual ( string first , string second , StringComparison comparisonType )
512
+ {
513
+ int firstRootLength = GetRootLength ( first ) ;
514
+ int secondRootLength = GetRootLength ( second ) ;
515
+
516
+ return firstRootLength == secondRootLength
517
+ && string . Compare (
518
+ strA : first ,
519
+ indexA : 0 ,
520
+ strB : second ,
521
+ indexB : 0 ,
522
+ length : firstRootLength ,
523
+ comparisonType : comparisonType ) == 0 ;
524
+ }
525
+
466
526
private string CombineInternal ( string [ ] paths )
467
527
{
468
528
string NormalizePath ( string path , bool ignoreStartingSeparator )
@@ -524,6 +584,110 @@ string NormalizePath(string path, bool ignoreStartingSeparator)
524
584
return sb . ToString ( ) ;
525
585
}
526
586
587
+ /// <summary>
588
+ /// We have the same root, we need to calculate the difference now using the
589
+ /// common Length and Segment count past the length.
590
+ /// </summary>
591
+ /// <remarks>
592
+ /// Some examples:
593
+ /// <para />
594
+ /// C:\Foo C:\Bar L3, S1 -> ..\Bar<br />
595
+ /// C:\Foo C:\Foo\Bar L6, S0 -> Bar<br />
596
+ /// C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar<br />
597
+ /// C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar<br />
598
+ /// </remarks>
599
+ private string CreateRelativePath ( string relativeTo , string path , int commonLength ,
600
+ int relativeToLength , int pathLength , bool pathEndsInSeparator )
601
+ {
602
+ StringBuilder sb = new ( ) ;
603
+
604
+ // Add parent segments for segments past the common on the "from" path
605
+ if ( commonLength < relativeToLength )
606
+ {
607
+ sb . Append ( ".." ) ;
608
+
609
+ for ( int i = commonLength + 1 ; i < relativeToLength ; i ++ )
610
+ {
611
+ if ( IsDirectorySeparator ( relativeTo [ i ] ) )
612
+ {
613
+ sb . Append ( DirectorySeparatorChar ) ;
614
+ sb . Append ( ".." ) ;
615
+ }
616
+ }
617
+ }
618
+ else if ( IsDirectorySeparator ( path [ commonLength ] ) )
619
+ {
620
+ // No parent segments, and we need to eat the initial separator
621
+ commonLength ++ ;
622
+ }
623
+
624
+ // Now add the rest of the "to" path, adding back the trailing separator
625
+ int differenceLength = pathLength - commonLength ;
626
+ if ( pathEndsInSeparator )
627
+ {
628
+ differenceLength ++ ;
629
+ }
630
+
631
+ if ( differenceLength > 0 )
632
+ {
633
+ if ( sb . Length > 0 )
634
+ {
635
+ sb . Append ( DirectorySeparatorChar ) ;
636
+ }
637
+
638
+ sb . Append ( path . Substring ( commonLength , differenceLength ) ) ;
639
+ }
640
+
641
+ return sb . ToString ( ) ;
642
+ }
643
+
644
+ /// <summary>
645
+ /// Get the common path length from the start of the string.
646
+ /// </summary>
647
+ private int GetCommonPathLength ( string first , string second ,
648
+ Func < char , char , bool > charComparer )
649
+ {
650
+ int commonChars = 0 ;
651
+ for ( ; commonChars < first . Length ; commonChars ++ )
652
+ {
653
+ if ( second . Length < commonChars )
654
+ {
655
+ break ;
656
+ }
657
+
658
+ if ( ! charComparer ( first [ commonChars ] , second [ commonChars ] ) )
659
+ {
660
+ break ;
661
+ }
662
+ }
663
+
664
+ // If nothing matches
665
+ if ( commonChars == 0 )
666
+ {
667
+ return commonChars ;
668
+ }
669
+
670
+ // Or we're a full string and equal length or match to a separator
671
+ if ( commonChars == first . Length
672
+ && ( commonChars == second . Length || IsDirectorySeparator ( second [ commonChars ] ) ) )
673
+ {
674
+ return commonChars ;
675
+ }
676
+
677
+ if ( commonChars == second . Length && IsDirectorySeparator ( first [ commonChars ] ) )
678
+ {
679
+ return commonChars ;
680
+ }
681
+
682
+ // It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
683
+ while ( commonChars > 0 && ! IsDirectorySeparator ( first [ commonChars - 1 ] ) )
684
+ {
685
+ commonChars -- ;
686
+ }
687
+
688
+ return commonChars ;
689
+ }
690
+
527
691
protected abstract int GetRootLength ( string path ) ;
528
692
protected abstract bool IsDirectorySeparator ( char c ) ;
529
693
protected abstract bool IsEffectivelyEmpty ( string path ) ;
0 commit comments