Add version 12 implementations of path utility classes

This commit is contained in:
Alex Barney 2021-07-11 23:09:39 -07:00
parent f444a999ba
commit 79a4c62b2e
8 changed files with 2479 additions and 0 deletions

View file

@ -0,0 +1,630 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using LibHac.Common;
using LibHac.Diag;
using LibHac.FsSystem;
using LibHac.Util;
using static LibHac.Fs.StringTraits;
namespace LibHac.Fs.Common
{
public static class PathFormatter
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Result CheckHostName(ReadOnlySpan<byte> name)
{
if (name.Length == 2 && name[0] == Dot && name[1] == Dot)
return ResultFs.InvalidPathFormat.Log();
for (int i = 0; i < name.Length; i++)
{
if (name[i] == ':' || name[i] == '$')
return ResultFs.InvalidPathFormat.Log();
}
return Result.Success;
}
private static Result CheckSharedName(ReadOnlySpan<byte> name)
{
if (name.Length == 1 && name[0] == Dot)
return ResultFs.InvalidPathFormat.Log();
if (name.Length == 2 && name[0] == Dot && name[1] == Dot)
return ResultFs.InvalidPathFormat.Log();
for (int i = 0; i < name.Length; i++)
{
if (name[i] == ':')
return ResultFs.InvalidPathFormat.Log();
}
return Result.Success;
}
public static Result ParseMountName(out ReadOnlySpan<byte> newPath, out int mountNameLength,
Span<byte> outMountNameBuffer, ReadOnlySpan<byte> path)
{
Assert.SdkRequiresNotNull(path);
UnsafeHelpers.SkipParamInit(out mountNameLength);
newPath = default;
int maxMountLength = outMountNameBuffer.Length == 0
? PathTools.MountNameLengthMax + 1
: Math.Min(outMountNameBuffer.Length, PathTools.MountNameLengthMax + 1);
int mountLength;
for (mountLength = 0; mountLength < maxMountLength && path.At(mountLength) != 0; mountLength++)
{
byte c = path[mountLength];
if (c == DriveSeparator)
{
mountLength++;
break;
}
if (c == DirectorySeparator || c == AltDirectorySeparator)
{
newPath = path;
mountNameLength = 0;
return Result.Success;
}
}
if (mountLength <= 2 || path[mountLength - 1] != DriveSeparator)
{
newPath = path;
mountNameLength = 0;
return Result.Success;
}
for (int i = 0; i < mountLength; i++)
{
if (path.At(i) is (byte)'*' or (byte)'?' or (byte)'<' or (byte)'>' or (byte)'|')
return ResultFs.InvalidCharacter.Log();
}
if (!outMountNameBuffer.IsEmpty)
{
if (mountLength >= outMountNameBuffer.Length)
return ResultFs.TooLongPath.Log();
path.Slice(0, mountLength).CopyTo(outMountNameBuffer);
outMountNameBuffer[mountLength] = NullTerminator;
}
newPath = path.Slice(mountLength);
mountNameLength = mountLength;
return Result.Success;
}
public static Result SkipMountName(out ReadOnlySpan<byte> newPath, out int mountNameLength,
ReadOnlySpan<byte> path)
{
return ParseMountName(out newPath, out mountNameLength, Span<byte>.Empty, path);
}
private static Result ParseWindowsPathImpl(out ReadOnlySpan<byte> newPath, out int windowsPathLength,
Span<byte> normalizeBuffer, ReadOnlySpan<byte> path, bool hasMountName)
{
Assert.SdkRequiresNotNull(path);
UnsafeHelpers.SkipParamInit(out windowsPathLength);
newPath = default;
if (normalizeBuffer.Length != 0)
normalizeBuffer[0] = NullTerminator;
ReadOnlySpan<byte> currentPath = path;
if (hasMountName && path.At(0) == DirectorySeparator)
{
if (path.At(1) == AltDirectorySeparator && path.At(2) == AltDirectorySeparator)
{
if (normalizeBuffer.Length == 0)
return ResultFs.NotNormalized.Log();
currentPath = path.Slice(1);
}
else if (path.Length != 0 && WindowsPath12.IsWindowsDrive(path.Slice(1)))
{
if (normalizeBuffer.Length == 0)
return ResultFs.NotNormalized.Log();
currentPath = path.Slice(1);
}
}
if (WindowsPath12.IsWindowsDrive(currentPath))
{
int winPathLength;
for (winPathLength = 2; currentPath.At(winPathLength) != NullTerminator; winPathLength++)
{
if (currentPath[winPathLength] == DirectorySeparator ||
currentPath[winPathLength] == AltDirectorySeparator)
{
break;
}
}
if (normalizeBuffer.IsEmpty)
{
for (int i = 0; i < winPathLength; i++)
{
if (currentPath[i] == '\\')
return ResultFs.NotNormalized.Log();
}
}
if (!normalizeBuffer.IsEmpty)
{
if (winPathLength >= normalizeBuffer.Length)
return ResultFs.TooLongPath.Log();
currentPath.Slice(0, winPathLength).CopyTo(normalizeBuffer);
normalizeBuffer[winPathLength] = NullTerminator;
PathUtility12.Replace(normalizeBuffer.Slice(0, winPathLength), AltDirectorySeparator,
DirectorySeparator);
}
newPath = currentPath.Slice(winPathLength);
windowsPathLength = winPathLength;
return Result.Success;
}
if (WindowsPath12.IsDosDevicePath(currentPath))
{
int dosPathLength = WindowsPath12.GetDosDevicePathPrefixLength();
if (WindowsPath12.IsWindowsDrive(currentPath.Slice(dosPathLength)))
{
dosPathLength += 2;
}
else
{
dosPathLength--;
}
if (!normalizeBuffer.IsEmpty)
{
if (dosPathLength >= normalizeBuffer.Length)
return ResultFs.TooLongPath.Log();
currentPath.Slice(0, dosPathLength).CopyTo(normalizeBuffer);
normalizeBuffer[dosPathLength] = NullTerminator;
PathUtility12.Replace(normalizeBuffer.Slice(0, dosPathLength), DirectorySeparator,
AltDirectorySeparator);
}
newPath = currentPath.Slice(dosPathLength);
windowsPathLength = dosPathLength;
return Result.Success;
}
if (WindowsPath12.IsUncPath(currentPath, false, true))
{
Result rc;
ReadOnlySpan<byte> finalPath = currentPath;
if (currentPath.At(2) == DirectorySeparator || currentPath.At(2) == AltDirectorySeparator)
return ResultFs.InvalidPathFormat.Log();
int currentComponentOffset = 0;
int pos;
for (pos = 2; currentPath.At(pos) != NullTerminator; pos++)
{
if (currentPath.At(pos) == DirectorySeparator || currentPath.At(pos) == AltDirectorySeparator)
{
if (currentComponentOffset != 0)
{
rc = CheckSharedName(
currentPath.Slice(currentComponentOffset, pos - currentComponentOffset));
if (rc.IsFailure()) return rc;
finalPath = currentPath.Slice(pos);
break;
}
if (currentPath.At(pos + 1) == DirectorySeparator || currentPath.At(pos + 1) == AltDirectorySeparator)
return ResultFs.InvalidPathFormat.Log();
rc = CheckHostName(currentPath.Slice(2, pos - 2));
if (rc.IsFailure()) return rc;
currentComponentOffset = pos + 1;
}
}
if (currentComponentOffset == pos)
return ResultFs.InvalidPathFormat.Log();
if (currentComponentOffset != 0 && finalPath == currentPath)
{
rc = CheckSharedName(currentPath.Slice(currentComponentOffset, pos - currentComponentOffset));
if (rc.IsFailure()) return rc;
finalPath = currentPath.Slice(pos);
}
ref byte currentPathStart = ref MemoryMarshal.GetReference(currentPath);
ref byte finalPathStart = ref MemoryMarshal.GetReference(finalPath);
int uncPrefixLength = (int)Unsafe.ByteOffset(ref currentPathStart, ref finalPathStart);
if (normalizeBuffer.IsEmpty)
{
for (int i = 0; i < uncPrefixLength; i++)
{
if (currentPath[i] == DirectorySeparator)
return ResultFs.NotNormalized.Log();
}
}
if (!normalizeBuffer.IsEmpty)
{
if (uncPrefixLength >= normalizeBuffer.Length)
return ResultFs.TooLongPath.Log();
currentPath.Slice(0, uncPrefixLength).CopyTo(normalizeBuffer);
normalizeBuffer[uncPrefixLength] = NullTerminator;
PathUtility12.Replace(normalizeBuffer.Slice(0, uncPrefixLength), DirectorySeparator, AltDirectorySeparator);
}
newPath = finalPath;
windowsPathLength = uncPrefixLength;
return Result.Success;
}
newPath = path;
windowsPathLength = 0;
return Result.Success;
}
public static Result ParseWindowsPath(out ReadOnlySpan<byte> newPath, out int windowsPathLength,
Span<byte> normalizeBuffer, ReadOnlySpan<byte> path, bool hasMountName)
{
return ParseWindowsPathImpl(out newPath, out windowsPathLength, normalizeBuffer, path, hasMountName);
}
public static Result SkipWindowsPath(out ReadOnlySpan<byte> newPath, out int windowsPathLength,
out bool isNormalized, ReadOnlySpan<byte> path, bool hasMountName)
{
isNormalized = true;
Result rc = ParseWindowsPathImpl(out newPath, out windowsPathLength, Span<byte>.Empty, path, hasMountName);
if (!rc.IsSuccess())
{
if (ResultFs.NotNormalized.Includes(rc))
{
isNormalized = false;
}
else
{
return rc;
}
}
return Result.Success;
}
private static Result ParseRelativeDotPathImpl(out ReadOnlySpan<byte> newPath, out int length,
Span<byte> relativePathBuffer, ReadOnlySpan<byte> path)
{
Assert.SdkRequiresNotNull(path);
UnsafeHelpers.SkipParamInit(out length);
newPath = default;
if (relativePathBuffer.Length != 0)
relativePathBuffer[0] = NullTerminator;
if (path.At(0) == Dot && (path.At(1) == NullTerminator || path.At(1) == DirectorySeparator ||
path.At(1) == AltDirectorySeparator))
{
if (relativePathBuffer.Length >= 2)
{
relativePathBuffer[0] = Dot;
relativePathBuffer[1] = NullTerminator;
}
newPath = path.Slice(1);
length = 1;
return Result.Success;
}
if (path.At(0) == Dot && path.At(1) == Dot)
return ResultFs.DirectoryUnobtainable.Log();
newPath = path;
length = 0;
return Result.Success;
}
public static Result ParseRelativeDotPath(out ReadOnlySpan<byte> newPath, out int length,
Span<byte> relativePathBuffer, ReadOnlySpan<byte> path)
{
return ParseRelativeDotPathImpl(out newPath, out length, relativePathBuffer, path);
}
public static Result SkipRelativeDotPath(out ReadOnlySpan<byte> newPath, out int length,
ReadOnlySpan<byte> path)
{
return ParseRelativeDotPathImpl(out newPath, out length, Span<byte>.Empty, path);
}
public static Result IsNormalized(out bool isNormalized, out int normalizedLength, ReadOnlySpan<byte> path,
PathFlags flags)
{
UnsafeHelpers.SkipParamInit(out isNormalized, out normalizedLength);
Result rc = PathUtility12.CheckUtf8(path);
if (rc.IsFailure()) return rc;
ReadOnlySpan<byte> buffer = path;
int totalLength = 0;
if (path.At(0) == NullTerminator)
{
if (!flags.IsEmptyPathAllowed())
return ResultFs.InvalidPathFormat.Log();
isNormalized = true;
normalizedLength = 0;
return Result.Success;
}
if (path.At(0) != DirectorySeparator &&
!flags.IsWindowsPathAllowed() &&
!flags.IsRelativePathAllowed() &&
!flags.IsMountNameAllowed())
{
return ResultFs.InvalidPathFormat.Log();
}
if (WindowsPath12.IsWindowsPath(path, false) && !flags.IsWindowsPathAllowed())
return ResultFs.InvalidPathFormat.Log();
bool hasMountName = false;
rc = SkipMountName(out buffer, out int mountNameLength, buffer);
if (rc.IsFailure()) return rc;
if (mountNameLength != 0)
{
if (!flags.IsMountNameAllowed())
return ResultFs.InvalidPathFormat.Log();
totalLength += mountNameLength;
hasMountName = true;
}
if (buffer.At(0) != DirectorySeparator && !PathUtility12.IsPathStartWithCurrentDirectory(buffer) &&
!WindowsPath12.IsWindowsPath(buffer, false))
{
if (!flags.IsRelativePathAllowed() || !PathUtility12.CheckInvalidCharacter(buffer.At(0)).IsSuccess())
return ResultFs.InvalidPathFormat.Log();
isNormalized = false;
return Result.Success;
}
bool isRelativePath = false;
rc = SkipRelativeDotPath(out buffer, out int relativePathLength, buffer);
if (rc.IsFailure()) return rc;
if (relativePathLength != 0)
{
if (!flags.IsRelativePathAllowed())
return ResultFs.InvalidPathFormat.Log();
totalLength += relativePathLength;
if (buffer.At(0) == NullTerminator)
{
isNormalized = true;
normalizedLength = totalLength;
return Result.Success;
}
isRelativePath = true;
}
rc = SkipWindowsPath(out buffer, out int windowsPathLength, out bool isNormalizedWin, buffer, hasMountName);
if (rc.IsFailure()) return rc;
if (!isNormalizedWin)
{
if (!flags.IsWindowsPathAllowed())
return ResultFs.InvalidPathFormat.Log();
isNormalized = false;
return Result.Success;
}
if (windowsPathLength != 0)
{
if (!flags.IsWindowsPathAllowed())
return ResultFs.InvalidPathFormat.Log();
totalLength += windowsPathLength;
if (isRelativePath)
return ResultFs.InvalidPathFormat.Log();
if (buffer.At(0) == NullTerminator)
{
isNormalized = false;
return Result.Success;
}
for (int i = 0; i < buffer.Length; i++)
{
if (buffer[i] == AltDirectorySeparator)
{
isNormalized = false;
return Result.Success;
}
}
}
if (PathNormalizer12.IsParentDirectoryPathReplacementNeeded(buffer))
return ResultFs.DirectoryUnobtainable.Log();
rc = PathUtility12.CheckInvalidBackslash(out bool isBackslashContained, buffer,
flags.IsWindowsPathAllowed() || flags.IsBackslashAllowed());
if (rc.IsFailure()) return rc;
if (isBackslashContained && !flags.IsBackslashAllowed())
{
isNormalized = false;
return Result.Success;
}
rc = PathNormalizer12.IsNormalized(out isNormalized, out int length, buffer);
if (rc.IsFailure()) return rc;
totalLength += length;
normalizedLength = totalLength;
return Result.Success;
}
public static Result Normalize(Span<byte> outputBuffer, ReadOnlySpan<byte> path, PathFlags flags)
{
Result rc;
ReadOnlySpan<byte> src = path;
int currentPos = 0;
bool isWindowsPath = false;
if (path.At(0) == NullTerminator)
{
if (!flags.IsEmptyPathAllowed())
return ResultFs.InvalidPathFormat.Log();
if (outputBuffer.Length != 0)
outputBuffer[0] = NullTerminator;
return Result.Success;
}
bool hasMountName = false;
if (flags.IsMountNameAllowed())
{
rc = ParseMountName(out src, out int mountNameLength, outputBuffer.Slice(currentPos), src);
if (rc.IsFailure()) return rc;
currentPos += mountNameLength;
hasMountName = mountNameLength != 0;
}
bool isDriveRelative = false;
if (src.At(0) != DirectorySeparator && !PathUtility12.IsPathStartWithCurrentDirectory(src) &&
!WindowsPath12.IsWindowsPath(src, false))
{
if (!flags.IsRelativePathAllowed() || !PathUtility12.CheckInvalidCharacter(src.At(0)).IsSuccess())
return ResultFs.InvalidPathFormat.Log();
outputBuffer[currentPos++] = Dot;
isDriveRelative = true;
}
if (flags.IsRelativePathAllowed())
{
if (currentPos >= outputBuffer.Length)
return ResultFs.TooLongPath.Log();
rc = ParseRelativeDotPath(out src, out int relativePathLength, outputBuffer.Slice(currentPos), src);
if (rc.IsFailure()) return rc;
currentPos += relativePathLength;
if (src.At(0) == NullTerminator)
{
if (currentPos >= outputBuffer.Length)
return ResultFs.TooLongPath.Log();
outputBuffer[currentPos] = NullTerminator;
return Result.Success;
}
}
if (flags.IsWindowsPathAllowed())
{
ReadOnlySpan<byte> originalPath = src;
if (currentPos >= outputBuffer.Length)
return ResultFs.TooLongPath.Log();
rc = ParseWindowsPath(out src, out int windowsPathLength, outputBuffer.Slice(currentPos), src,
hasMountName);
if (rc.IsFailure()) return rc;
currentPos += windowsPathLength;
if (src.At(0) == NullTerminator)
{
// Note: Bug is in the original code. Should be "currentPos + 2"
if (currentPos + 1 >= outputBuffer.Length)
return ResultFs.TooLongPath.Log();
outputBuffer[currentPos] = DirectorySeparator;
outputBuffer[currentPos + 1] = NullTerminator;
return Result.Success;
}
int skippedLength = (int)Unsafe.ByteOffset(ref MemoryMarshal.GetReference(originalPath),
ref MemoryMarshal.GetReference(src));
if (skippedLength > 0)
isWindowsPath = true;
}
rc = PathUtility12.CheckInvalidBackslash(out bool isBackslashContained, src,
flags.IsWindowsPathAllowed() || flags.IsBackslashAllowed());
if (rc.IsFailure()) return rc;
byte[] srcBufferSlashReplaced = null;
try
{
if (isBackslashContained && flags.IsWindowsPathAllowed())
{
srcBufferSlashReplaced = ArrayPool<byte>.Shared.Rent(path.Length);
StringUtils.Copy(srcBufferSlashReplaced, path);
PathUtility12.Replace(srcBufferSlashReplaced, AltDirectorySeparator, DirectorySeparator);
int srcOffset = (int)Unsafe.ByteOffset(ref MemoryMarshal.GetReference(path),
ref MemoryMarshal.GetReference(src));
src = srcBufferSlashReplaced.AsSpan(srcOffset);
}
rc = PathNormalizer12.Normalize(outputBuffer.Slice(currentPos), out _, src, isWindowsPath, isDriveRelative);
if (rc.IsFailure()) return rc;
return Result.Success;
}
finally
{
if (srcBufferSlashReplaced is not null)
{
ArrayPool<byte>.Shared.Return(srcBufferSlashReplaced);
}
}
}
public static Result CheckPathFormat(ReadOnlySpan<byte> path, PathFlags flags)
{
return Result.Success;
}
}
}

