Skip to content

Commit

Permalink
Merge branch 'main' into topic/add-hasextension-and-trimendingdirecto…
Browse files Browse the repository at this point in the history
…ryseparator
  • Loading branch information
vbreuss authored Apr 20, 2024
2 parents 90a8b21 + c9d4256 commit 97d59fa
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 35 deletions.
15 changes: 15 additions & 0 deletions Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ internal static ArgumentException AppendAccessOnlyInWriteOnlyMode(
#endif
};

internal static ArgumentException BasePathNotFullyQualified(string paramName)
=> new("Basepath argument is not fully qualified.", paramName)
{
#if FEATURE_EXCEPTION_HRESULT
HResult = -2147024809
#endif
};

internal static IOException CannotCreateFileAsAlreadyExists(Execute execute, string path)
=> new(
$"Cannot create '{path}' because a file or directory with the same name already exists.",
Expand Down Expand Up @@ -127,6 +135,13 @@ internal static NotSupportedException NotSupportedSafeFileHandle()
internal static NotSupportedException NotSupportedTimerWrapping()
=> new("You cannot wrap an existing Timer in the MockTimeSystem instance!");

internal static ArgumentException NullCharacterInPath(string paramName)
#if NET8_0_OR_GREATER
=> new("Null character in path.", paramName);
#else
=> new("Illegal characters in path.", paramName);
#endif

internal static PlatformNotSupportedException OperationNotSupportedOnThisPlatform()
=> new("Operation is not supported on this platform.")
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using System;
using System.Text;

namespace Testably.Abstractions.Testing.Helpers;

Expand All @@ -18,6 +19,54 @@ private class LinuxPath(MockFileSystem fileSystem) : SimulatedPath(fileSystem)
/// <inheritdoc cref="IPath.VolumeSeparatorChar" />
public override char VolumeSeparatorChar => '/';

private readonly MockFileSystem _fileSystem = fileSystem;

/// <inheritdoc cref="IPath.GetFullPath(string)" />
public override string GetFullPath(string path)
{
path.EnsureValidArgument(_fileSystem, nameof(path));

if (!IsPathRooted(path))
{
path = Combine(_fileSystem.Storage.CurrentDirectory, path);
}

// We would ideally use realpath to do this, but it resolves symlinks and requires that the file actually exist.
string collapsedString = RemoveRelativeSegments(path, GetRootLength(path));

string result = collapsedString.Length == 0
? $"{DirectorySeparatorChar}"
: collapsedString;

if (result.Contains('\0', StringComparison.Ordinal))
{
throw ExceptionFactory.NullCharacterInPath(nameof(path));
}

return result;
}

#if FEATURE_PATH_RELATIVE
/// <inheritdoc cref="IPath.GetFullPath(string, string)" />
public override string GetFullPath(string path, string basePath)
{
path.EnsureValidArgument(_fileSystem, nameof(path));
basePath.EnsureValidArgument(_fileSystem, nameof(basePath));

if (!IsPathFullyQualified(basePath))
{
throw ExceptionFactory.BasePathNotFullyQualified(nameof(basePath));
}

if (IsPathFullyQualified(path))
{
return GetFullPath(path);
}

return GetFullPath(Combine(basePath, path));
}
#endif

/// <inheritdoc cref="IPath.GetInvalidFileNameChars()" />
public override char[] GetInvalidFileNameChars() => ['\0', '/'];

Expand Down Expand Up @@ -65,6 +114,12 @@ protected override bool IsDirectorySeparator(char c)
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#L77
/// </summary>
protected override bool IsPartiallyQualified(string path)
=> !IsPathRooted(path);

/// <summary>
/// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/Common/src/System/IO/PathInternal.Unix.cs#L39
/// </summary>
Expand Down
144 changes: 117 additions & 27 deletions Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -259,35 +260,11 @@ public ReadOnlySpan<char> GetFileNameWithoutExtension(ReadOnlySpan<char> path)
}

/// <inheritdoc cref="IPath.GetFullPath(string)" />
public string GetFullPath(string path)
{
path.EnsureValidArgument(fileSystem, nameof(path));

string? pathRoot = System.IO.Path.GetPathRoot(path);
string? directoryRoot =
System.IO.Path.GetPathRoot(fileSystem.Storage.CurrentDirectory);
if (!string.IsNullOrEmpty(pathRoot) && !string.IsNullOrEmpty(directoryRoot))
{
if (char.ToUpperInvariant(pathRoot[0]) != char.ToUpperInvariant(directoryRoot[0]))
{
return System.IO.Path.GetFullPath(path);
}

if (pathRoot.Length < directoryRoot.Length)
{
path = path.Substring(pathRoot.Length);
}
}

return System.IO.Path.GetFullPath(System.IO.Path.Combine(
fileSystem.Storage.CurrentDirectory,
path));
}
public abstract string GetFullPath(string path);

#if FEATURE_PATH_RELATIVE
/// <inheritdoc cref="IPath.GetFullPath(string, string)" />
public string GetFullPath(string path, string basePath)
=> System.IO.Path.GetFullPath(path, basePath);
public abstract string GetFullPath(string path, string basePath);
#endif

/// <inheritdoc cref="IPath.GetInvalidFileNameChars()" />
Expand Down Expand Up @@ -361,7 +338,14 @@ public bool IsPathFullyQualified(ReadOnlySpan<char> path)
#if FEATURE_PATH_RELATIVE
/// <inheritdoc cref="IPath.IsPathFullyQualified(string)" />
public bool IsPathFullyQualified(string path)
=> System.IO.Path.IsPathFullyQualified(path);
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}

return !IsPartiallyQualified(path);
}
#endif

#if FEATURE_SPAN
Expand Down Expand Up @@ -545,6 +529,8 @@ string NormalizePath(string path, bool ignoreStartingSeparator)
protected abstract bool IsDirectorySeparator(char c);
protected abstract bool IsEffectivelyEmpty(string path);

protected abstract bool IsPartiallyQualified(string path);

#if FEATURE_PATH_JOIN || FEATURE_PATH_ADVANCED
private string JoinInternal(string?[] paths)
{
Expand Down Expand Up @@ -594,6 +580,110 @@ protected string RandomString(int length)
.Select(s => s[fileSystem.RandomSystem.Random.Shared.Next(s.Length)]).ToArray());
}

/// <summary>
/// Remove relative segments from the given path (without combining with a root).
/// </summary>
protected string RemoveRelativeSegments(string path, int rootLength)
{
Debug.Assert(rootLength > 0);
bool flippedSeparator = false;

StringBuilder sb = new();

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 (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.Substring(0, skip));
}

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

if (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 (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 || 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 || 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 (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 != DirectorySeparatorChar && c == AltDirectorySeparatorChar)
{
c = DirectorySeparatorChar;
flippedSeparator = true;
}

sb.Append(c);
}

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

// 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 sb.ToString();
}

private bool TryGetExtensionIndex(string path, [NotNullWhen(true)] out int? dotIndex)
{
for (int i = path.Length - 1; i >= 0; i--)
Expand Down
Loading

0 comments on commit 97d59fa

Please sign in to comment.