|
1 | 1 | using System;
|
| 2 | +using System.Diagnostics; |
2 | 3 | using System.Diagnostics.CodeAnalysis;
|
3 | 4 | using System.Linq;
|
4 | 5 | using System.Text;
|
@@ -259,35 +260,11 @@ public ReadOnlySpan<char> GetFileNameWithoutExtension(ReadOnlySpan<char> path)
|
259 | 260 | }
|
260 | 261 |
|
261 | 262 | /// <inheritdoc cref="IPath.GetFullPath(string)" />
|
262 |
| - public string GetFullPath(string path) |
263 |
| - { |
264 |
| - path.EnsureValidArgument(fileSystem, nameof(path)); |
265 |
| - |
266 |
| - string? pathRoot = System.IO.Path.GetPathRoot(path); |
267 |
| - string? directoryRoot = |
268 |
| - System.IO.Path.GetPathRoot(fileSystem.Storage.CurrentDirectory); |
269 |
| - if (!string.IsNullOrEmpty(pathRoot) && !string.IsNullOrEmpty(directoryRoot)) |
270 |
| - { |
271 |
| - if (char.ToUpperInvariant(pathRoot[0]) != char.ToUpperInvariant(directoryRoot[0])) |
272 |
| - { |
273 |
| - return System.IO.Path.GetFullPath(path); |
274 |
| - } |
275 |
| - |
276 |
| - if (pathRoot.Length < directoryRoot.Length) |
277 |
| - { |
278 |
| - path = path.Substring(pathRoot.Length); |
279 |
| - } |
280 |
| - } |
281 |
| - |
282 |
| - return System.IO.Path.GetFullPath(System.IO.Path.Combine( |
283 |
| - fileSystem.Storage.CurrentDirectory, |
284 |
| - path)); |
285 |
| - } |
| 263 | + public abstract string GetFullPath(string path); |
286 | 264 |
|
287 | 265 | #if FEATURE_PATH_RELATIVE
|
288 | 266 | /// <inheritdoc cref="IPath.GetFullPath(string, string)" />
|
289 |
| - public string GetFullPath(string path, string basePath) |
290 |
| - => System.IO.Path.GetFullPath(path, basePath); |
| 267 | + public abstract string GetFullPath(string path, string basePath); |
291 | 268 | #endif
|
292 | 269 |
|
293 | 270 | /// <inheritdoc cref="IPath.GetInvalidFileNameChars()" />
|
@@ -353,7 +330,14 @@ public bool IsPathFullyQualified(ReadOnlySpan<char> path)
|
353 | 330 | #if FEATURE_PATH_RELATIVE
|
354 | 331 | /// <inheritdoc cref="IPath.IsPathFullyQualified(string)" />
|
355 | 332 | public bool IsPathFullyQualified(string path)
|
356 |
| - => System.IO.Path.IsPathFullyQualified(path); |
| 333 | + { |
| 334 | + if (path == null) |
| 335 | + { |
| 336 | + throw new ArgumentNullException(nameof(path)); |
| 337 | + } |
| 338 | + |
| 339 | + return !IsPartiallyQualified(path); |
| 340 | + } |
357 | 341 | #endif
|
358 | 342 |
|
359 | 343 | #if FEATURE_SPAN
|
@@ -533,6 +517,8 @@ string NormalizePath(string path, bool ignoreStartingSeparator)
|
533 | 517 | protected abstract bool IsDirectorySeparator(char c);
|
534 | 518 | protected abstract bool IsEffectivelyEmpty(string path);
|
535 | 519 |
|
| 520 | + protected abstract bool IsPartiallyQualified(string path); |
| 521 | + |
536 | 522 | #if FEATURE_PATH_JOIN || FEATURE_PATH_ADVANCED
|
537 | 523 | private string JoinInternal(string?[] paths)
|
538 | 524 | {
|
@@ -582,6 +568,110 @@ protected string RandomString(int length)
|
582 | 568 | .Select(s => s[fileSystem.RandomSystem.Random.Shared.Next(s.Length)]).ToArray());
|
583 | 569 | }
|
584 | 570 |
|
| 571 | + /// <summary> |
| 572 | + /// Remove relative segments from the given path (without combining with a root). |
| 573 | + /// </summary> |
| 574 | + protected string RemoveRelativeSegments(string path, int rootLength) |
| 575 | + { |
| 576 | + Debug.Assert(rootLength > 0); |
| 577 | + bool flippedSeparator = false; |
| 578 | + |
| 579 | + StringBuilder sb = new(); |
| 580 | + |
| 581 | + int skip = rootLength; |
| 582 | + // We treat "\.." , "\." and "\\" as a relative segment. We want to collapse the first separator past the root presuming |
| 583 | + // the root actually ends in a separator. Otherwise the first segment for RemoveRelativeSegments |
| 584 | + // in cases like "\\?\C:\.\" and "\\?\C:\..\", the first segment after the root will be ".\" and "..\" which is not considered as a relative segment and hence not be removed. |
| 585 | + if (IsDirectorySeparator(path[skip - 1])) |
| 586 | + { |
| 587 | + skip--; |
| 588 | + } |
| 589 | + |
| 590 | + // Remove "//", "/./", and "/../" from the path by copying each character to the output, |
| 591 | + // except the ones we're removing, such that the builder contains the normalized path |
| 592 | + // at the end. |
| 593 | + if (skip > 0) |
| 594 | + { |
| 595 | + sb.Append(path.Substring(0, skip)); |
| 596 | + } |
| 597 | + |
| 598 | + for (int i = skip; i < path.Length; i++) |
| 599 | + { |
| 600 | + char c = path[i]; |
| 601 | + |
| 602 | + if (IsDirectorySeparator(c) && i + 1 < path.Length) |
| 603 | + { |
| 604 | + // Skip this character if it's a directory separator and if the next character is, too, |
| 605 | + // e.g. "parent//child" => "parent/child" |
| 606 | + if (IsDirectorySeparator(path[i + 1])) |
| 607 | + { |
| 608 | + continue; |
| 609 | + } |
| 610 | + |
| 611 | + // Skip this character and the next if it's referring to the current directory, |
| 612 | + // e.g. "parent/./child" => "parent/child" |
| 613 | + if ((i + 2 == path.Length || IsDirectorySeparator(path[i + 2])) && |
| 614 | + path[i + 1] == '.') |
| 615 | + { |
| 616 | + i++; |
| 617 | + continue; |
| 618 | + } |
| 619 | + |
| 620 | + // Skip this character and the next two if it's referring to the parent directory, |
| 621 | + // e.g. "parent/child/../grandchild" => "parent/grandchild" |
| 622 | + if (i + 2 < path.Length && |
| 623 | + (i + 3 == path.Length || IsDirectorySeparator(path[i + 3])) && |
| 624 | + path[i + 1] == '.' && path[i + 2] == '.') |
| 625 | + { |
| 626 | + // Unwind back to the last slash (and if there isn't one, clear out everything). |
| 627 | + int s; |
| 628 | + for (s = sb.Length - 1; s >= skip; s--) |
| 629 | + { |
| 630 | + if (IsDirectorySeparator(sb[s])) |
| 631 | + { |
| 632 | + sb.Length = |
| 633 | + i + 3 >= path.Length && s == skip |
| 634 | + ? s + 1 |
| 635 | + : s; // to avoid removing the complete "\tmp\" segment in cases like \\?\C:\tmp\..\, C:\tmp\.. |
| 636 | + break; |
| 637 | + } |
| 638 | + } |
| 639 | + |
| 640 | + if (s < skip) |
| 641 | + { |
| 642 | + sb.Length = skip; |
| 643 | + } |
| 644 | + |
| 645 | + i += 2; |
| 646 | + continue; |
| 647 | + } |
| 648 | + } |
| 649 | + |
| 650 | + // Normalize the directory separator if needed |
| 651 | + if (c != DirectorySeparatorChar && c == AltDirectorySeparatorChar) |
| 652 | + { |
| 653 | + c = DirectorySeparatorChar; |
| 654 | + flippedSeparator = true; |
| 655 | + } |
| 656 | + |
| 657 | + sb.Append(c); |
| 658 | + } |
| 659 | + |
| 660 | + // If we haven't changed the source path, return the original |
| 661 | + if (!flippedSeparator && sb.Length == path.Length) |
| 662 | + { |
| 663 | + return path; |
| 664 | + } |
| 665 | + |
| 666 | + // We may have eaten the trailing separator from the root when we started and not replaced it |
| 667 | + if (skip != rootLength && sb.Length < rootLength) |
| 668 | + { |
| 669 | + sb.Append(path[rootLength - 1]); |
| 670 | + } |
| 671 | + |
| 672 | + return sb.ToString(); |
| 673 | + } |
| 674 | + |
585 | 675 | private bool TryGetExtensionIndex(string path, [NotNullWhen(true)] out int? dotIndex)
|
586 | 676 | {
|
587 | 677 | for (int i = path.Length - 1; i >= 0; i--)
|
|
0 commit comments