View file

@ -0,0 +1,367 @@
using System;
using LibHac.Common;
using LibHac.Diag;
using LibHac.FsSystem;
using static LibHac.Fs.Common.PathUtility12;
using static LibHac.Fs.StringTraits;
namespace LibHac.Fs.Common
{
public static class PathNormalizer12
{
private enum PathState
{
Initial,
Normal,
FirstSeparator,
Separator,
CurrentDir,
ParentDir
}
public static Result Normalize(Span<byte> outputBuffer, out int length, ReadOnlySpan<byte> path, bool isWindowsPath,
bool isDriveRelativePath)
{
UnsafeHelpers.SkipParamInit(out length);
ReadOnlySpan<byte> currentPath = path;
int totalLength = 0;
int i = 0;
if (!IsSeparator(path.At(0)))
{
if (!isDriveRelativePath)
return ResultFs.InvalidPathFormat.Log();
outputBuffer[totalLength++] = DirectorySeparator;
}
var convertedPath = new RentedArray<byte>();
try
{
// Check if parent directory path replacement is needed.
if (IsParentDirectoryPathReplacementNeeded(currentPath))
{
// Allocate a buffer to hold the replacement path.
convertedPath = new RentedArray<byte>(PathTools.MaxPathLength + 1);
// Replace the path.
ReplaceParentDirectoryPath(convertedPath.Span, currentPath);
// Set current path to be the replacement path.
currentPath = new U8Span(convertedPath.Span);
}
bool skipNextSeparator = false;
while (!IsNul(currentPath.At(i)))
{
if (IsSeparator(currentPath[i]))
{
do
{
i++;
} while (IsSeparator(currentPath.At(i)));
if (IsNul(currentPath.At(i)))
break;
if (!skipNextSeparator)
{
if (totalLength + 1 == outputBuffer.Length)
{
outputBuffer[totalLength] = NullTerminator;
length = totalLength;
return ResultFs.TooLongPath.Log();
}
outputBuffer[totalLength++] = DirectorySeparator;
}
skipNextSeparator = false;
}
int dirLen = 0;
while (!IsSeparator(currentPath.At(i + dirLen)) && !IsNul(currentPath.At(i + dirLen)))
{
dirLen++;
}
if (IsCurrentDirectory(currentPath.Slice(i)))
{
skipNextSeparator = true;
}
else if (IsParentDirectory(currentPath.Slice(i)))
{
Assert.SdkAssert(outputBuffer[totalLength - 1] == DirectorySeparator);
if (!isWindowsPath)
Assert.SdkAssert(outputBuffer[0] == DirectorySeparator);
if (totalLength == 1)
{
if (!isWindowsPath)
return ResultFs.DirectoryUnobtainable.Log();
totalLength--;
}
else
{
totalLength -= 2;
do
{
if (outputBuffer[totalLength] == DirectorySeparator)
break;
totalLength--;
} while (totalLength != 0);
}
if (!isWindowsPath)
Assert.SdkAssert(outputBuffer[totalLength] == DirectorySeparator);
Assert.SdkAssert(totalLength < outputBuffer.Length);
}
else
{
if (totalLength + dirLen + 1 > outputBuffer.Length)
{
int copyLen = outputBuffer.Length - 1 - totalLength;
for (int j = 0; j < copyLen; j++)
{
outputBuffer[totalLength++] = currentPath[i + j];
}
outputBuffer[totalLength] = NullTerminator;
length = totalLength;
return ResultFs.TooLongPath.Log();
}
for (int j = 0; j < dirLen; j++)
{
outputBuffer[totalLength++] = currentPath[i + j];
}
}
i += dirLen;
}
if (skipNextSeparator)
totalLength--;
if (totalLength == 0 && outputBuffer.Length != 0)
{
totalLength = 1;
outputBuffer[0] = DirectorySeparator;
}
// Note: This bug is in the original code. They probably meant to put "totalLength + 1"
if (totalLength - 1 > outputBuffer.Length)
return ResultFs.TooLongPath.Log();
outputBuffer[totalLength] = NullTerminator;
Result rc = IsNormalized(out bool isNormalized, out _, outputBuffer);
if (rc.IsFailure()) return rc;
Assert.SdkAssert(isNormalized);
length = totalLength;
return Result.Success;
}
finally
{
convertedPath.Dispose();
}
}
/// <summary>
/// Checks if a given path is normalized. Path must be a basic path, starting with a directory separator
/// and not containing any sort of prefix such as a mount name.
/// </summary>
/// <param name="isNormalized">When this function returns <see cref="Result.Success"/>,
/// contains <see langword="true"/> if the path is normalized or <see langword="false"/> if it is not.
/// Contents are undefined if the function does not return <see cref="Result.Success"/>.
/// </param>
/// <param name="length">When this function returns <see cref="Result.Success"/> and
/// <paramref name="isNormalized"/> is <see langword="true"/>, contains the length of the normalized path.
/// Contents are undefined if the function does not return <see cref="Result.Success"/>
/// or <paramref name="isNormalized"/> is <see langword="false"/>.
/// </param>
/// <param name="path">The path to check.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.InvalidCharacter"/>: The path contains an invalid character.<br/>
/// <see cref="ResultFs.InvalidPathFormat"/>: The path is not in a valid format.</returns>
public static Result IsNormalized(out bool isNormalized, out int length, ReadOnlySpan<byte> path)
{
UnsafeHelpers.SkipParamInit(out isNormalized, out length);
var state = PathState.Initial;
int pathLength = 0;
for (int i = 0; i < path.Length; i++)
{
byte c = path[i];
if (c == NullTerminator) break;
pathLength++;
if (state != PathState.Initial)
{
Result rc = CheckInvalidCharacter(c);
if (rc.IsFailure()) return rc;
}
switch (state)
{
case PathState.Initial:
if (c != DirectorySeparator)
return ResultFs.InvalidPathFormat.Log();
state = PathState.FirstSeparator;
break;
case PathState.Normal:
if (c == DirectorySeparator)
state = PathState.Separator;
break;
case PathState.FirstSeparator:
case PathState.Separator:
if (c == DirectorySeparator)
{
isNormalized = false;
return Result.Success;
}
state = c == Dot ? PathState.CurrentDir : PathState.Normal;
break;
case PathState.CurrentDir:
if (c == DirectorySeparator)
{
isNormalized = false;
return Result.Success;
}
state = c == Dot ? PathState.ParentDir : PathState.Normal;
break;
case PathState.ParentDir:
if (c == DirectorySeparator)
{
isNormalized = false;
return Result.Success;
}
state = PathState.Normal;
break;
// ReSharper disable once UnreachableSwitchCaseDueToIntegerAnalysis
default:
Abort.UnexpectedDefault();
break;
}
}
switch (state)
{
case PathState.Initial:
return ResultFs.InvalidPathFormat.Log();
case PathState.Normal:
case PathState.FirstSeparator:
isNormalized = true;
break;
case PathState.Separator:
case PathState.CurrentDir:
case PathState.ParentDir:
isNormalized = false;
break;
// ReSharper disable once UnreachableSwitchCaseDueToIntegerAnalysis
default:
Abort.UnexpectedDefault();
break;
}
length = pathLength;
return Result.Success;
}
/// <summary>
/// Checks if a path begins with / or \ and contains any of these patterns:
/// "/..\", "\..\", "\../", "\..0" where '0' is the null terminator.
/// </summary>
public static bool IsParentDirectoryPathReplacementNeeded(ReadOnlySpan<byte> path)
{
if (path.Length == 0 || (path[0] != DirectorySeparator && path[0] != AltDirectorySeparator))
return false;
for (int i = 0; i < path.Length - 2 && path[i] != NullTerminator; i++)
{
byte c3 = path.At(i + 3);
if (path[i] == AltDirectorySeparator &&
path[i + 1] == Dot &&
path[i + 2] == Dot &&
(c3 == DirectorySeparator || c3 == AltDirectorySeparator || c3 == NullTerminator))
{
return true;
}
if ((path[i] == DirectorySeparator || path[i] == AltDirectorySeparator) &&
path[i + 1] == Dot &&
path[i + 2] == Dot &&
c3 == AltDirectorySeparator)
{
return true;
}
}
return false;
}
private static void ReplaceParentDirectoryPath(Span<byte> dest, ReadOnlySpan<byte> source)
{
dest[0] = DirectorySeparator;
int i = 1;
while (source.Length > i && source[i] != NullTerminator)
{
if (source.Length > i + 2 &&
(source[i - 1] == DirectorySeparator || source[i - 1] == AltDirectorySeparator) &&
source[i + 0] == Dot &&
source[i + 1] == Dot &&
(source[i + 2] == DirectorySeparator || source[i + 2] == AltDirectorySeparator))
{
dest[i - 1] = DirectorySeparator;
dest[i + 0] = Dot;
dest[i + 1] = Dot;
dest[i + 2] = DirectorySeparator;
i += 3;
}
else
{
if (source.Length > i + 1 &&
source[i - 1] == AltDirectorySeparator &&
source[i + 0] == Dot &&
source[i + 1] == Dot &&
(source.Length == i + 2 || source[i + 2] == NullTerminator))
{
dest[i - 1] = DirectorySeparator;
dest[i + 0] = Dot;
dest[i + 1] = Dot;
i += 2;
break;
}
dest[i] = source[i];
i++;
}
}
dest[i] = NullTerminator;
}
}
}

