Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
918bfba
Remove old internal API unit tests.
carlossanlop Jun 29, 2020
1319ff0
Add refs to System.Runtime.Extensions
carlossanlop Jun 29, 2020
523784a
csproj and projitems changes
carlossanlop Jun 29, 2020
470b4d3
Add Path.cs new API code and const int for initial buffer length.
carlossanlop Jun 29, 2020
2e2a925
Update platform-specific Path calls to old internal method.
carlossanlop Jun 29, 2020
1e17018
Remove old internal method from PathInternal
carlossanlop Jun 29, 2020
170140d
Add RedundantSegmentHelper class, with platform-specific methods.
carlossanlop Jun 29, 2020
ce0e1a7
Existing GetFullPath methods: align test cases, add missing tests for…
carlossanlop Jun 29, 2020
b0ca7d6
RedundantSegment unit tests.
carlossanlop Jun 29, 2020
a75b43a
Address suggestions
carlossanlop Jun 29, 2020
a60fe1b
Add comment to explain charsToSkip vs prefixAndRootLength.
carlossanlop Jun 30, 2020
5160156
Update license header.
carlossanlop Sep 28, 2020
5716533
Improve public documentation.
carlossanlop Sep 28, 2020
d791614
Remove unused directive and add nullability directive to Unix Path file.
carlossanlop Nov 25, 2020
8236cdc
Remove duplicate test cases causing test warnings.
carlossanlop Jan 19, 2021
45c3de1
Add RedundantSegmentHelper class, with platform-specific methods.
carlossanlop Jun 29, 2020
9d3e004
RedundantSegment unit tests.
carlossanlop Jun 29, 2020
c92959d
Address suggestions
carlossanlop Jun 29, 2020
f1fa5d1
Add comment to explain charsToSkip vs prefixAndRootLength.
carlossanlop Jun 30, 2020
ec7bca3
Update license header.
carlossanlop Sep 28, 2020
19405cd
Remove unused directive and add nullability directive to Unix Path file.
carlossanlop Nov 25, 2020
90dc158
Add condition to Interop.Libraries.cs in S.R.Ext.Tests.csproj require…
carlossanlop Nov 25, 2020
39d9387
Small fixes
carlossanlop Nov 26, 2020
c65e25f
Fix merge conflict
carlossanlop Jan 19, 2021
3b72390
Use string overload in Path.GetFullPath to ensure the same string is …
carlossanlop Jan 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 0 additions & 340 deletions src/libraries/Common/tests/Tests/System/IO/PathInternal.Tests.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472</TargetFrameworks>
<DefineConstants>$(DefineConstants);MS_IO_REDIST</DefineConstants>
Expand Down Expand Up @@ -120,6 +120,10 @@
Link="System\IO\PathInternal.cs" />
<Compile Include="$(CoreLibSharedDir)System\IO\PathInternal.Windows.cs"
Link="System\IO\PathInternal.Windows.cs" />
<Compile Include="$(CoreLibSharedDir)System\IO\RedundantSegmentHelper.cs"
Link="System\IO\RedundantSegmentHelper.cs" />
<Compile Include="$(CoreLibSharedDir)System\IO\RedundantSegmentHelper.Windows.cs"
Link="System\IO\RedundantSegmentHelper.Windows.cs" />
<Compile Include="$(CommonPath)System\IO\Win32Marshal.cs"
Link="Common\System\IO\Win32Marshal.cs" />
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathInternal.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathTooLongException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PinnedBufferMemoryStream.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\RedundantSegmentHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\SeekOrigin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\Stream.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\StreamReader.cs" />
Expand Down Expand Up @@ -1610,14 +1611,15 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\GlobalizationMode.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\HijriCalendar.Win32.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Guid.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DisableMediaInsertionPrompt.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DriveInfoInternal.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStream.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStream.Win32.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStreamCompletionSource.Win32.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\Path.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathHelper.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathInternal.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DisableMediaInsertionPrompt.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\RedundantSegmentHelper.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\PasteArguments.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Loader\LibraryNameVariation.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\MemoryFailPoint.Windows.cs" />
Expand Down Expand Up @@ -1822,6 +1824,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\IO\Path.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathInternal.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PersistedFiles.Names.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\RedundantSegmentHelper.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\PasteArguments.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Loader\LibraryNameVariation.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\MemoryFailPoint.Unix.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
Expand Down Expand Up @@ -33,7 +34,7 @@ public static string GetFullPath(string path)

