Skip to content

Commit

Permalink
feat: implement GetDirectoryName for simulated Path (#571)
Browse files Browse the repository at this point in the history
Implement the `GetDirectoryName` methods for `Path`.
  • Loading branch information
vbreuss authored Apr 20, 2024
1 parent 8eb3fd1 commit 2932ab1
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Testably.Abstractions.Testing.Helpers;
using System.Text;

namespace Testably.Abstractions.Testing.Helpers;

internal partial class Execute
{
Expand Down Expand Up @@ -43,10 +45,67 @@ public override string GetTempPath()
public override bool IsPathRooted(string? path)
=> path?.Length > 0 && path[0] == '/';

/// <summary>
/// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/Common/src/System/IO/PathInternal.Unix.cs#L22
/// </summary>
protected override int GetRootLength(string path)
{
return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0;
}

/// <summary>
/// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/Common/src/System/IO/PathInternal.Unix.cs#L27
/// </summary>
protected override bool IsDirectorySeparator(char c)
=> c == DirectorySeparatorChar;

/// <summary>
/// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/Common/src/System/IO/PathInternal.Unix.cs#L89
/// </summary>
protected override bool IsEffectivelyEmpty(string path)
=> string.IsNullOrEmpty(path);

/// <summary>
/// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/Common/src/System/IO/PathInternal.Unix.cs#L39
/// </summary>
protected override string NormalizeDirectorySeparators(string path)
{
bool IsAlreadyNormalized()
{
for (int i = 0; i < path.Length - 1; i++)
{
if (IsDirectorySeparator(path[i]) &&
IsDirectorySeparator(path[i + 1]))
{
return false;
}
}

return true;
}

if (IsAlreadyNormalized())
{
return path;
}

StringBuilder builder = new(path.Length);

for (int j = 0; j < path.Length - 1; j++)
{
char current = path[j];

if (IsDirectorySeparator(current)
&& IsDirectorySeparator(path[j + 1]))
{
continue;
}

builder.Append(current);
}

builder.Append(path[path.Length - 1]);
return builder.ToString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,31 @@ public ReadOnlySpan<char> GetDirectoryName(ReadOnlySpan<char> path)

/// <inheritdoc cref="IPath.GetDirectoryName(string)" />
public string? GetDirectoryName(string? path)
=> System.IO.Path.GetDirectoryName(path);
{
if (path == null || IsEffectivelyEmpty(path))
{
return null;
}

int rootLength = GetRootLength(path);
if (path.Length <= rootLength)
{
return null;
}

int end = path.Length;
while (end > rootLength && !IsDirectorySeparator(path[end - 1]))
{
end--;
}

while (end > rootLength && IsDirectorySeparator(path[end - 1]))
{
end--;
}

return NormalizeDirectorySeparators(path.Substring(0, end));
}

#if FEATURE_SPAN
/// <inheritdoc cref="IPath.GetExtension(ReadOnlySpan{char})" />
Expand Down Expand Up @@ -447,7 +471,9 @@ public bool TryJoin(ReadOnlySpan<char> path1,
private static string CombineInternal(string[] paths)
=> System.IO.Path.Combine(paths);

protected abstract int GetRootLength(string path);
protected abstract bool IsDirectorySeparator(char c);
protected abstract bool IsEffectivelyEmpty(string path);

#if FEATURE_PATH_JOIN || FEATURE_PATH_ADVANCED
private string JoinInternal(string?[] paths)
Expand Down Expand Up @@ -489,6 +515,8 @@ private string JoinInternal(string?[] paths)
}
#endif

protected abstract string NormalizeDirectorySeparators(string path);

protected string RandomString(int length)
{
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
Expand Down
176 changes: 176 additions & 0 deletions Source/Testably.Abstractions.Testing/Helpers/Execute.WindowsPath.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Linq;
using System.Text;

namespace Testably.Abstractions.Testing.Helpers;

Expand Down Expand Up @@ -63,17 +65,191 @@ public override bool IsPathRooted(string? path)
(length >= 2 && IsValidDriveChar(path![0]) && path[1] == VolumeSeparatorChar);
}

/// <summary>
/// https://github.com/dotnet/runtime/blob/v8.0.3/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L181
/// </summary>
protected override int GetRootLength(string path)
{
bool IsDeviceUNC(string p)
=> p.Length >= 8
&& IsDevice(p)
&& IsDirectorySeparator(p[7])
&& p[4] == 'U'
&& p[5] == 'N'
&& p[6] == 'C';

bool IsDevice(string p)
=> IsExtended(p)
||
(
p.Length >= 4
&& IsDirectorySeparator(p[0])
&& IsDirectorySeparator(p[1])
&& (p[2] == '.' || p[2] == '?')
&& IsDirectorySeparator(p[3])
);

bool IsExtended(string p)
=> p.Length >= 4
&& p[0] == '\\'
&& (p[1] == '\\' || p[1] == '?')
&& p[2] == '?'
&& p[3] == '\\';

int pathLength = path.Length;

if (pathLength > 0 && IsDirectorySeparator(path[0]))
{
bool deviceSyntax = IsDevice(path);
bool deviceUnc = deviceSyntax && IsDeviceUNC(path);

if (deviceSyntax && !deviceUnc)
{
return GetRootLengthWithDeviceSyntax(path);
}

// UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
if (deviceUnc || (path.Length > 1 && IsDirectorySeparator(path[1])))
{
return GetRootLengthWithDeviceUncSyntax(path, deviceUnc);
}

// Current drive rooted (e.g. "\foo")
return 1;
}

if (pathLength >= 2
&& path[1] == ':'
&& IsValidDriveChar(path[0]))
{
// If the colon is followed by a directory separator, move past it (e.g "C:\")
if (pathLength > 2 && IsDirectorySeparator(path[2]))
{
return 3;
}

// Valid drive specified path ("C:", "D:", etc.)
return 2;
}

return 0;
}

private int GetRootLengthWithDeviceSyntax(string path)
{
// Device path (e.g. "\\?\.", "\\.\")
// Skip any characters following the prefix that aren't a separator
int i = 4;
while (i < path.Length && !IsDirectorySeparator(path[i]))
{
i++;
}

// If there is another separator take it, as long as we have had at least one
// non-separator after the prefix (e.g. don't take "\\?\\", but take "\\?\a\")
if (i < path.Length && i > 4 && IsDirectorySeparator(path[i]))
{
i++;
}

return i;
}

private int GetRootLengthWithDeviceUncSyntax(string path,
bool deviceUnc)
{
// Start past the prefix ("\\" or "\\?\UNC\")
int i = deviceUnc ? 8 : 2;

// Skip two separators at most
int n = 2;
while (i < path.Length && (!IsDirectorySeparator(path[i]) || --n > 0))
{
i++;
}

return i;
}

/// <summary>
/// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L280
/// </summary>
protected override bool IsDirectorySeparator(char c)
=> c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;

/// <summary>
/// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L381
/// </summary>
protected override bool IsEffectivelyEmpty(string path)
{
if (string.IsNullOrEmpty(path))
{
return true;
}

return path.All(c => c == ' ');
}

/// <summary>
/// Returns true if the given character is a valid drive letter
/// </summary>
/// <remarks>https://github.com/dotnet/runtime/blob/v8.0.3/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L72</remarks>
private static bool IsValidDriveChar(char value)
=> (uint)((value | 0x20) - 'a') <= 'z' - 'a';

/// <summary>
/// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L318
/// </summary>
protected override string NormalizeDirectorySeparators(string path)
{
bool IsAlreadyNormalized()
{
for (int i = 1; i < path.Length; i++)
{
char current = path[i];
if (IsDirectorySeparator(current)
&& (current != DirectorySeparatorChar
|| (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))))
{
return false;
}
}

return true;
}

if (IsAlreadyNormalized())
{
return path;
}

StringBuilder builder = new();

int start = 0;
if (IsDirectorySeparator(path[start]))
{
start++;
builder.Append(DirectorySeparatorChar);
}

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

if (IsDirectorySeparator(current))
{
if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))
{
continue;
}

current = DirectorySeparatorChar;
}

builder.Append(current);
}

return builder.ToString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ private bool IncludeSimulatedTests(ClassModel @class)
[
"ChangeExtensionTests",
"EndsInDirectorySeparatorTests",
"GetDirectoryNameTests",
"GetExtensionTests",
"GetFileNameTests",
"GetFileNameWithoutExtensionTests",
Expand Down
Loading

0 comments on commit 2932ab1

Please sign in to comment.