View file

@ -0,0 +1,245 @@
using System;
using System.Runtime.CompilerServices;
using LibHac.Common;
using LibHac.Diag;
using LibHac.FsSrv.Sf;
using LibHac.Util;
using static LibHac.Fs.StringTraits;
namespace LibHac.Fs.Common
{
public static class PathUtility12
{
public static void Replace(Span<byte> buffer, byte currentChar, byte newChar)
{
Assert.SdkRequiresNotNull(buffer);
for (int i = 0; i < buffer.Length; i++)
{
if (buffer[i] == currentChar)
{
buffer[i] = newChar;
}
}
}
public static bool IsCurrentDirectory(ReadOnlySpan<byte> path)
{
if (path.Length < 1)
return false;
return path[0] == Dot &&
(path.Length < 2 || path[1] == NullTerminator || path[1] == DirectorySeparator);
}
public static bool IsParentDirectory(ReadOnlySpan<byte> path)
{
if (path.Length < 2)
return false;
return path[0] == Dot &&
path[1] == Dot &&
(path.Length < 3 || path[2] == NullTerminator || path[2] == DirectorySeparator);
}
public static bool IsSeparator(byte c)
{
return c == DirectorySeparator;
}
public static bool IsNul(byte c)
{
return c == NullTerminator;
}
public static Result ConvertToFspPath(out FspPath fspPath, ReadOnlySpan<byte> path)
{
UnsafeHelpers.SkipParamInit(out fspPath);
int length = StringUtils.Copy(SpanHelpers.AsByteSpan(ref fspPath), path, PathTool.EntryNameLengthMax + 1);
if (length >= PathTool.EntryNameLengthMax + 1)
return ResultFs.TooLongPath.Log();
Result rc = PathFormatter.SkipMountName(out ReadOnlySpan<byte> pathWithoutMountName, out _,
new U8Span(path));
if (rc.IsFailure()) return rc;
if (!WindowsPath12.IsWindowsPath(pathWithoutMountName, true))
{
Replace(SpanHelpers.AsByteSpan(ref fspPath).Slice(0, 0x300), AltDirectorySeparator, DirectorySeparator);
}
else if (fspPath.Str[0] == DirectorySeparator && fspPath.Str[1] == DirectorySeparator)
{
SpanHelpers.AsByteSpan(ref fspPath)[0] = AltDirectorySeparator;
SpanHelpers.AsByteSpan(ref fspPath)[1] = AltDirectorySeparator;
}
return Result.Success;
}
public static bool IsDirectoryPath(ReadOnlySpan<byte> path)
{
if (path.Length < 1 || path[0] == NullTerminator)
return false;
int length = StringUtils.GetLength(path);
return path[length - 1] == DirectorySeparator || path[length - 1] == AltDirectorySeparator;
}
public static bool IsDirectoryPath(in FspPath path)
{
return IsDirectoryPath(SpanHelpers.AsReadOnlyByteSpan(in path));
}
public static Result CheckUtf8(ReadOnlySpan<byte> path)
{
Assert.SdkRequiresNotNull(path);
uint utf8Buffer = 0;
Span<byte> utf8BufferSpan = SpanHelpers.AsByteSpan(ref utf8Buffer);
ReadOnlySpan<byte> currentChar = path;
while (currentChar.Length > 0 && currentChar[0] != NullTerminator)
{
utf8BufferSpan.Clear();
CharacterEncodingResult result =
CharacterEncoding.PickOutCharacterFromUtf8String(utf8BufferSpan, ref currentChar);
if (result != CharacterEncodingResult.Success)
return ResultFs.InvalidPathFormat.Log();
result = CharacterEncoding.ConvertCharacterUtf8ToUtf32(out _, utf8BufferSpan);
if (result != CharacterEncodingResult.Success)
return ResultFs.InvalidPathFormat.Log();
}
return Result.Success;
}
public static Result CheckInvalidCharacter(byte c)
{
/*
The optimized code is equivalent to this:
ReadOnlySpan<byte> invalidChars = new[]
{(byte) ':', (byte) '*', (byte) '?', (byte) '<', (byte) '>', (byte) '|'};
for (int i = 0; i < invalidChars.Length; i++)
{
if (c == invalidChars[i])
return ResultFs.InvalidCharacter.Log();
}
return Result.Success;
*/
const ulong mask = (1ul << (byte)':') |
(1ul << (byte)'*') |
(1ul << (byte)'?') |
(1ul << (byte)'<') |
(1ul << (byte)'>');
if (c <= 0x3Fu && ((1ul << c) & mask) != 0 || c == (byte)'|')
return ResultFs.InvalidCharacter.Log();
return Result.Success;
}
public static Result CheckInvalidBackslash(out bool containsBackslash, ReadOnlySpan<byte> path, bool allowBackslash)
{
containsBackslash = false;
for (int i = 0; i < path.Length && path[i] != NullTerminator; i++)
{
if (path[i] == '\\')
{
containsBackslash = true;
if (!allowBackslash)
return ResultFs.InvalidCharacter.Log();
}
}
return Result.Success;
}
public static Result CheckEntryNameBytes(ReadOnlySpan<byte> path, int maxEntryLength)
{
Assert.SdkRequiresNotNull(path);
int currentEntryLength = 0;
for (int i = 0; i < path.Length && path[i] != NullTerminator; i++)
{
currentEntryLength++;
if (path[i] == DirectorySeparator || path[i] == AltDirectorySeparator)
currentEntryLength = 0;
// Note: The original does use >= instead of >
if (currentEntryLength >= maxEntryLength)
return ResultFs.TooLongPath.Log();
}
return Result.Success;
}
public static bool IsSubPath(ReadOnlySpan<byte> lhs, ReadOnlySpan<byte> rhs)
{
Assert.SdkRequiresNotNull(lhs);
Assert.SdkRequiresNotNull(rhs);
if (WindowsPath12.IsUncPath(lhs) && !WindowsPath12.IsUncPath(rhs))
return false;
if (!WindowsPath12.IsUncPath(lhs) && WindowsPath12.IsUncPath(rhs))
return false;
if (lhs.At(0) == DirectorySeparator && lhs.At(1) == NullTerminator &&
rhs.At(0) == DirectorySeparator && rhs.At(1) != NullTerminator)
return true;
if (rhs.At(0) == DirectorySeparator && rhs.At(1) == NullTerminator &&
lhs.At(0) == DirectorySeparator && lhs.At(1) != NullTerminator)
return true;
for (int i = 0; ; i++)
{
if (lhs.At(i) == NullTerminator)
{
return rhs.At(i) == DirectorySeparator;
}
else if (rhs.At(i) == NullTerminator)
{
return lhs.At(i) == DirectorySeparator;
}
else if (lhs.At(i) != rhs.At(i))
{
return false;
}
}
}
public static bool IsPathAbsolute(ReadOnlySpan<byte> path)
{
if (WindowsPath12.IsWindowsPath(path, false))
return true;
return path.At(0) == DirectorySeparator;
}
public static bool IsPathRelative(ReadOnlySpan<byte> path)
{
return path.At(0) != NullTerminator && !IsPathAbsolute(path);
}
public static bool IsPathStartWithCurrentDirectory(ReadOnlySpan<byte> path)
{
return IsCurrentDirectory(path) || IsParentDirectory(path);
}
}
}