// We would ideally use realpath to do this, but it resolves symlinks, requires that the file actually exist,
// and turns it into a full path, which we only want if fullCheck is true.
string collapsedString = PathInternal.RemoveRelativeSegments(path, PathInternal.GetRootLength(path));
string collapsedString = Path.RemoveRedundantSegments(path);

Debug.Assert(collapsedString.Length < path.Length || collapsedString.ToString() == path,
"Either we've removed characters, or the string should be unmodified from the input path.");
Expand All @@ -57,9 +58,12 @@ public static string GetFullPath(string path, string basePath)
if (basePath.Contains('\0') || path.Contains('\0'))
throw new ArgumentException(SR.Argument_InvalidPathChars);

if (IsPathFullyQualified(path))
if (Path.IsPathRooted(path))
return GetFullPath(path);

if (PathInternal.IsEffectivelyEmpty(path.AsSpan()))
return basePath;

return GetFullPath(CombineInternal(basePath, path));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,7 @@ public static string GetFullPath(string path, string basePath)
// to Windows APIs won't do anything by design. Additionally, GetFullPathName() in Windows doesn't root
// them properly. As such we need to manually remove segments and not use GetFullPath().

return PathInternal.IsDevice(combinedPath.AsSpan())
? PathInternal.RemoveRelativeSegments(combinedPath, PathInternal.GetRootLength(combinedPath.AsSpan()))
: GetFullPath(combinedPath);
return PathInternal.IsDevice(combinedPath.AsSpan()) ? RemoveRedundantSegments(combinedPath) : GetFullPath(combinedPath);
}

public static string GetTempPath()
Expand Down
103 changes: 100 additions & 3 deletions src/libraries/System.Private.CoreLib/src/System/IO/Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public static partial class Path
// 8 random bytes provides 12 chars in our encoding for the 8.3 name.
private const int KeyLength = 8;

// Initial cross-platform length for a buffer that is to be passed to the ValueStringBuilder constructor.
// The ValueStringBuilder will increase the internal buffer length when necessary.
// The value is equivalent to PathInternal.MaxShortPath, a limitation that only exists in older Windows versions.
private const int InitialValueStringBuilderBufferLength = 260;

[Obsolete("Please use GetInvalidPathChars or GetInvalidFileNameChars instead.")]
public static readonly char[] InvalidPathChars = GetInvalidPathChars();

Expand Down Expand Up @@ -383,7 +388,7 @@ public static string Combine(params string[] paths)
maxSize++;
}

var builder = new ValueStringBuilder(stackalloc char[260]); // MaxShortPath on Windows
var builder = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]);
builder.EnsureCapacity(maxSize);

for (int i = firstComponent; i < paths.Length; i++)
Expand Down Expand Up @@ -490,7 +495,7 @@ public static string Join(params string?[] paths)
}
maxSize += paths.Length - 1;

var builder = new ValueStringBuilder(stackalloc char[260]); // MaxShortPath on Windows
var builder = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]);
builder.EnsureCapacity(maxSize);

for (int i = 0; i < paths.Length; i++)
Expand Down Expand Up @@ -916,7 +921,7 @@ private static string GetRelativePath(string relativeTo, string path, StringComp
// C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
// C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar

var sb = new ValueStringBuilder(stackalloc char[260]);
var sb = new ValueStringBuilder(stackalloc char[InitialValueStringBuilderBufferLength]);
sb.EnsureCapacity(Math.Max(relativeTo.Length, path.Length));

// Add parent segments for segments past the common on the "from" path
Expand Down Expand Up @@ -983,5 +988,97 @@ private static string GetRelativePath(string relativeTo, string path, StringComp
/// Returns true if the path ends in a directory separator.
/// </summary>
public static bool EndsInDirectorySeparator(string path) => PathInternal.EndsInDirectorySeparator(path);

/// <summary>
/// Removes redundant segments from the specified string containing a path.
/// </summary>
/// <param name="path">The path to remove redundant segments from.</param>
/// <returns>The <paramref name="path" /> without redundant segments, or <see langword="null" /> if <paramref name="path"/> is <see langword="null" />, or <see cref="string.Empty"/> if <paramref name="path"/> is effectively empty (an empty string or whitespace characters).</returns>
[return: NotNullIfNotNull("path")]
public static string? RemoveRedundantSegments(string? path)
{
if (path == null)
{
return null;
}

var spanPath = path.AsSpan();
if (PathInternal.IsEffectivelyEmpty(spanPath))
{
return string.Empty;
}

var sb = new ValueStringBuilder(spanPath.Length > InitialValueStringBuilderBufferLength? InitialValueStringBuilderBufferLength: spanPath.Length);

if (!RedundantSegmentHelper.TryRemoveRedundantSegments(spanPath, ref sb))
{
sb.Dispose();
return path;
}

return sb.ToString(); // Disposes
}

/// <summary>
/// Removes redundant segments from the specified read-only span of characters containing a path.
/// </summary>
/// <param name="path">The path to remove redundant segments from.</param>
/// <returns>The <paramref name="path" /> without redundant segments, or <see cref="string.Empty"/> if <paramref name="path"/> is effectively empty (an empty string or whitespace characters).</returns>
public static string RemoveRedundantSegments(ReadOnlySpan<char> path)
{
if (PathInternal.IsEffectivelyEmpty(path))
{
return string.Empty;
}

var sb = new ValueStringBuilder(path.Length > InitialValueStringBuilderBufferLength ? InitialValueStringBuilderBufferLength : path.Length);

if (!RedundantSegmentHelper.TryRemoveRedundantSegments(path, ref sb))
{
sb.Dispose();
return path.ToString();
}
else
{
return sb.ToString(); // Disposes
}
}

/// <summary>
/// Tries to remove redundant segments from the specified path read-only span.
/// </summary>
/// <param name="path">The path to analyze.</param>
/// <param name="destination">When this method returns <see langword="true" />, if <paramref name="path"/> contained redundant segments, this parameter contains the <paramref name="path"/> without redundant segments, or if <paramref name="path"/> did not contain redundant segments, or was an invalid path, this parameter contains the value of <paramref name="path"/>, unmodified; when this method returns <see langword="false" />, this parameter does not contain a valid value.</param>
/// <param name="charsWritten">The total number of characters written to <paramref name="destination" />, which is less or equal than the length of <paramref name="path" />.</param>
/// <returns><see langword="true" /> if the original path was modified and writing into <paramref name="destination" /> was successful; <see langword="false" /> if <paramref name="path" /> is effectively empty (an empty string or whitespace characters) or there was a problem attempting to write into <paramref name="destination"/>.</returns>
public static bool TryRemoveRedundantSegments(ReadOnlySpan<char> path, Span<char> destination, out int charsWritten)
{
charsWritten = 0;

if (PathInternal.IsEffectivelyEmpty(path))
{
return false;
}

var sb = new ValueStringBuilder(path.Length > InitialValueStringBuilderBufferLength ? InitialValueStringBuilderBufferLength : path.Length);

bool result = false;

if (!RedundantSegmentHelper.TryRemoveRedundantSegments(path, ref sb))
{
if (path.TryCopyTo(destination))
{
charsWritten = path.Length;
result = true;
sb.Dispose();
}
}
else
{
result = sb.TryCopyTo(destination, out charsWritten); // Disposes
}

return result;
}
}
}
118 changes: 0 additions & 118 deletions src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,124 +100,6 @@ internal static bool AreRootsEqual(string? first, string? second, StringComparis
comparisonType: comparisonType) == 0;
}