View file

@ -0,0 +1,249 @@
using System;
using LibHac.Common;
using LibHac.Diag;
using LibHac.Util;
using static LibHac.Util.CharacterEncoding;
namespace LibHac.Fs.Common
{
public static class WindowsPath12
{
public static int GetCodePointByteLength(byte firstCodeUnit)
{
if ((firstCodeUnit & 0x80) == 0x00) return 1;
if ((firstCodeUnit & 0xE0) == 0xC0) return 2;
if ((firstCodeUnit & 0xF0) == 0xE0) return 3;
if ((firstCodeUnit & 0xF8) == 0xF0) return 4;
return 0;
}
private static bool IsUncPathImpl(ReadOnlySpan<byte> path, bool checkForwardSlash, bool checkBackSlash)
{
Assert.SdkRequiresNotNull(path);
if ((uint)path.Length < 2)
return false;
if (checkForwardSlash && path[0] == '/' && path[1] == '/')
return true;
return checkBackSlash && path[0] == '\\' && path[1] == '\\';
}
private static bool IsUncPathImpl(ReadOnlySpan<char> path, bool checkForwardSlash, bool checkBackSlash)
{
Assert.SdkRequiresNotNull(path);
if ((uint)path.Length < 2)
return false;
if (checkForwardSlash && path[0] == '/' && path[1] == '/')
return true;
return checkBackSlash && path[0] == '\\' && path[1] == '\\';
}
private static int GetUncPathPrefixLengthImpl(ReadOnlySpan<byte> path, bool checkForwardSlash)
{
Assert.SdkRequiresNotNull(path);
int length;
int separatorCount = 0;
for (length = 0; length < path.Length && path[length] != 0; length++)
{
if (checkForwardSlash && path[length] == '/')
++separatorCount;
if (path[length] == '\\')
++separatorCount;
if (separatorCount == 4)
return length;
}
return length;
}
private static int GetUncPathPrefixLengthImpl(ReadOnlySpan<char> path, bool checkForwardSlash)
{
Assert.SdkRequiresNotNull(path);
int length;
int separatorCount = 0;
for (length = 0; length < path.Length && path[length] != 0; length++)
{
if (checkForwardSlash && path[length] == '/')
++separatorCount;
if (path[length] == '\\')
++separatorCount;
if (separatorCount == 4)
return length;
}
return length;
}
private static bool IsDosDevicePathImpl(ReadOnlySpan<byte> path)
{
Assert.SdkRequiresNotNull(path);
if ((uint)path.Length < 4)
return false;
return path[0] == '\\' &&
path[1] == '\\' &&
(path[2] == '.' || path[2] == '?') &&
(path[3] == '/' || path[3] == '\\');
}
private static bool IsDosDevicePathImpl(ReadOnlySpan<char> path)
{
Assert.SdkRequiresNotNull(path);
if ((uint)path.Length < 4)
return false;
return path[0] == '\\' &&
path[1] == '\\' &&
(path[2] == '.' || path[2] == '?') &&
(path[3] == '/' || path[3] == '\\');
}
public static bool IsWindowsDrive(ReadOnlySpan<byte> path)
{
Assert.SdkRequiresNotNull(path);
if ((uint)path.Length < 2)
return false;
// Mask lowercase letters to uppercase and check if it's in range
return ((0b1101_1111 & path[0]) - 'A' <= 'Z' - 'A') && path[1] == ':';
// return ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') && path[1] == ':';
}
public static bool IsUncPath(ReadOnlySpan<byte> path)
{
return IsUncPathImpl(path, true, true);
}
public static bool IsUncPath(ReadOnlySpan<byte> path, bool checkForwardSlash, bool checkBackSlash)
{
return IsUncPathImpl(path, checkForwardSlash, checkBackSlash);
}
public static int GetUncPathPrefixLength(ReadOnlySpan<byte> path)
{
return GetUncPathPrefixLengthImpl(path, true);
}
public static bool IsDosDevicePath(ReadOnlySpan<byte> path)
{
return IsDosDevicePathImpl(path);
}
public static int GetDosDevicePathPrefixLength()
{
return 4;
}
public static bool IsWindowsPath(ReadOnlySpan<byte> path, bool checkForwardSlash)
{
return IsWindowsDrive(path) || IsDosDevicePath(path) || IsUncPath(path, checkForwardSlash, true);
}
public static int GetWindowsSkipLength(ReadOnlySpan<byte> path)
{
if (IsDosDevicePath(path))
return GetDosDevicePathPrefixLength();
if (IsWindowsDrive(path))
return 2;
if (IsUncPath(path))
return GetUncPathPrefixLength(path);
return 0;
}
public static bool IsDosDelimiterW(char c)
{
return c == '/' || c == '\\';
}
public static bool IsWindowsDriveW(ReadOnlySpan<char> path)
{
Assert.SdkRequiresNotNull(path);
if ((uint)path.Length < 2)
return false;
// Mask lowercase letters to uppercase and check if it's in range
return ((0b1101_1111 & path[0]) - 'A' <= 'Z' - 'A') && path[1] == ':';
// return ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') && path[1] == ':';
}
public static bool IsUncPathW(ReadOnlySpan<char> path)
{
return IsUncPathImpl(path, true, true);
}
public static int GetUncPathPrefixLengthW(ReadOnlySpan<char> path)
{
return GetUncPathPrefixLengthImpl(path, true);
}
public static bool IsDosDevicePathW(ReadOnlySpan<char> path)
{
return IsDosDevicePathImpl(path);
}
public static bool IsWindowsPathW(ReadOnlySpan<char> path)
{
return IsWindowsDriveW(path) || IsUncPathW(path) || IsDosDevicePathW(path);
}
public static Result CheckCharacterCountForWindows(ReadOnlySpan<byte> path, int maxNameLength, int maxPathLength)
{
Assert.SdkRequiresNotNull(path);
ReadOnlySpan<byte> currentChar = path;
int currentNameLength = 0;
int currentPathLength = 0;
while (currentChar.Length > 1 && currentChar[0] != 0)
{
int utf16CodeUnitCount = GetCodePointByteLength(currentChar[0]) < 4 ? 1 : 2;
int utf8Buffer = 0;
CharacterEncodingResult result =
PickOutCharacterFromUtf8String(SpanHelpers.AsByteSpan(ref utf8Buffer), ref currentChar);
if (result != CharacterEncodingResult.Success)
return ResultFs.InvalidPathFormat.Log();
result = ConvertCharacterUtf8ToUtf32(out uint pathChar, SpanHelpers.AsReadOnlyByteSpan(in utf8Buffer));
if (result != CharacterEncodingResult.Success)
return ResultFs.InvalidPathFormat.Log();
currentNameLength += utf16CodeUnitCount;
currentPathLength += utf16CodeUnitCount;
if (pathChar == '/' || pathChar == '\\')
currentNameLength = 0;
if (maxNameLength > 0 && currentNameLength > maxNameLength)
return ResultFs.TooLongPath.Log();
if (maxPathLength > 0 && currentPathLength > maxPathLength)
return ResultFs.TooLongPath.Log();
}
return Result.Success;
}
}
}

View file