/// <summary>
/// Try to remove relative segments from the given path (without combining with a root).
/// </summary>
/// <param name="path">Input path</param>
/// <param name="rootLength">The length of the root of the given path</param>
internal static string RemoveRelativeSegments(string path, int rootLength)
{
var sb = new ValueStringBuilder(stackalloc char[260 /* PathInternal.MaxShortPath */]);

if (RemoveRelativeSegments(path.AsSpan(), rootLength, ref sb))
{
path = sb.ToString();
}

sb.Dispose();
return path;
}

/// <summary>
/// Try to remove relative segments from the given path (without combining with a root).
/// </summary>
/// <param name="path">Input path</param>
/// <param name="rootLength">The length of the root of the given path</param>
/// <param name="sb">String builder that will store the result</param>
/// <returns>"true" if the path was modified</returns>
internal static bool RemoveRelativeSegments(ReadOnlySpan<char> path, int rootLength, ref ValueStringBuilder sb)
{
Debug.Assert(rootLength > 0);
bool flippedSeparator = false;

int skip = rootLength;
// We treat "\.." , "\." and "\\" as a relative segment. We want to collapse the first separator past the root presuming
// the root actually ends in a separator. Otherwise the first segment for RemoveRelativeSegments
// 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.
if (PathInternal.IsDirectorySeparator(path[skip - 1]))
skip--;

// Remove "//", "/./", and "/../" from the path by copying each character to the output,
// except the ones we're removing, such that the builder contains the normalized path
// at the end.
if (skip > 0)
{
sb.Append(path.Slice(0, skip));
}

for (int i = skip; i < path.Length; i++)
{
char c = path[i];

if (PathInternal.IsDirectorySeparator(c) && i + 1 < path.Length)
{
// Skip this character if it's a directory separator and if the next character is, too,
// e.g. "parent//child" => "parent/child"
if (PathInternal.IsDirectorySeparator(path[i + 1]))
{
continue;
}

// Skip this character and the next if it's referring to the current directory,
// e.g. "parent/./child" => "parent/child"
if ((i + 2 == path.Length || PathInternal.IsDirectorySeparator(path[i + 2])) &&
path[i + 1] == '.')
{
i++;
continue;
}

// Skip this character and the next two if it's referring to the parent directory,
// e.g. "parent/child/../grandchild" => "parent/grandchild"
if (i + 2 < path.Length &&
(i + 3 == path.Length || PathInternal.IsDirectorySeparator(path[i + 3])) &&
path[i + 1] == '.' && path[i + 2] == '.')
{
// Unwind back to the last slash (and if there isn't one, clear out everything).
int s;
for (s = sb.Length - 1; s >= skip; s--)
{
if (PathInternal.IsDirectorySeparator(sb[s]))
{
sb.Length = (i + 3 >= path.Length && s == skip) ? s + 1 : s; // to avoid removing the complete "\tmp\" segment in cases like \\?\C:\tmp\..\, C:\tmp\..
break;
}
}
if (s < skip)
{
sb.Length = skip;
}

i += 2;
continue;
}
}

// Normalize the directory separator if needed
if (c != PathInternal.DirectorySeparatorChar && c == PathInternal.AltDirectorySeparatorChar)
{
c = PathInternal.DirectorySeparatorChar;
flippedSeparator = true;
}

sb.Append(c);
}

// If we haven't changed the source path, return the original
if (!flippedSeparator && sb.Length == path.Length)
{
return false;
}

// We may have eaten the trailing separator from the root when we started and not replaced it
if (skip != rootLength && sb.Length < rootLength)
{
sb.Append(path[rootLength - 1]);
}

return true;
}

/// <summary>
/// Trims one trailing directory separator beyond the root of the path.
/// </summary>
Expand Down
Loading