@ -0,0 +1,396 @@
// ReSharper disable InconsistentNaming
using System;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Common;
using LibHac.Util;
using Xunit;
namespace LibHac.Tests.Fs
{
public class PathFormatterTests
{
public static TheoryData<string, string, string, Result> TestData_Normalize_EmptyPath => new()
{
{ @"", "", @"", ResultFs.InvalidPathFormat.Value },
{ @"", "E", @"", Result.Success },
{ @"/aa/bb/../cc", "E", @"/aa/cc", Result.Success }
};
[Theory, MemberData(nameof(TestData_Normalize_EmptyPath))]
public static void Normalize_EmptyPath(string path, string pathFlags, string expectedNormalized, Result expectedResult)
{
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
}
public static TheoryData<string, string, string, Result> TestData_Normalize_MountName => new()
{
{ @"mount:/aa/bb", "", @"", ResultFs.InvalidPathFormat.Value },
{ @"mount:/aa/bb", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"mount:/aa/bb", "M", @"mount:/aa/bb", Result.Success },
{ @"mount:/aa/./bb", "M", @"mount:/aa/bb", Result.Success },
{ @"mount:\aa\bb", "M", @"mount:", ResultFs.InvalidPathFormat.Value },
{ @"m:/aa/bb", "M", @"", ResultFs.InvalidPathFormat.Value },
{ @"mo>unt:/aa/bb", "M", @"", ResultFs.InvalidCharacter.Value },
{ @"moun?t:/aa/bb", "M", @"", ResultFs.InvalidCharacter.Value },
{ @"mo&unt:/aa/bb", "M", @"mo&unt:/aa/bb", Result.Success },
{ @"/aa/./bb", "M", @"/aa/bb", Result.Success },
{ @"mount/aa/./bb", "M", @"", ResultFs.InvalidPathFormat.Value }
};
[Theory, MemberData(nameof(TestData_Normalize_MountName))]
public static void Normalize_MountName(string path, string pathFlags, string expectedNormalized, Result expectedResult)
{
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
}
public static TheoryData<string, string, string, Result> TestData_Normalize_WindowsPath => new()
{
{ @"c:/aa/bb", "", @"", ResultFs.InvalidPathFormat.Value },
{ @"c:\aa\bb", "", @"", ResultFs.InvalidCharacter.Value },
{ @"\\host\share", "", @"", ResultFs.InvalidCharacter.Value },
{ @"\\.\c:\", "", @"", ResultFs.InvalidCharacter.Value },
{ @"\\.\c:/aa/bb/.", "", @"", ResultFs.InvalidCharacter.Value },
{ @"\\?\c:\", "", @"", ResultFs.InvalidCharacter.Value },
{ @"mount:\\host\share\aa\bb", "M", @"mount:", ResultFs.InvalidCharacter.Value },
{ @"mount:\\host/share\aa\bb", "M", @"mount:", ResultFs.InvalidCharacter.Value },
{ @"mount:/\\aa\..\bb", "MW", @"mount:", ResultFs.InvalidPathFormat.Value },
{ @"mount:/c:\aa\..\bb", "MW", @"mount:c:/bb", Result.Success },
{ @"mount:/aa/bb", "MW", @"mount:/aa/bb", Result.Success },
{ @"/mount:/aa/bb", "MW", @"/mount:/aa/bb", ResultFs.InvalidCharacter.Value },
{ @"/mount:/aa/bb", "W", @"/mount:/aa/bb", ResultFs.InvalidCharacter.Value },
{ @"a:aa/../bb", "MW", @"a:aa/bb", Result.Success },
{ @"a:aa\..\bb", "MW", @"a:aa/bb", Result.Success },
{ @"/a:aa\..\bb", "W", @"/bb", Result.Success },
{ @"\\?\c:\.\aa", "W", @"\\?\c:/aa", Result.Success },
{ @"\\.\c:\.\aa", "W", @"\\.\c:/aa", Result.Success },
{ @"\\.\mount:\.\aa", "W", @"\\./mount:/aa", ResultFs.InvalidCharacter.Value },
{ @"\\./.\aa", "W", @"\\./aa", Result.Success },
{ @"\\/aa", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\\aa", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\", "W", @"/", Result.Success },
{ @"\\host\share", "W", @"\\host\share/", Result.Success },
{ @"\\host\share\path", "W", @"\\host\share/path", Result.Success },
{ @"\\host\share\path\aa\bb\..\cc\.", "W", @"\\host\share/path/aa/cc", Result.Success },
{ @"\\host\", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\ho$st\share\path", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\host:\share\path", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\..\share\path", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\host\s:hare\path", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\host\.\path", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\host\..\path", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\host\sha:re", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @".\\host\share", "RW", @"..\\host\share/", Result.Success }
};
[Theory, MemberData(nameof(TestData_Normalize_WindowsPath))]
public static void Normalize_WindowsPath(string path, string pathFlags, string expectedNormalized, Result expectedResult)
{
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
}
public static TheoryData<string, string, string, Result> TestData_Normalize_RelativePath => new()
{
{ @"./aa/bb", "", @"", ResultFs.InvalidPathFormat.Value },
{ @"./aa/bb/../cc", "R", @"./aa/cc", Result.Success },
{ @".\aa/bb/../cc", "R", @"..", ResultFs.InvalidCharacter.Value },
{ @".", "R", @".", Result.Success },
{ @"../aa/bb", "R", @"", ResultFs.DirectoryUnobtainable.Value },
{ @"/aa/./bb", "R", @"/aa/bb", Result.Success },
{ @"mount:./aa/bb", "MR", @"mount:./aa/bb", Result.Success },
{ @"mount:./aa/./bb", "MR", @"mount:./aa/bb", Result.Success },
{ @"mount:./aa/bb", "M", @"mount:", ResultFs.InvalidPathFormat.Value }
};
[Theory, MemberData(nameof(TestData_Normalize_RelativePath))]
public static void Normalize_RelativePath(string path, string pathFlags, string expectedNormalized, Result expectedResult)
{
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
}
public static TheoryData<string, string, string, Result> TestData_Normalize_Backslash => new()
{
{ @"\aa\bb\..\cc", "", @"", ResultFs.InvalidPathFormat.Value },
{ @"\aa\bb\..\cc", "B", @"", ResultFs.InvalidPathFormat.Value },
{ @"/aa\bb\..\cc", "", @"", ResultFs.InvalidCharacter.Value },
{ @"/aa\bb\..\cc", "B", @"/cc", Result.Success },
{ @"/aa\bb\cc", "", @"", ResultFs.InvalidCharacter.Value },
{ @"/aa\bb\cc", "B", @"/aa\bb\cc", Result.Success },
{ @"\\host\share\path\aa\bb\cc", "W", @"\\host\share/path/aa/bb/cc", Result.Success },
{ @"\\host\share\path\aa\bb\cc", "WB", @"\\host\share/path/aa/bb/cc", Result.Success },
{ @"/aa/bb\../cc/..\dd\..\ee/..", "", @"", ResultFs.InvalidCharacter.Value },
{ @"/aa/bb\../cc/..\dd\..\ee/..", "B", @"/aa", Result.Success }
};
[Theory, MemberData(nameof(TestData_Normalize_Backslash))]
public static void Normalize_Backslash(string path, string pathFlags, string expectedNormalized, Result expectedResult)
{
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
}
public static TheoryData<string, string, string, Result> TestData_Normalize_All => new()
{
{ @"mount:./aa/bb", "WRM", @"mount:./aa/bb", Result.Success },
{ @"mount:./aa/bb\cc/dd", "WRM", @"mount:./aa/bb/cc/dd", Result.Success },
{ @"mount:./aa/bb\cc/dd", "WRMB", @"mount:./aa/bb/cc/dd", Result.Success },
{ @"mount:./.c:/aa/bb", "RM", @"mount:./.c:/aa/bb", ResultFs.InvalidCharacter.Value },
{ @"mount:.c:/aa/bb", "WRM", @"mount:./.c:/aa/bb", ResultFs.InvalidCharacter.Value },
{ @"mount:./cc:/aa/bb", "WRM", @"mount:./cc:/aa/bb", ResultFs.InvalidCharacter.Value },
{ @"mount:./\\host\share/aa/bb", "MW", @"mount:", ResultFs.InvalidPathFormat.Value },
{ @"mount:./\\host\share/aa/bb", "WRM", @"mount:.\\host\share/aa/bb", Result.Success },
{ @"mount:.\\host\share/aa/bb", "WRM", @"mount:..\\host\share/aa/bb", Result.Success },
{ @"mount:..\\host\share/aa/bb", "WRM", @"mount:.", ResultFs.DirectoryUnobtainable.Value },
{ @".\\host\share/aa/bb", "WRM", @"..\\host\share/aa/bb", Result.Success },
{ @"..\\host\share/aa/bb", "WRM", @".", ResultFs.DirectoryUnobtainable.Value },
{ @"mount:\\host\share/aa/bb", "MW", @"mount:\\host\share/aa/bb", Result.Success },
{ @"mount:\aa\bb", "BM", @"mount:", ResultFs.InvalidPathFormat.Value },
{ @"mount:/aa\bb", "BM", @"mount:/aa\bb", Result.Success },
{ @".//aa/bb", "RW", @"./aa/bb", Result.Success },
{ @"./aa/bb", "R", @"./aa/bb", Result.Success },
{ @"./c:/aa/bb", "RW", @"./c:/aa/bb", ResultFs.InvalidCharacter.Value }
};
[Theory, MemberData(nameof(TestData_Normalize_All))]
public static void Normalize_All(string path, string pathFlags, string expectedNormalized, Result expectedResult)
{
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
}
public static TheoryData<string, string, int, string, Result> TestData_Normalize_SmallBuffer => new()
{
{ @"/aa/bb", "M", 1, @"", ResultFs.TooLongPath.Value },
{ @"mount:/aa/bb", "MR", 6, @"", ResultFs.TooLongPath.Value },
{ @"mount:/aa/bb", "MR", 7, @"mount:", ResultFs.TooLongPath.Value },
{ @"aa/bb", "MR", 3, @"./", ResultFs.TooLongPath.Value },
{ @"\\host\share", "W", 13, @"\\host\share", ResultFs.TooLongPath.Value }
};
[Theory, MemberData(nameof(TestData_Normalize_SmallBuffer))]
public static void Normalize_SmallBuffer(string path, string pathFlags, int bufferSize, string expectedNormalized, Result expectedResult)
{
NormalizeImpl(path, pathFlags, bufferSize, expectedNormalized, expectedResult);
}
private static void NormalizeImpl(string path, string pathFlags, int bufferSize, string expectedNormalized, Result expectedResult)
{
byte[] buffer = new byte[bufferSize];
Result result = PathFormatter.Normalize(buffer, path.ToU8Span(), GetPathFlags(pathFlags));
Assert.Equal(expectedResult, result);
Assert.Equal(expectedNormalized, StringUtils.Utf8ZToString(buffer));
}
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_EmptyPath => new()
{
{ @"", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"", "E", true, 0, Result.Success },
{ @"/aa/bb/../cc", "E", false, 0, Result.Success }
};
[Theory, MemberData(nameof(TestData_IsNormalized_EmptyPath))]
public static void IsNormalized_EmptyPath(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
Result expectedResult)
{
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
}
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_MountName => new()
{
{ @"mount:/aa/bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mount:/aa/bb", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mount:/aa/bb", "M", true, 12, Result.Success },
{ @"mount:/aa/./bb", "M", false, 6, Result.Success },
{ @"mount:\aa\bb", "M", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"m:/aa/bb", "M", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mo>unt:/aa/bb", "M", false, 0, ResultFs.InvalidCharacter.Value },
{ @"moun?t:/aa/bb", "M", false, 0, ResultFs.InvalidCharacter.Value },
{ @"mo&unt:/aa/bb", "M", true, 13, Result.Success },
{ @"/aa/./bb", "M", false, 0, Result.Success },
{ @"mount/aa/./bb", "M", false, 0, ResultFs.InvalidPathFormat.Value }
};
[Theory, MemberData(nameof(TestData_IsNormalized_MountName))]
public static void IsNormalized_MountName(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
Result expectedResult)
{
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
}
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_WindowsPath => new()
{
{ @"c:/aa/bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"c:/aa/bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"c:\aa\bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\host\share", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\.\c:\", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\.\c:/aa/bb/.", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\?\c:\", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mount:\\host\share\aa\bb", "M", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mount:\\host/share\aa\bb", "M", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mount:/\\aa\..\bb", "MW", false, 0, Result.Success },
{ @"mount:/c:\aa\..\bb", "MW", false, 0, Result.Success },
{ @"mount:/aa/bb", "MW", true, 12, Result.Success },
{ @"/mount:/aa/bb", "MW", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/mount:/aa/bb", "W", false, 0, ResultFs.InvalidCharacter.Value },
{ @"a:aa/../bb", "MW", false, 8, Result.Success },
{ @"a:aa\..\bb", "MW", false, 0, Result.Success },
{ @"/a:aa\..\bb", "W", false, 0, ResultFs.DirectoryUnobtainable.Value },
{ @"\\?\c:\.\aa", "W", false, 0, Result.Success },
{ @"\\.\c:\.\aa", "W", false, 0, Result.Success },
{ @"\\.\mount:\.\aa", "W", false, 0, Result.Success },
{ @"\\./.\aa", "W", false, 0, Result.Success },
{ @"\\/aa", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\\aa", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\", "W", false, 0, Result.Success },
{ @"\\host\share", "W", false, 0, Result.Success },
{ @"\\host\share\path", "W", false, 0, Result.Success },
{ @"\\host\share\path\aa\bb\..\cc\.", "W", false, 0, Result.Success },
{ @"\\host\", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\ho$st\share\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\host:\share\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\..\share\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\host\s:hare\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\host\.\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\host\..\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\host\sha:re", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @".\\host\share", "RW", false, 0, Result.Success }
};
[Theory, MemberData(nameof(TestData_IsNormalized_WindowsPath))]
public static void IsNormalized_WindowsPath(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
Result expectedResult)
{
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
}
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_RelativePath => new()
{
{ @"./aa/bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"./aa/bb/../cc", "R", false, 1, Result.Success },
{ @".\aa/bb/../cc", "R", false, 0, Result.Success },
{ @".", "R", true, 1, Result.Success },
{ @"../aa/bb", "R", false, 0, ResultFs.DirectoryUnobtainable.Value },
{ @"/aa/./bb", "R", false, 0, Result.Success },
{ @"mount:./aa/bb", "MR", true, 13, Result.Success },
{ @"mount:./aa/./bb", "MR", false, 7, Result.Success },
{ @"mount:./aa/bb", "M", false, 0, ResultFs.InvalidPathFormat.Value }
};
[Theory, MemberData(nameof(TestData_IsNormalized_RelativePath))]
public static void IsNormalized_RelativePath(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
Result expectedResult)
{
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
}
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_Backslash => new()
{
{ @"\aa\bb\..\cc", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\aa\bb\..\cc", "B", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"/aa\bb\..\cc", "", false, 0, ResultFs.DirectoryUnobtainable.Value },
{ @"/aa\bb\..\cc", "B", false, 0, ResultFs.DirectoryUnobtainable.Value },
{ @"/aa\bb\cc", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa\bb\cc", "B", true, 9, Result.Success },
{ @"\\host\share\path\aa\bb\cc", "W", false, 0, Result.Success },
{ @"\\host\share\path\aa\bb\cc", "WB", false, 0, Result.Success },
{ @"/aa/bb\../cc/..\dd\..\ee/..", "", false, 0, ResultFs.DirectoryUnobtainable.Value },
{ @"/aa/bb\../cc/..\dd\..\ee/..", "B", false, 0, ResultFs.DirectoryUnobtainable.Value }
};
[Theory, MemberData(nameof(TestData_IsNormalized_Backslash))]
public static void IsNormalized_Backslash(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
Result expectedResult)
{
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
}
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_All => new()
{
{ @"mount:./aa/bb", "WRM", true, 13, Result.Success },
{ @"mount:./aa/bb\cc/dd", "WRM", false, 0, Result.Success },
{ @"mount:./aa/bb\cc/dd", "WRMB", true, 19, Result.Success },
{ @"mount:./.c:/aa/bb", "RM", false, 0, ResultFs.InvalidCharacter.Value },
{ @"mount:.c:/aa/bb", "WRM", false, 0, Result.Success },
{ @"mount:./cc:/aa/bb", "WRM", false, 0, ResultFs.InvalidCharacter.Value },
{ @"mount:./\\host\share/aa/bb", "MW", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mount:./\\host\share/aa/bb", "WRM", false, 0, Result.Success },
{ @"mount:.\\host\share/aa/bb", "WRM", false, 0, Result.Success },
{ @"mount:..\\host\share/aa/bb", "WRM", false, 0, Result.Success },
{ @".\\host\share/aa/bb", "WRM", false, 0, Result.Success },
{ @"..\\host\share/aa/bb", "WRM", false, 0, Result.Success },
{ @"mount:\\host\share/aa/bb", "MW", true, 24, Result.Success },
{ @"mount:\aa\bb", "BM", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mount:/aa\bb", "BM", true, 12, Result.Success },
{ @".//aa/bb", "RW", false, 1, Result.Success },
{ @"./aa/bb", "R", true, 7, Result.Success },
{ @"./c:/aa/bb", "RW", false, 0, ResultFs.InvalidCharacter.Value }
};
[Theory, MemberData(nameof(TestData_IsNormalized_All))]
public static void IsNormalized_All(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
Result expectedResult)
{
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
}
private static void IsNormalizedImpl(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
Result expectedResult)
{
Result result = PathFormatter.IsNormalized(out bool isNormalized, out int length, path.ToU8Span(),
GetPathFlags(pathFlags));
Assert.Equal(expectedResult, result);
if (result.IsSuccess())
{
Assert.Equal(expectedIsNormalized, isNormalized);
if (isNormalized)
{
Assert.Equal(expectedLength, length);
}
}
}
[Fact]
public static void IsNormalized_InvalidUtf8()
{
ReadOnlySpan<byte> invalidUtf8 = new byte[] { 0x44, 0xE3, 0xAA, 0x55, 0x50 };
Result result = PathFormatter.IsNormalized(out _, out _, invalidUtf8, new PathFlags());
Assert.Result(ResultFs.InvalidPathFormat, result);
}
private static PathFlags GetPathFlags(string pathFlags)
{
var flags = new PathFlags();
foreach (char c in pathFlags)
{
switch (c)
{
case 'B':
flags.AllowBackslash();
break;
case 'E':
flags.AllowEmptyPath();
break;
case 'M':
flags.AllowMountName();
break;
case 'R':
flags.AllowRelativePath();
break;
case 'W':
flags.AllowWindowsPath();
break;
}
}
return flags;
}
}
}

View file

@ -0,0 +1,406 @@
// Uses GLoat to run code in nnsdk https://github.com/h1k421/GLoat
#include <gloat.hpp>
#include<array>
#include<string>
#include<tuple>
static char Buf[0x80000];
static int BufPos = 0;
static char ResultNameBuf[0x100];
namespace nn::fs::detail {
bool IsEnabledAccessLog();
}
// SDK 12
namespace nn::fs {
bool IsSubPath(const char* path1, const char* path2);
class PathFlags {
private:
int32_t value;
public:
PathFlags() { value = 0; }
void AllowWindowsPath() { value |= (1 << 0); }
void AllowRelativePath() { value |= (1 << 1); }
void AllowEmptyPath() { value |= (1 << 2); }
void AllowMountName() { value |= (1 << 3); }
void AllowBackslash() { value |= (1 << 4); }
const bool IsWindowsPathAllowed() { return (value & (1 << 0)) != 0; }
const bool IsRelativePathAllowed() { return (value & (1 << 1)) != 0; }
const bool IsEmptyPathAllowed() { return (value & (1 << 2)) != 0; }
const bool IsMountNameAllowed() { return (value & (1 << 3)) != 0; }
const bool IsBackslashAllowed() { return (value & (1 << 4)) != 0; }
};
class PathFormatter {
public:
static nn::Result Normalize(char* buffer, uint64_t normalizeBufferLength, const char* path, uint64_t pathLength, const nn::fs::PathFlags&);
static nn::Result IsNormalized(bool* outIsNormalized, uint64_t* outNormalizedPathLength, const char* path, const nn::fs::PathFlags&);
static nn::Result SkipWindowsPath(const char** outPath, uint64_t* outLength, bool* outIsNormalized, const char* path, bool hasMountName);
static nn::Result SkipMountName(const char** outPath, uint64_t* outLength, const char* path);
};
class PathNormalizer {
public:
static nn::Result Normalize(char* outBuffer, uint64_t* outLength, const char* path, uint64_t outBufferLength, bool isWindowsPath, bool isDriveRelative);
static nn::Result IsNormalized(bool* outIsNormalized, uint64_t* outNormalizedPathLength, const char* path);
};
}
template<typename T, typename... Ts>
constexpr auto make_array(T&& head, Ts&&... tail)->std::array<T, 1 + sizeof...(Ts)>
{
return { head, tail ... };
}
template<size_t N, typename... Ts>
void CreateTest(const char* name, void (*func)(Ts...), const std::array<std::tuple<Ts...>, N>& testData) {
Buf[0] = '\n';
BufPos = 1;
BufPos += sprintf(&Buf[BufPos], "%s\n", name);
for (auto item : testData) {
std::apply(func, item);
}
svcOutputDebugString(Buf, BufPos);
}
const char* GetResultName(nn::Result result) {
switch (result.GetValue()) {
case 0: return "Result.Success";
case 0x2EE402: return "ResultFs.InvalidPath.Value";
case 0x2EE602: return "ResultFs.TooLongPath.Value";
case 0x2EE802: return "ResultFs.InvalidCharacter.Value";
case 0x2EEA02: return "ResultFs.InvalidPathFormat.Value";
case 0x2EEC02: return "ResultFs.DirectoryUnobtainable.Value";
default:
sprintf(ResultNameBuf, "0x%x", result.GetValue());
return ResultNameBuf;
}
}
constexpr const char* const BoolStr(bool value)
{
return value ? "true" : "false";
}
nn::fs::PathFlags GetPathFlags(char const* pathFlags) {
nn::fs::PathFlags flags = nn::fs::PathFlags();
for (char const* c = pathFlags; *c; c++) {
switch (*c) {
case 'B':
flags.AllowBackslash();
break;
case 'E':
flags.AllowEmptyPath();
break;
case 'M':
flags.AllowMountName();
break;
case 'R':
flags.AllowRelativePath();
break;
case 'W':
flags.AllowWindowsPath();
break;
}
}
return flags;
}
static constexpr const auto TestData_PathFormatterNormalize_EmptyPath = make_array(
// Check AllowEmptyPath option
std::make_tuple("", ""),
std::make_tuple("", "E"),
std::make_tuple("/aa/bb/../cc", "E")
);
static constexpr const auto TestData_PathFormatterNormalize_MountName = make_array(
// Mount names should only be allowed with the AllowMountNames option
std::make_tuple("mount:/aa/bb", ""), // Mount name isn't allowed without the AllowMountNames option
std::make_tuple("mount:/aa/bb", "W"),
std::make_tuple("mount:/aa/bb", "M"), // Basic mount names
std::make_tuple("mount:/aa/./bb", "M"),
std::make_tuple("mount:\\aa\\bb", "M"),
std::make_tuple("m:/aa/bb", "M"), // Windows mount name without AllowWindowsPath option
std::make_tuple("mo>unt:/aa/bb", "M"), // Mount names with invalid characters
std::make_tuple("moun?t:/aa/bb", "M"),
std::make_tuple("mo&unt:/aa/bb", "M"), // Mount name with valid special character
std::make_tuple("/aa/./bb", "M"), // AllowMountName set when path has no mount name
std::make_tuple("mount/aa/./bb", "M") // Relative path or mount name is missing separator
);
static constexpr const auto TestData_PathFormatterNormalize_WindowsPath = make_array(
// Windows paths should only be allowed with the AllowWindowsPath option
std::make_tuple(R"(c:/aa/bb)", ""),
std::make_tuple(R"(c:\aa\bb)", ""),
std::make_tuple(R"(\\host\share)", ""),
std::make_tuple(R"(\\.\c:\)", ""),
std::make_tuple(R"(\\.\c:/aa/bb/.)", ""),
std::make_tuple(R"(\\?\c:\)", ""),
std::make_tuple(R"(mount:\\host\share\aa\bb)", "M"), // Catch instances where the Windows path comes after other parts in the path
std::make_tuple(R"(mount:\\host/share\aa\bb)", "M"), // And do it again with the UNC path not normalized
std::make_tuple(R"(mount:/\\aa\..\bb)", "MW"),
std::make_tuple(R"(mount:/c:\aa\..\bb)", "MW"),
std::make_tuple(R"(mount:/aa/bb)", "MW"),
std::make_tuple(R"(/mount:/aa/bb)", "MW"),
std::make_tuple(R"(/mount:/aa/bb)", "W"),
std::make_tuple(R"(a:aa/../bb)", "MW"),
std::make_tuple(R"(a:aa\..\bb)", "MW"),
std::make_tuple(R"(/a:aa\..\bb)", "W"),
std::make_tuple(R"(\\?\c:\.\aa)", "W"), // Path with win32 file namespace prefix
std::make_tuple(R"(\\.\c:\.\aa)", "W"), // Path with win32 device namespace prefix
std::make_tuple(R"(\\.\mount:\.\aa)", "W"),
std::make_tuple(R"(\\./.\aa)", "W"),
std::make_tuple(R"(\\/aa)", "W"),
std::make_tuple(R"(\\\aa)", "W"),
std::make_tuple(R"(\\)", "W"),
std::make_tuple(R"(\\host\share)", "W"), // Basic UNC paths
std::make_tuple(R"(\\host\share\path)", "W"),
std::make_tuple(R"(\\host\share\path\aa\bb\..\cc\.)", "W"), // UNC path using only backslashes that is not normalized
std::make_tuple(R"(\\host\)", "W"), // Share name cannot be empty
std::make_tuple(R"(\\ho$st\share\path)", "W"), // Invalid character '$' in host name
std::make_tuple(R"(\\host:\share\path)", "W"), // Invalid character ':' in host name
std::make_tuple(R"(\\..\share\path)", "W"), // Host name can't be ".."
std::make_tuple(R"(\\host\s:hare\path)", "W"), // Invalid character ':' in host name
std::make_tuple(R"(\\host\.\path)", "W"), // Share name can't be "."
std::make_tuple(R"(\\host\..\path)", "W"), // Share name can't be ".."
std::make_tuple(R"(\\host\sha:re)", "W"), // Invalid share name when nothing follows it
std::make_tuple(R"(.\\host\share)", "RW") // Can't have a relative Windows path
);
static constexpr const auto TestData_PathFormatterNormalize_RelativePath = make_array(
std::make_tuple("./aa/bb", ""), // Relative path isn't allowed without the AllowRelativePaths option
std::make_tuple("./aa/bb/../cc", "R"), // Basic relative paths using different separators
std::make_tuple(".\\aa/bb/../cc", "R"),
std::make_tuple(".", "R"), // Standalone current directory
std::make_tuple("../aa/bb", "R"), // Path starting with parent directory is not allowed
std::make_tuple("/aa/./bb", "R"), // Absolute paths should work normally
std::make_tuple("mount:./aa/bb", "MR"), // Mount name with relative path
std::make_tuple("mount:./aa/./bb", "MR"),
std::make_tuple("mount:./aa/bb", "M")
);
static constexpr const auto TestData_PathFormatterNormalize_Backslash = make_array(
std::make_tuple(R"(\aa\bb\..\cc)", ""), // Paths can't start with a backslash no matter the path flags set
std::make_tuple(R"(\aa\bb\..\cc)", "B"),
std::make_tuple(R"(/aa\bb\..\cc)", ""), // Paths can contain backslashes if they start with a frontslash and have AllowBackslash set
std::make_tuple(R"(/aa\bb\..\cc)", "B"), // When backslashes are allowed they do not count as a directory separator
std::make_tuple(R"(/aa\bb\cc)", ""), // Normalized path without a prefix except it uses backslashes
std::make_tuple(R"(/aa\bb\cc)", "B"),
std::make_tuple(R"(\\host\share\path\aa\bb\cc)", "W"), // Otherwise normalized Windows path except with backslashes
std::make_tuple(R"(\\host\share\path\aa\bb\cc)", "WB"),
std::make_tuple(R"(/aa/bb\../cc/..\dd\..\ee/..)", ""), // Path with "parent directory path replacement needed"
std::make_tuple(R"(/aa/bb\../cc/..\dd\..\ee/..)", "B")
);
static constexpr const auto TestData_PathFormatterNormalize_All = make_array(
std::make_tuple(R"(mount:./aa/bb)", "WRM"), // Normalized path with both mount name and relative path
std::make_tuple(R"(mount:./aa/bb\cc/dd)", "WRM"), // Path with backslashes
std::make_tuple(R"(mount:./aa/bb\cc/dd)", "WRMB"), // This path is considered normalized but the backslashes still normalize to forward slashes
std::make_tuple(R"(mount:./.c:/aa/bb)", "RM"), // These next 2 form a chain where if you normalize one it'll turn into the next
std::make_tuple(R"(mount:.c:/aa/bb)", "WRM"),
std::make_tuple(R"(mount:./cc:/aa/bb)", "WRM"),
std::make_tuple(R"(mount:./\\host\share/aa/bb)", "MW"),
std::make_tuple(R"(mount:./\\host\share/aa/bb)", "WRM"), // These next 3 form a chain where if you normalize one it'll turn into the next
std::make_tuple(R"(mount:.\\host\share/aa/bb)", "WRM"),
std::make_tuple(R"(mount:..\\host\share/aa/bb)", "WRM"),
std::make_tuple(R"(.\\host\share/aa/bb)", "WRM"), // These next 2 form a chain where if you normalize one it'll turn into the next
std::make_tuple(R"(..\\host\share/aa/bb)", "WRM"),
std::make_tuple(R"(mount:\\host\share/aa/bb)", "MW"), // Use a mount name and windows path together
std::make_tuple(R"(mount:\aa\bb)", "BM"), // Backslashes are never allowed directly after a mount name even with AllowBackslashes
std::make_tuple(R"(mount:/aa\bb)", "BM"),
std::make_tuple(R"(.//aa/bb)", "RW"), // Relative path followed by a Windows path won't work
std::make_tuple(R"(./aa/bb)", "R"),
std::make_tuple(R"(./c:/aa/bb)", "RW")
);
void CreateTest_PathFormatterNormalize(char const* path, char const* pathFlags) {
char normalized[0x200] = { 0 };
nn::fs::PathFlags flags = GetPathFlags(pathFlags);
nn::Result result = nn::fs::PathFormatter::Normalize(normalized, 0x200, path, 0x200, flags);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", @\"%s\", %s},\n",
path, pathFlags, normalized, GetResultName(result));
}
void CreateTest_PathFormatterIsNormalized(char const* path, char const* pathFlags) {
bool isNormalized = 0;
uint64_t normalizedLength = 0;
nn::fs::PathFlags flags = GetPathFlags(pathFlags);
nn::Result result = nn::fs::PathFormatter::IsNormalized(&isNormalized, &normalizedLength, path, flags);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", %s, %ld, %s},\n",
path, pathFlags, BoolStr(isNormalized), normalizedLength, GetResultName(result));
}
static constexpr const auto TestData_PathFormatterNormalize_SmallBuffer = make_array(
//std::make_tuple(R"(aa/bb)", "MR", 2), // Crashes nnsdk and throws an out-of-range exception in LibHac. I guess that counts as a pass?
std::make_tuple(R"(/aa/bb)", "M", 1),
std::make_tuple(R"(mount:/aa/bb)", "MR", 6),
std::make_tuple(R"(mount:/aa/bb)", "MR", 7),
std::make_tuple(R"(aa/bb)", "MR", 3),
std::make_tuple(R"(\\host\share)", "W", 13)
);
void CreateTest_PathFormatterNormalize_SmallBuffer(char const* path, char const* pathFlags, int bufferSize) {
char normalized[0x200] = { 0 };
nn::fs::PathFlags flags = GetPathFlags(pathFlags);
svcOutputDebugString(path, strnlen(path, 0x200));
nn::Result result = nn::fs::PathFormatter::Normalize(normalized, bufferSize, path, 0x200, flags);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", %d, @\"%s\", %s},\n",
path, pathFlags, bufferSize, normalized, GetResultName(result));
}
static constexpr const auto TestData_PathNormalizerNormalize = make_array(
std::make_tuple("/aa/bb/c/", false, true),
std::make_tuple("aa/bb/c/", false, false),
std::make_tuple("aa/bb/c/", false, true),
std::make_tuple("mount:a/b", false, true),
std::make_tuple("/aa/bb/../..", true, false),
std::make_tuple("/aa/bb/../../..", true, false),
std::make_tuple("/aa/bb/../../..", false, false),
std::make_tuple("aa/bb/../../..", true, true),
std::make_tuple("aa/bb/../../..", false, true),
std::make_tuple("", false, false),
std::make_tuple("/", false, false),
std::make_tuple("/.", false, false),
std::make_tuple("/./", false, false),
std::make_tuple("/..", false, false),
std::make_tuple("//.", false, false),
std::make_tuple("/ ..", false, false),
std::make_tuple("/.. /", false, false),
std::make_tuple("/. /.", false, false),
std::make_tuple("/aa/bb/cc/dd/./.././../..", false, false),
std::make_tuple("/aa/bb/cc/dd/./.././../../..", false, false),
std::make_tuple("/./aa/./bb/./cc/./dd/.", false, false),
std::make_tuple("/aa\\bb/cc", false, false),
std::make_tuple("/aa\\bb/cc", false, false),
std::make_tuple("/a|/bb/cc", false, false),
std::make_tuple("/>a/bb/cc", false, false),
std::make_tuple("/aa/.</cc", false, false),
std::make_tuple("/aa/..</cc", false, false),
std::make_tuple("\\\\aa/bb/cc", false, false),
std::make_tuple("\\\\aa\\bb\\cc", false, false),
std::make_tuple("/aa/bb/..\\cc", false, false),
std::make_tuple("/aa/bb\\..\\cc", false, false),
std::make_tuple("/aa/bb\\..", false, false),
std::make_tuple("/aa\\bb/../cc", false, false)
);
void CreateTest_PathNormalizerNormalize(char const* path, bool isWindowsPath, bool isRelativePath) {
char normalized[0x200] = { 0 };
uint64_t normalizedLength = 0;
nn::Result result = nn::fs::PathNormalizer::Normalize(normalized, &normalizedLength, path, 0x200, isWindowsPath, isRelativePath);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %s, @\"%s\", %ld, %s},\n",
path, BoolStr(isWindowsPath), BoolStr(isRelativePath), normalized, normalizedLength, GetResultName(result));
}
void CreateTest_PathNormalizerIsNormalized(char const* path, bool isWindowsPath, bool isRelativePath) {
bool isNormalized = false;
uint64_t normalizedLength = 0;
nn::Result result = nn::fs::PathNormalizer::IsNormalized(&isNormalized, &normalizedLength, path);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %ld, %s},\n",
path, BoolStr(isNormalized), normalizedLength, GetResultName(result));
}
static constexpr const auto TestData_PathNormalizerNormalize_SmallBuffer = make_array(
std::make_tuple("/aa/bb/cc/", 7),
std::make_tuple("/aa/bb/cc/", 8),
std::make_tuple("/aa/bb/cc/", 9),
std::make_tuple("/aa/bb/cc/", 10),
std::make_tuple("/aa/bb/cc", 9),
std::make_tuple("/aa/bb/cc", 10),
std::make_tuple("/./aa/./bb/./cc", 9),
std::make_tuple("/./aa/./bb/./cc", 10),
std::make_tuple("/aa/bb/cc/../../..", 9),
std::make_tuple("/aa/bb/cc/../../..", 10),
std::make_tuple("/aa/bb/.", 7),
std::make_tuple("/aa/bb/./", 7),
std::make_tuple("/aa/bb/..", 8),
std::make_tuple("/aa/bb", 1),
std::make_tuple("/aa/bb", 2),
std::make_tuple("/aa/bb", 3),
std::make_tuple("aa/bb", 1)
);
void CreateTest_PathNormalizerNormalize_SmallBuffer(char const* path, int bufferSize) {
char normalized[0x200] = { 0 };
uint64_t normalizedLength = 0;
nn::Result result = nn::fs::PathNormalizer::Normalize(normalized, &normalizedLength, path, bufferSize, false, false);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %d, @\"%s\", %ld, %s},\n",
path, bufferSize, normalized, normalizedLength, GetResultName(result));
}
static constexpr const auto TestData_PathUtility_IsSubPath = make_array(
std::make_tuple("//a/b", "/a"),
std::make_tuple("/a", "//a/b"),
std::make_tuple("//a/b", "\\\\a"),
std::make_tuple("//a/b", "//a"),
std::make_tuple("/", "/a"),
std::make_tuple("/a", "/"),
std::make_tuple("/", "/"),
std::make_tuple("", ""),
std::make_tuple("/", ""),
std::make_tuple("/", "mount:/a"),
std::make_tuple("mount:/", "mount:/"),
std::make_tuple("mount:/a/b", "mount:/a/b"),
std::make_tuple("mount:/a/b", "mount:/a/b/c"),
std::make_tuple("/a/b", "/a/b/c"),
std::make_tuple("/a/b/c", "/a/b"),
std::make_tuple("/a/b", "/a/b"),
std::make_tuple("/a/b", "/a/b\\c")
);
void CreateTest_PathUtility_IsSubPath(const char* path1, const char* path2) {
bool result = nn::fs::IsSubPath(path1, path2);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", @\"%s\", %s},\n",
path1, path2, BoolStr(result));
}
extern "C" void nnMain(void) {
// nn::fs::detail::IsEnabledAccessLog(); // Adds the sdk version to the output
CreateTest("TestData_PathFormatter_Normalize_EmptyPath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_EmptyPath);
CreateTest("TestData_PathFormatter_Normalize_MountName", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_MountName);
CreateTest("TestData_PathFormatter_Normalize_WindowsPath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_WindowsPath);
CreateTest("TestData_PathFormatter_Normalize_RelativePath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_RelativePath);
CreateTest("TestData_PathFormatter_Normalize_Backslash", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_Backslash);
CreateTest("TestData_PathFormatter_Normalize_All", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_All);
CreateTest("TestData_PathFormatter_Normalize_SmallBuffer", CreateTest_PathFormatterNormalize_SmallBuffer, TestData_PathFormatterNormalize_SmallBuffer);
CreateTest("TestData_PathFormatter_IsNormalized_EmptyPath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_EmptyPath);
CreateTest("TestData_PathFormatter_IsNormalized_MountName", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_MountName);
CreateTest("TestData_PathFormatter_IsNormalized_WindowsPath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_WindowsPath);
CreateTest("TestData_PathFormatter_IsNormalized_RelativePath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_RelativePath);
CreateTest("TestData_PathFormatter_IsNormalized_Backslash", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_Backslash);
CreateTest("TestData_PathFormatter_IsNormalized_All", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_All);
CreateTest("TestData_PathNormalizer_Normalize", CreateTest_PathNormalizerNormalize, TestData_PathNormalizerNormalize);
CreateTest("TestData_PathNormalizer_Normalize_SmallBuffer", CreateTest_PathNormalizerNormalize_SmallBuffer, TestData_PathNormalizerNormalize_SmallBuffer);
CreateTest("TestData_PathNormalizer_IsNormalized", CreateTest_PathNormalizerIsNormalized, TestData_PathNormalizerNormalize);
CreateTest("TestData_PathUtility_IsSubPath", CreateTest_PathUtility_IsSubPath, TestData_PathUtility_IsSubPath);
}

View file

@ -0,0 +1,148 @@
// ReSharper disable InconsistentNaming
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Common;
using LibHac.Util;
using Xunit;
namespace LibHac.Tests.Fs
{
public class PathNormalizerTests
{
public static TheoryData<string, bool, bool, string, long, Result> TestData_Normalize => new()
{
{ @"/aa/bb/c/", false, true, @"/aa/bb/c", 8, Result.Success },
{ @"aa/bb/c/", false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
{ @"aa/bb/c/", false, true, @"/aa/bb/c", 8, Result.Success },
{ @"mount:a/b", false, true, @"/mount:a/b", 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/bb/../..", true, false, @"/", 1, Result.Success },
{ @"/aa/bb/../../..", true, false, @"/", 1, Result.Success },
{ @"/aa/bb/../../..", false, false, @"/aa/bb/", 0, ResultFs.DirectoryUnobtainable.Value },
{ @"aa/bb/../../..", true, true, @"/", 1, Result.Success },
{ @"aa/bb/../../..", false, true, @"/aa/bb/", 0, ResultFs.DirectoryUnobtainable.Value },
{ @"", false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
{ @"/", false, false, @"/", 1, Result.Success },
{ @"/.", false, false, @"/", 1, Result.Success },
{ @"/./", false, false, @"/", 1, Result.Success },
{ @"/..", false, false, @"/", 0, ResultFs.DirectoryUnobtainable.Value },
{ @"//.", false, false, @"/", 1, Result.Success },
{ @"/ ..", false, false, @"/ ..", 4, Result.Success },
{ @"/.. /", false, false, @"/.. ", 4, Result.Success },
{ @"/. /.", false, false, @"/. ", 3, Result.Success },
{ @"/aa/bb/cc/dd/./.././../..", false, false, @"/aa", 3, Result.Success },
{ @"/aa/bb/cc/dd/./.././../../..", false, false, @"/", 1, Result.Success },
{ @"/./aa/./bb/./cc/./dd/.", false, false, @"/aa/bb/cc/dd", 12, Result.Success },
{ @"/aa\bb/cc", false, false, @"/aa\bb/cc", 9, Result.Success },
{ @"/aa\bb/cc", false, false, @"/aa\bb/cc", 9, Result.Success },
{ @"/a|/bb/cc", false, false, @"/a|/bb/cc", 0, ResultFs.InvalidCharacter.Value },
{ @"/>a/bb/cc", false, false, @"/>a/bb/cc", 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/.</cc", false, false, @"/aa/.</cc", 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/..</cc", false, false, @"/aa/..</cc", 0, ResultFs.InvalidCharacter.Value },
{ @"\\aa/bb/cc", false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
{ @"\\aa\bb\cc", false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
{ @"/aa/bb/..\cc", false, false, @"/aa/cc", 6, Result.Success },
{ @"/aa/bb\..\cc", false, false, @"/aa/cc", 6, Result.Success },
{ @"/aa/bb\..", false, false, @"/aa", 3, Result.Success },
{ @"/aa\bb/../cc", false, false, @"/cc", 3, Result.Success }
};
[Theory, MemberData(nameof(TestData_Normalize))]
public static void Normalize(string path, bool isWindowsPath, bool isDriveRelativePath, string expectedNormalized,
long expectedLength, Result expectedResult)
{
byte[] buffer = new byte[0x301];
Result result = PathNormalizer12.Normalize(buffer, out int normalizedLength, path.ToU8Span(), isWindowsPath,
isDriveRelativePath);
Assert.Equal(expectedResult, result);
Assert.Equal(expectedNormalized, StringUtils.Utf8ZToString(buffer));
Assert.Equal(expectedLength, normalizedLength);
}
public static TheoryData<string, int, string, long, Result> TestData_Normalize_SmallBuffer => new()
{
{ @"/aa/bb/cc/", 7, @"/aa/bb", 6, ResultFs.TooLongPath.Value },
{ @"/aa/bb/cc/", 8, @"/aa/bb/", 7, ResultFs.TooLongPath.Value },
{ @"/aa/bb/cc/", 9, @"/aa/bb/c", 8, ResultFs.TooLongPath.Value },
{ @"/aa/bb/cc/", 10, @"/aa/bb/cc", 9, Result.Success },
{ @"/aa/bb/cc", 9, @"/aa/bb/c", 8, ResultFs.TooLongPath.Value },
{ @"/aa/bb/cc", 10, @"/aa/bb/cc", 9, Result.Success },
{ @"/./aa/./bb/./cc", 9, @"/aa/bb/c", 8, ResultFs.TooLongPath.Value },
{ @"/./aa/./bb/./cc", 10, @"/aa/bb/cc", 9, Result.Success },
{ @"/aa/bb/cc/../../..", 9, @"/aa/bb/c", 8, ResultFs.TooLongPath.Value },
{ @"/aa/bb/cc/../../..", 10, @"/aa/bb/cc", 9, ResultFs.TooLongPath.Value },
{ @"/aa/bb/.", 7, @"/aa/bb", 6, ResultFs.TooLongPath.Value },
{ @"/aa/bb/./", 7, @"/aa/bb", 6, ResultFs.TooLongPath.Value },
{ @"/aa/bb/..", 8, @"/aa", 3, Result.Success },
{ @"/aa/bb", 1, @"", 0, ResultFs.TooLongPath.Value },
{ @"/aa/bb", 2, @"/", 1, ResultFs.TooLongPath.Value },
{ @"/aa/bb", 3, @"/a", 2, ResultFs.TooLongPath.Value },
{ @"aa/bb", 1, @"", 0, ResultFs.InvalidPathFormat.Value }
};
[Theory, MemberData(nameof(TestData_Normalize_SmallBuffer))]
public static void Normalize_SmallBuffer(string path, int bufferLength, string expectedNormalized, long expectedLength, Result expectedResult)
{
byte[] buffer = new byte[bufferLength];
Result result = PathNormalizer12.Normalize(buffer, out int normalizedLength, path.ToU8Span(), false, false);
Assert.Equal(expectedResult, result);
Assert.Equal(expectedNormalized, StringUtils.Utf8ZToString(buffer));
Assert.Equal(expectedLength, normalizedLength);
}
public static TheoryData<string, bool, long, Result> TestData_IsNormalized => new()
{
{ @"/aa/bb/c/", false, 9, Result.Success },
{ @"aa/bb/c/", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"aa/bb/c/", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mount:a/b", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"/aa/bb/../..", false, 0, Result.Success },
{ @"/aa/bb/../../..", false, 0, Result.Success },
{ @"/aa/bb/../../..", false, 0, Result.Success },
{ @"aa/bb/../../..", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"aa/bb/../../..", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"/", true, 1, Result.Success },
{ @"/.", false, 2, Result.Success },
{ @"/./", false, 0, Result.Success },
{ @"/..", false, 3, Result.Success },
{ @"//.", false, 0, Result.Success },
{ @"/ ..", true, 4, Result.Success },
{ @"/.. /", false, 5, Result.Success },
{ @"/. /.", false, 5, Result.Success },
{ @"/aa/bb/cc/dd/./.././../..", false, 0, Result.Success },
{ @"/aa/bb/cc/dd/./.././../../..", false, 0, Result.Success },
{ @"/./aa/./bb/./cc/./dd/.", false, 0, Result.Success },
{ @"/aa\bb/cc", true, 9, Result.Success },
{ @"/aa\bb/cc", true, 9, Result.Success },
{ @"/a|/bb/cc", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/>a/bb/cc", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/.</cc", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/..</cc", false, 0, ResultFs.InvalidCharacter.Value },
{ @"\\aa/bb/cc", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\aa\bb\cc", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"/aa/bb/..\cc", true, 12, Result.Success },
{ @"/aa/bb\..\cc", true, 12, Result.Success },
{ @"/aa/bb\..", true, 9, Result.Success },
{ @"/aa\bb/../cc", false, 0, Result.Success }
};
[Theory, MemberData(nameof(TestData_IsNormalized))]
public static void IsNormalized(string path, bool expectedIsNormalized, long expectedLength, Result expectedResult)
{
Result result = PathNormalizer12.IsNormalized(out bool isNormalized, out int length, path.ToU8Span());
Assert.Equal(expectedResult, result);
Assert.Equal(expectedLength, length);
if (result.IsSuccess())
{
Assert.Equal(expectedIsNormalized, isNormalized);
}
}
}
}

View file

@ -0,0 +1,38 @@
using LibHac.Common;
using LibHac.Fs.Common;
using Xunit;
namespace LibHac.Tests.Fs
{
public class PathUtilityTests
{
public static TheoryData<string, string, bool> TestData_IsSubPath => new()
{
{ @"//a/b", @"/a", false },
{ @"/a", @"//a/b", false },
{ @"//a/b", @"\\a", false },
{ @"//a/b", @"//a", true },
{ @"/", @"/a", true },
{ @"/a", @"/", true },
{ @"/", @"/", false },
{ @"", @"", false },
{ @"/", @"", true },
{ @"/", @"mount:/a", false },
{ @"mount:/", @"mount:/", false },
{ @"mount:/a/b", @"mount:/a/b", false },
{ @"mount:/a/b", @"mount:/a/b/c", true },
{ @"/a/b", @"/a/b/c", true },
{ @"/a/b/c", @"/a/b", true },
{ @"/a/b", @"/a/b", false },
{ @"/a/b", @"/a/b\c", false }
};
[Theory, MemberData(nameof(TestData_IsSubPath))]
public static void IsSubPath(string path1, string path2, bool expectedResult)
{
bool result = PathUtility12.IsSubPath(path1.ToU8Span(), path2.ToU8Span());
Assert.Equal(expectedResult, result);
}
}
}