Update PathFormatter and PathNormalizer for 13.1.0

This commit is contained in:
Alex Barney 2022-01-04 22:17:35 -07:00
parent e1fd31c1ff
commit 61b29e57a3
6 changed files with 448 additions and 178 deletions

View File

@ -19,12 +19,14 @@ public struct PathFlags
public void AllowEmptyPath() => _value |= 1 << 2; public void AllowEmptyPath() => _value |= 1 << 2;
public void AllowMountName() => _value |= 1 << 3; public void AllowMountName() => _value |= 1 << 3;
public void AllowBackslash() => _value |= 1 << 4; public void AllowBackslash() => _value |= 1 << 4;
public void AllowAllCharacters() => _value |= 1 << 5;
public bool IsWindowsPathAllowed() => (_value & (1 << 0)) != 0; public bool IsWindowsPathAllowed() => (_value & (1 << 0)) != 0;
public bool IsRelativePathAllowed() => (_value & (1 << 1)) != 0; public bool IsRelativePathAllowed() => (_value & (1 << 1)) != 0;
public bool IsEmptyPathAllowed() => (_value & (1 << 2)) != 0; public bool IsEmptyPathAllowed() => (_value & (1 << 2)) != 0;
public bool IsMountNameAllowed() => (_value & (1 << 3)) != 0; public bool IsMountNameAllowed() => (_value & (1 << 3)) != 0;
public bool IsBackslashAllowed() => (_value & (1 << 4)) != 0; public bool IsBackslashAllowed() => (_value & (1 << 4)) != 0;
public bool AreAllCharactersAllowed() => (_value & (1 << 5)) != 0;
} }
/// <summary> /// <summary>

View File

@ -13,9 +13,19 @@ namespace LibHac.Fs;
/// <summary> /// <summary>
/// Contains functions for working with path formatting and normalization. /// Contains functions for working with path formatting and normalization.
/// </summary> /// </summary>
/// <remarks>Based on FS 12.1.0 (nnSdk 12.3.1)</remarks> /// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
public static class PathFormatter public static class PathFormatter
{ {
private static ReadOnlySpan<byte> InvalidCharacter =>
new[] { (byte)':', (byte)'*', (byte)'?', (byte)'<', (byte)'>', (byte)'|' };
private static ReadOnlySpan<byte> InvalidCharacterForHostName =>
new[] { (byte)':', (byte)'*', (byte)'<', (byte)'>', (byte)'|', (byte)'$' };
private static ReadOnlySpan<byte> InvalidCharacterForMountName =>
new[] { (byte)'*', (byte)'?', (byte)'<', (byte)'>', (byte)'|' };
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Result CheckHostName(ReadOnlySpan<byte> name) private static Result CheckHostName(ReadOnlySpan<byte> name)
{ {
@ -24,8 +34,11 @@ public static class PathFormatter
for (int i = 0; i < name.Length; i++) for (int i = 0; i < name.Length; i++)
{ {
if (name[i] == ':' || name[i] == '$') foreach (byte c in InvalidCharacterForHostName)
return ResultFs.InvalidPathFormat.Log(); {
if (name[i] == c)
return ResultFs.InvalidCharacter.Log();
}
} }
return Result.Success; return Result.Success;
@ -41,8 +54,11 @@ public static class PathFormatter
for (int i = 0; i < name.Length; i++) for (int i = 0; i < name.Length; i++)
{ {
if (name[i] == ':') foreach (byte c in InvalidCharacter)
return ResultFs.InvalidPathFormat.Log(); {
if (name[i] == c)
return ResultFs.InvalidCharacter.Log();
}
} }
return Result.Success; return Result.Success;
@ -90,8 +106,11 @@ public static class PathFormatter
for (int i = 0; i < mountLength; i++) for (int i = 0; i < mountLength; i++)
{ {
if (path.At(i) is (byte)'*' or (byte)'?' or (byte)'<' or (byte)'>' or (byte)'|') foreach (byte c in InvalidCharacterForMountName)
return ResultFs.InvalidCharacter.Log(); {
if (path.At(i) == c)
return ResultFs.InvalidCharacter.Log();
}
} }
if (!outMountNameBuffer.IsEmpty) if (!outMountNameBuffer.IsEmpty)
@ -150,6 +169,12 @@ public static class PathFormatter
int winPathLength; int winPathLength;
for (winPathLength = 2; currentPath.At(winPathLength) != NullTerminator; winPathLength++) for (winPathLength = 2; currentPath.At(winPathLength) != NullTerminator; winPathLength++)
{ {
foreach (byte c in InvalidCharacter)
{
if (currentPath[winPathLength] == c)
return ResultFs.InvalidCharacter.Log();
}
if (currentPath[winPathLength] == DirectorySeparator || if (currentPath[winPathLength] == DirectorySeparator ||
currentPath[winPathLength] == AltDirectorySeparator) currentPath[winPathLength] == AltDirectorySeparator)
{ {
@ -478,8 +503,11 @@ public static class PathFormatter
} }
} }
if (PathNormalizer.IsParentDirectoryPathReplacementNeeded(buffer)) if (flags.IsBackslashAllowed() && PathNormalizer.IsParentDirectoryPathReplacementNeeded(buffer))
return ResultFs.DirectoryUnobtainable.Log(); {
isNormalized = false;
return Result.Success;
}
rc = PathUtility.CheckInvalidBackslash(out bool isBackslashContained, buffer, rc = PathUtility.CheckInvalidBackslash(out bool isBackslashContained, buffer,
flags.IsWindowsPathAllowed() || flags.IsBackslashAllowed()); flags.IsWindowsPathAllowed() || flags.IsBackslashAllowed());
@ -491,7 +519,7 @@ public static class PathFormatter
return Result.Success; return Result.Success;
} }
rc = PathNormalizer.IsNormalized(out isNormalized, out int length, buffer); rc = PathNormalizer.IsNormalized(out isNormalized, out int length, buffer, flags.AreAllCharactersAllowed());
if (rc.IsFailure()) return rc; if (rc.IsFailure()) return rc;
totalLength += length; totalLength += length;
@ -612,7 +640,8 @@ public static class PathFormatter
src = srcBufferSlashReplaced.AsSpan(srcOffset); src = srcBufferSlashReplaced.AsSpan(srcOffset);
} }
rc = PathNormalizer.Normalize(outputBuffer.Slice(currentPos), out _, src, isWindowsPath, isDriveRelative); rc = PathNormalizer.Normalize(outputBuffer.Slice(currentPos), out _, src, isWindowsPath, isDriveRelative,
flags.AreAllCharactersAllowed());
if (rc.IsFailure()) return rc; if (rc.IsFailure()) return rc;
return Result.Success; return Result.Success;

View File

@ -10,7 +10,7 @@ namespace LibHac.Fs;
/// <summary> /// <summary>
/// Contains functions for doing with basic path normalization. /// Contains functions for doing with basic path normalization.
/// </summary> /// </summary>
/// <remarks>Based on FS 12.1.0 (nnSdk 12.3.1)</remarks> /// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
public static class PathNormalizer public static class PathNormalizer
{ {
private enum PathState private enum PathState
@ -25,6 +25,12 @@ public static class PathNormalizer
public static Result Normalize(Span<byte> outputBuffer, out int length, ReadOnlySpan<byte> path, bool isWindowsPath, public static Result Normalize(Span<byte> outputBuffer, out int length, ReadOnlySpan<byte> path, bool isWindowsPath,
bool isDriveRelativePath) bool isDriveRelativePath)
{
return Normalize(outputBuffer, out length, path, isWindowsPath, isDriveRelativePath, false);
}
public static Result Normalize(Span<byte> outputBuffer, out int length, ReadOnlySpan<byte> path, bool isWindowsPath,
bool isDriveRelativePath, bool allowAllCharacters)
{ {
UnsafeHelpers.SkipParamInit(out length); UnsafeHelpers.SkipParamInit(out length);
@ -32,7 +38,7 @@ public static class PathNormalizer
int totalLength = 0; int totalLength = 0;
int i = 0; int i = 0;
if (!IsSeparator(path.At(0))) if (path.At(0) != DirectorySeparator)
{ {
if (!isDriveRelativePath) if (!isDriveRelativePath)
return ResultFs.InvalidPathFormat.Log(); return ResultFs.InvalidPathFormat.Log();
@ -43,6 +49,7 @@ public static class PathNormalizer
var convertedPath = new RentedArray<byte>(); var convertedPath = new RentedArray<byte>();
try try
{ {
Result rc;
// Check if parent directory path replacement is needed. // Check if parent directory path replacement is needed.
if (IsParentDirectoryPathReplacementNeeded(currentPath)) if (IsParentDirectoryPathReplacementNeeded(currentPath))
{ {
@ -58,20 +65,22 @@ public static class PathNormalizer
bool skipNextSeparator = false; bool skipNextSeparator = false;
while (!IsNul(currentPath.At(i))) while (currentPath.At(i) != NullTerminator)
{ {
if (IsSeparator(currentPath[i])) if (currentPath[i] == DirectorySeparator)
{ {
do do
{ {
i++; i++;
} while (IsSeparator(currentPath.At(i))); } while (currentPath.At(i) == DirectorySeparator);
if (IsNul(currentPath.At(i))) if (currentPath.At(i) == NullTerminator)
break; break;
if (!skipNextSeparator) if (!skipNextSeparator)
{ {
// Note: Nintendo returns TooLongPath in some cases where the output buffer is actually long
// enough to hold the normalized path. e.g. "/aa/bb/." with an output buffer length of 7
if (totalLength + 1 == outputBuffer.Length) if (totalLength + 1 == outputBuffer.Length)
{ {
outputBuffer[totalLength] = NullTerminator; outputBuffer[totalLength] = NullTerminator;
@ -87,8 +96,14 @@ public static class PathNormalizer
} }
int dirLen = 0; int dirLen = 0;
while (!IsSeparator(currentPath.At(i + dirLen)) && !IsNul(currentPath.At(i + dirLen))) while (currentPath.At(i + dirLen) != DirectorySeparator && currentPath.At(i + dirLen) != NullTerminator)
{ {
if (!allowAllCharacters)
{
rc = CheckInvalidCharacter(currentPath[i + dirLen]);
if (rc.IsFailure()) return rc.Miss();
}
dirLen++; dirLen++;
} }
@ -163,12 +178,14 @@ public static class PathNormalizer
} }
// Note: This bug is in the original code. They probably meant to put "totalLength + 1" // Note: This bug is in the original code. They probably meant to put "totalLength + 1"
if (totalLength - 1 > outputBuffer.Length) // The buffer needs to be able to contain the total length of the normalized string plus
// one for the null terminator
if (outputBuffer.Length < totalLength - 1)
return ResultFs.TooLongPath.Log(); return ResultFs.TooLongPath.Log();
outputBuffer[totalLength] = NullTerminator; outputBuffer[totalLength] = NullTerminator;
Result rc = IsNormalized(out bool isNormalized, out _, outputBuffer); rc = IsNormalized(out bool isNormalized, out _, outputBuffer, allowAllCharacters);
if (rc.IsFailure()) return rc; if (rc.IsFailure()) return rc;
Assert.SdkAssert(isNormalized); Assert.SdkAssert(isNormalized);
@ -200,6 +217,12 @@ public static class PathNormalizer
/// <see cref="ResultFs.InvalidCharacter"/>: The path contains an invalid character.<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> /// <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) public static Result IsNormalized(out bool isNormalized, out int length, ReadOnlySpan<byte> path)
{
return IsNormalized(out isNormalized, out length, path, false);
}
public static Result IsNormalized(out bool isNormalized, out int length, ReadOnlySpan<byte> path,
bool allowAllCharacters)
{ {
UnsafeHelpers.SkipParamInit(out isNormalized, out length); UnsafeHelpers.SkipParamInit(out isNormalized, out length);
@ -213,7 +236,7 @@ public static class PathNormalizer
pathLength++; pathLength++;
if (state != PathState.Initial) if (!allowAllCharacters && state != PathState.Initial)
{ {
Result rc = CheckInvalidCharacter(c); Result rc = CheckInvalidCharacter(c);
if (rc.IsFailure()) return rc; if (rc.IsFailure()) return rc;
@ -292,7 +315,6 @@ public static class PathNormalizer
return Result.Success; return Result.Success;
} }
/// <summary> /// <summary>
/// Checks if a path begins with / or \ and contains any of these patterns: /// Checks if a path begins with / or \ and contains any of these patterns:
/// "/..\", "\..\", "\../", "\..0" where '0' is the null terminator. /// "/..\", "\..\", "\../", "\..0" where '0' is the null terminator.

View File

@ -54,17 +54,18 @@ public class PathFormatterTests
{ @"\\?\c:\", "", @"", 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:\\host/share\aa\bb", "M", @"mount:", ResultFs.InvalidCharacter.Value }, { @"mount:\\host/share\aa\bb", "M", @"mount:", ResultFs.InvalidCharacter.Value },
{ @"c:\aa\..\..\..\bb", "W", @"c:/bb", Result.Success },
{ @"mount:/\\aa\..\bb", "MW", @"mount:", ResultFs.InvalidPathFormat.Value }, { @"mount:/\\aa\..\bb", "MW", @"mount:", ResultFs.InvalidPathFormat.Value },
{ @"mount:/c:\aa\..\bb", "MW", @"mount:c:/bb", Result.Success }, { @"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", Result.Success },
{ @"/mount:/aa/bb", "MW", @"/mount:/aa/bb", ResultFs.InvalidCharacter.Value }, { @"/mount:/aa/bb", "MW", @"/", ResultFs.InvalidCharacter.Value },
{ @"/mount:/aa/bb", "W", @"/mount:/aa/bb", ResultFs.InvalidCharacter.Value }, { @"/mount:/aa/bb", "W", @"/", ResultFs.InvalidCharacter.Value },
{ @"a:aa/../bb", "MW", @"a:aa/bb", Result.Success }, { @"a:aa/../bb", "MW", @"a:aa/bb", Result.Success },
{ @"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 }, { @"/a:aa\..\bb", "W", @"/", ResultFs.InvalidCharacter.Value },
{ @"\\?\c:\.\aa", "W", @"\\?\c:/aa", Result.Success }, { @"\\?\c:\.\aa", "W", @"\\?\c:/aa", Result.Success },
{ @"\\.\c:\.\aa", "W", @"\\.\c:/aa", Result.Success }, { @"\\.\c:\.\aa", "W", @"\\.\c:/aa", Result.Success },
{ @"\\.\mount:\.\aa", "W", @"\\./mount:/aa", ResultFs.InvalidCharacter.Value }, { @"\\.\mount:\.\aa", "W", @"\\./", ResultFs.InvalidCharacter.Value },
{ @"\\./.\aa", "W", @"\\./aa", Result.Success }, { @"\\./.\aa", "W", @"\\./aa", Result.Success },
{ @"\\/aa", "W", @"", ResultFs.InvalidPathFormat.Value }, { @"\\/aa", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\\aa", "W", @"", ResultFs.InvalidPathFormat.Value }, { @"\\\aa", "W", @"", ResultFs.InvalidPathFormat.Value },
@ -73,13 +74,13 @@ public class PathFormatterTests
{ @"\\host\share\path", "W", @"\\host\share/path", 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\share\path\aa\bb\..\cc\.", "W", @"\\host\share/path/aa/cc", Result.Success },
{ @"\\host\", "W", @"", ResultFs.InvalidPathFormat.Value }, { @"\\host\", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\ho$st\share\path", "W", @"", ResultFs.InvalidPathFormat.Value }, { @"\\ho$st\share\path", "W", @"", ResultFs.InvalidCharacter.Value },
{ @"\\host:\share\path", "W", @"", ResultFs.InvalidPathFormat.Value }, { @"\\host:\share\path", "W", @"", ResultFs.InvalidCharacter.Value },
{ @"\\..\share\path", "W", @"", ResultFs.InvalidPathFormat.Value }, { @"\\..\share\path", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\host\s:hare\path", "W", @"", ResultFs.InvalidPathFormat.Value }, { @"\\host\s:hare\path", "W", @"", ResultFs.InvalidCharacter.Value },
{ @"\\host\.\path", "W", @"", ResultFs.InvalidPathFormat.Value }, { @"\\host\.\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\sha:re", "W", @"", ResultFs.InvalidCharacter.Value },
{ @".\\host\share", "RW", @"..\\host\share/", Result.Success } { @".\\host\share", "RW", @"..\\host\share/", Result.Success }
}; };
@ -128,14 +129,46 @@ public class PathFormatterTests
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult); NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
} }
public static TheoryData<string, string, string, Result> TestData_Normalize_AllowAllChars => new()
{
{ @"/aa/b:b/cc", "", @"/aa/", ResultFs.InvalidCharacter.Value },
{ @"/aa/b*b/cc", "", @"/aa/", ResultFs.InvalidCharacter.Value },
{ @"/aa/b?b/cc", "", @"/aa/", ResultFs.InvalidCharacter.Value },
{ @"/aa/b<b/cc", "", @"/aa/", ResultFs.InvalidCharacter.Value },
{ @"/aa/b>b/cc", "", @"/aa/", ResultFs.InvalidCharacter.Value },
{ @"/aa/b|b/cc", "", @"/aa/", ResultFs.InvalidCharacter.Value },
{ @"/aa/b:b/cc", "C", @"/aa/b:b/cc", Result.Success },
{ @"/aa/b*b/cc", "C", @"/aa/b*b/cc", Result.Success },
{ @"/aa/b?b/cc", "C", @"/aa/b?b/cc", Result.Success },
{ @"/aa/b<b/cc", "C", @"/aa/b<b/cc", Result.Success },
{ @"/aa/b>b/cc", "C", @"/aa/b>b/cc", Result.Success },
{ @"/aa/b|b/cc", "C", @"/aa/b|b/cc", Result.Success },
{ @"/aa/b'b/cc", "", @"/aa/b'b/cc", Result.Success },
{ @"/aa/b""b/cc", "", @"/aa/b""b/cc", Result.Success },
{ @"/aa/b(b/cc", "", @"/aa/b(b/cc", Result.Success },
{ @"/aa/b)b/cc", "", @"/aa/b)b/cc", Result.Success },
{ @"/aa/b'b/cc", "C", @"/aa/b'b/cc", Result.Success },
{ @"/aa/b""b/cc", "C", @"/aa/b""b/cc", Result.Success },
{ @"/aa/b(b/cc", "C", @"/aa/b(b/cc", Result.Success },
{ @"/aa/b)b/cc", "C", @"/aa/b)b/cc", Result.Success },
{ @"mount:/aa/b<b/cc", "MC", @"mount:/aa/b<b/cc", Result.Success },
{ @"mo>unt:/aa/bb/cc", "MC", @"", ResultFs.InvalidCharacter.Value }
};
[Theory, MemberData(nameof(TestData_Normalize_AllowAllChars))]
public static void Normalize_AllowAllChars(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() public static TheoryData<string, string, string, Result> TestData_Normalize_All => new()
{ {
{ @"mount:./aa/bb", "WRM", @"mount:./aa/bb", Result.Success }, { @"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", "WRM", @"mount:./aa/bb/cc/dd", Result.Success },
{ @"mount:./aa/bb\cc/dd", "WRMB", @"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", "RM", @"mount:./", ResultFs.InvalidCharacter.Value },
{ @"mount:.c:/aa/bb", "WRM", @"mount:./.c:/aa/bb", ResultFs.InvalidCharacter.Value }, { @"mount:.c:/aa/bb", "WRM", @"mount:./", ResultFs.InvalidCharacter.Value },
{ @"mount:./cc:/aa/bb", "WRM", @"mount:./cc:/aa/bb", ResultFs.InvalidCharacter.Value }, { @"mount:./cc:/aa/bb", "WRM", @"mount:./", ResultFs.InvalidCharacter.Value },
{ @"mount:./\\host\share/aa/bb", "MW", @"mount:", ResultFs.InvalidPathFormat.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:..\\host\share/aa/bb", Result.Success }, { @"mount:.\\host\share/aa/bb", "WRM", @"mount:..\\host\share/aa/bb", Result.Success },
@ -147,7 +180,8 @@ public class PathFormatterTests
{ @"mount:/aa\bb", "BM", @"mount:/aa\bb", Result.Success }, { @"mount:/aa\bb", "BM", @"mount:/aa\bb", Result.Success },
{ @".//aa/bb", "RW", @"./aa/bb", Result.Success }, { @".//aa/bb", "RW", @"./aa/bb", Result.Success },
{ @"./aa/bb", "R", @"./aa/bb", Result.Success }, { @"./aa/bb", "R", @"./aa/bb", Result.Success },
{ @"./c:/aa/bb", "RW", @"./c:/aa/bb", ResultFs.InvalidCharacter.Value } { @"./c:/aa/bb", "RW", @"./", ResultFs.InvalidCharacter.Value },
{ @"mount:./aa/b:b\cc/dd", "WRMBC", @"mount:./aa/b:b/cc/dd", Result.Success }
}; };
[Theory, MemberData(nameof(TestData_Normalize_All))] [Theory, MemberData(nameof(TestData_Normalize_All))]
@ -219,7 +253,6 @@ public class PathFormatterTests
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_WindowsPath => new() 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 },
{ @"c:\aa\bb", "", false, 0, ResultFs.InvalidPathFormat.Value }, { @"c:\aa\bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\host\share", "", false, 0, ResultFs.InvalidPathFormat.Value }, { @"\\host\share", "", false, 0, ResultFs.InvalidPathFormat.Value },
@ -228,6 +261,7 @@ public class PathFormatterTests
{ @"\\?\c:\", "", 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:\\host/share\aa\bb", "M", false, 0, ResultFs.InvalidPathFormat.Value }, { @"mount:\\host/share\aa\bb", "M", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"c:\aa\..\..\..\bb", "W", false, 0, Result.Success },
{ @"mount:/\\aa\..\bb", "MW", false, 0, Result.Success }, { @"mount:/\\aa\..\bb", "MW", false, 0, Result.Success },
{ @"mount:/c:\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", true, 12, Result.Success },
@ -235,7 +269,7 @@ public class PathFormatterTests
{ @"/mount:/aa/bb", "W", 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, 8, Result.Success },
{ @"a:aa\..\bb", "MW", false, 0, Result.Success }, { @"a:aa\..\bb", "MW", false, 0, Result.Success },
{ @"/a:aa\..\bb", "W", false, 0, ResultFs.DirectoryUnobtainable.Value }, { @"/a:aa\..\bb", "W", false, 0, Result.Success },
{ @"\\?\c:\.\aa", "W", false, 0, Result.Success }, { @"\\?\c:\.\aa", "W", false, 0, Result.Success },
{ @"\\.\c:\.\aa", "W", false, 0, Result.Success }, { @"\\.\c:\.\aa", "W", false, 0, Result.Success },
{ @"\\.\mount:\.\aa", "W", false, 0, Result.Success }, { @"\\.\mount:\.\aa", "W", false, 0, Result.Success },
@ -247,13 +281,13 @@ public class PathFormatterTests
{ @"\\host\share\path", "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\share\path\aa\bb\..\cc\.", "W", false, 0, Result.Success },
{ @"\\host\", "W", false, 0, ResultFs.InvalidPathFormat.Value }, { @"\\host\", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\ho$st\share\path", "W", false, 0, ResultFs.InvalidPathFormat.Value }, { @"\\ho$st\share\path", "W", false, 0, ResultFs.InvalidCharacter.Value },
{ @"\\host:\share\path", "W", false, 0, ResultFs.InvalidPathFormat.Value }, { @"\\host:\share\path", "W", false, 0, ResultFs.InvalidCharacter.Value },
{ @"\\..\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\s:hare\path", "W", false, 0, ResultFs.InvalidCharacter.Value },
{ @"\\host\.\path", "W", false, 0, ResultFs.InvalidPathFormat.Value }, { @"\\host\.\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\sha:re", "W", false, 0, ResultFs.InvalidCharacter.Value },
{ @".\\host\share", "RW", false, 0, Result.Success } { @".\\host\share", "RW", false, 0, Result.Success }
}; };
@ -288,14 +322,14 @@ public class PathFormatterTests
{ {
{ @"\aa\bb\..\cc", "", false, 0, ResultFs.InvalidPathFormat.Value }, { @"\aa\bb\..\cc", "", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\aa\bb\..\cc", "B", 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", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa\bb\..\cc", "B", false, 0, ResultFs.DirectoryUnobtainable.Value }, { @"/aa\bb\..\cc", "B", false, 0, Result.Success },
{ @"/aa\bb\cc", "", false, 0, ResultFs.InvalidCharacter.Value }, { @"/aa\bb\cc", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa\bb\cc", "B", true, 9, Result.Success }, { @"/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", "W", false, 0, Result.Success },
{ @"\\host\share\path\aa\bb\cc", "WB", 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/..", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/bb\../cc/..\dd\..\ee/..", "B", false, 0, ResultFs.DirectoryUnobtainable.Value } { @"/aa/bb\../cc/..\dd\..\ee/..", "B", false, 0, Result.Success }
}; };
[Theory, MemberData(nameof(TestData_IsNormalized_Backslash))] [Theory, MemberData(nameof(TestData_IsNormalized_Backslash))]
@ -305,6 +339,39 @@ public class PathFormatterTests
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult); IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
} }
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_AllowAllChars => new()
{
{ @"/aa/b:b/cc", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/b*b/cc", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/b?b/cc", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/b<b/cc", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/b>b/cc", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/b|b/cc", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/b:b/cc", "C", true, 10, Result.Success },
{ @"/aa/b*b/cc", "C", true, 10, Result.Success },
{ @"/aa/b?b/cc", "C", true, 10, Result.Success },
{ @"/aa/b<b/cc", "C", true, 10, Result.Success },
{ @"/aa/b>b/cc", "C", true, 10, Result.Success },
{ @"/aa/b|b/cc", "C", true, 10, Result.Success },
{ @"/aa/b'b/cc", "", true, 10, Result.Success },
{ @"/aa/b""b/cc", "", true, 10, Result.Success },
{ @"/aa/b(b/cc", "", true, 10, Result.Success },
{ @"/aa/b)b/cc", "", true, 10, Result.Success },
{ @"/aa/b'b/cc", "C", true, 10, Result.Success },
{ @"/aa/b""b/cc", "C", true, 10, Result.Success },
{ @"/aa/b(b/cc", "C", true, 10, Result.Success },
{ @"/aa/b)b/cc", "C", true, 10, Result.Success },
{ @"mount:/aa/b<b/cc", "MC", true, 16, Result.Success },
{ @"mo>unt:/aa/bb/cc", "MC", false, 0, ResultFs.InvalidCharacter.Value }
};
[Theory, MemberData(nameof(TestData_IsNormalized_AllowAllChars))]
public static void IsNormalized_AllowAllChars(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() public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_All => new()
{ {
{ @"mount:./aa/bb", "WRM", true, 13, Result.Success }, { @"mount:./aa/bb", "WRM", true, 13, Result.Success },
@ -324,7 +391,8 @@ public class PathFormatterTests
{ @"mount:/aa\bb", "BM", true, 12, Result.Success }, { @"mount:/aa\bb", "BM", true, 12, Result.Success },
{ @".//aa/bb", "RW", false, 1, Result.Success }, { @".//aa/bb", "RW", false, 1, Result.Success },
{ @"./aa/bb", "R", true, 7, Result.Success }, { @"./aa/bb", "R", true, 7, Result.Success },
{ @"./c:/aa/bb", "RW", false, 0, ResultFs.InvalidCharacter.Value } { @"./c:/aa/bb", "RW", false, 0, ResultFs.InvalidCharacter.Value },
{ @"mount:./aa/b:b\cc/dd", "WRMBC", true, 20, Result.Success }
}; };
[Theory, MemberData(nameof(TestData_IsNormalized_All))] [Theory, MemberData(nameof(TestData_IsNormalized_All))]
@ -386,9 +454,14 @@ public class PathFormatterTests
case 'W': case 'W':
flags.AllowWindowsPath(); flags.AllowWindowsPath();
break; break;
case 'C':
flags.AllowAllCharacters();
break;
default:
throw new NotSupportedException();
} }
} }
return flags; return flags;
} }
} }

View File

@ -14,7 +14,7 @@ namespace nn::fs::detail {
bool IsEnabledAccessLog(); bool IsEnabledAccessLog();
} }
// SDK 12 // SDK 13
namespace nn::fs { namespace nn::fs {
bool IsSubPath(const char* path1, const char* path2); bool IsSubPath(const char* path1, const char* path2);
@ -29,12 +29,32 @@ namespace nn::fs {
void AllowEmptyPath() { value |= (1 << 2); } void AllowEmptyPath() { value |= (1 << 2); }
void AllowMountName() { value |= (1 << 3); } void AllowMountName() { value |= (1 << 3); }
void AllowBackslash() { value |= (1 << 4); } void AllowBackslash() { value |= (1 << 4); }
void AllowAllCharacters() { value |= (1 << 5); }
const bool IsWindowsPathAllowed() { return (value & (1 << 0)) != 0; } bool IsWindowsPathAllowed()const { return (value & (1 << 0)) != 0; }
const bool IsRelativePathAllowed() { return (value & (1 << 1)) != 0; } bool IsRelativePathAllowed()const { return (value & (1 << 1)) != 0; }
const bool IsEmptyPathAllowed() { return (value & (1 << 2)) != 0; } bool IsEmptyPathAllowed()const { return (value & (1 << 2)) != 0; }
const bool IsMountNameAllowed() { return (value & (1 << 3)) != 0; } bool IsMountNameAllowed()const { return (value & (1 << 3)) != 0; }
const bool IsBackslashAllowed() { return (value & (1 << 4)) != 0; } bool IsBackslashAllowed()const { return (value & (1 << 4)) != 0; }
bool AreAllCharactersAllowed()const { return (value & (1 << 5)) != 0; }
};
class Path {
public:
char* m_String;
char* m_WriteBuffer;
uint64_t m_UniquePtrLength;
uint64_t m_WriteBufferLength;
bool m_IsNormalized;
Path();
nn::Result Initialize(char const* path);
nn::Result InitializeWithNormalization(char const* path);
nn::Result InitializeWithReplaceUnc(char const* path);
nn::Result Initialize(char const* path, uint64_t pathLength);
nn::Result InsertParent(char const* path);
nn::Result RemoveChild();
nn::Result Normalize(const nn::fs::PathFlags&);
}; };
class PathFormatter { class PathFormatter {
@ -48,7 +68,9 @@ namespace nn::fs {
class PathNormalizer { class PathNormalizer {
public: public:
static nn::Result Normalize(char* outBuffer, uint64_t* outLength, const char* path, uint64_t outBufferLength, bool isWindowsPath, bool isDriveRelative); static nn::Result Normalize(char* outBuffer, uint64_t* outLength, const char* path, uint64_t outBufferLength, bool isWindowsPath, bool isDriveRelative);
static nn::Result Normalize(char* outBuffer, uint64_t* outLength, const char* path, uint64_t outBufferLength, bool isWindowsPath, bool isDriveRelative, bool allowAllCharacters);
static nn::Result IsNormalized(bool* outIsNormalized, uint64_t* outNormalizedPathLength, const char* path); static nn::Result IsNormalized(bool* outIsNormalized, uint64_t* outNormalizedPathLength, const char* path);
static nn::Result IsNormalized(bool* outIsNormalized, uint64_t* outNormalizedPathLength, const char* path, bool allowAllCharacters);
}; };
} }
@ -75,6 +97,7 @@ void CreateTest(const char* name, void (*func)(Ts...), const std::array<std::tup
const char* GetResultName(nn::Result result) { const char* GetResultName(nn::Result result) {
switch (result.GetValue()) { switch (result.GetValue()) {
case 0: return "Result.Success"; case 0: return "Result.Success";
case 0x177202: return "ResultFs.NotImplemented.Value";
case 0x2EE402: return "ResultFs.InvalidPath.Value"; case 0x2EE402: return "ResultFs.InvalidPath.Value";
case 0x2EE602: return "ResultFs.TooLongPath.Value"; case 0x2EE602: return "ResultFs.TooLongPath.Value";
case 0x2EE802: return "ResultFs.InvalidCharacter.Value"; case 0x2EE802: return "ResultFs.InvalidCharacter.Value";
@ -111,12 +134,33 @@ nn::fs::PathFlags GetPathFlags(char const* pathFlags) {
case 'W': case 'W':
flags.AllowWindowsPath(); flags.AllowWindowsPath();
break; break;
case 'C':
flags.AllowAllCharacters();
break;
} }
} }
return flags; return flags;
} }
// Escape single double-quotes to double double-quotes for C# verbatim string literals
void EscapeQuotes(char* dst, char const* src) {
while (*src) {
if (*src == '"')
*dst++ = '"';
*dst++ = *src++;
}
*dst = 0;
}
std::string GetEscaped(const char* s) {
char escaped[0x200] = { 0 };
EscapeQuotes(escaped, s);
return std::string(escaped);
}
static constexpr const auto TestData_PathFormatterNormalize_EmptyPath = make_array( static constexpr const auto TestData_PathFormatterNormalize_EmptyPath = make_array(
// Check AllowEmptyPath option // Check AllowEmptyPath option
std::make_tuple("", ""), std::make_tuple("", ""),
@ -150,6 +194,7 @@ static constexpr const auto TestData_PathFormatterNormalize_WindowsPath = make_a
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"), // 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:\\host/share\aa\bb)", "M"), // And do it again with the UNC path not normalized
std::make_tuple(R"(c:\aa\..\..\..\bb)", "W"), // Windows paths won't error when trying to navigate to the parent of the root directory
std::make_tuple(R"(mount:/\\aa\..\bb)", "MW"), std::make_tuple(R"(mount:/\\aa\..\bb)", "MW"),
std::make_tuple(R"(mount:/c:\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"),
@ -171,9 +216,9 @@ static constexpr const auto TestData_PathFormatterNormalize_WindowsPath = make_a
std::make_tuple(R"(\\host\)", "W"), // Share name cannot be empty 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"(\\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"(\\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"(\\..\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\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\..\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\sha:re)", "W"), // Invalid share name when nothing follows it
std::make_tuple(R"(.\\host\share)", "RW") // Can't have a relative Windows path std::make_tuple(R"(.\\host\share)", "RW") // Can't have a relative Windows path
@ -204,6 +249,33 @@ static constexpr const auto TestData_PathFormatterNormalize_Backslash = make_arr
std::make_tuple(R"(/aa/bb\../cc/..\dd\..\ee/..)", "B") std::make_tuple(R"(/aa/bb\../cc/..\dd\..\ee/..)", "B")
); );
static constexpr const auto TestData_PathFormatterNormalize_AllowAllChars = make_array(
std::make_tuple(R"(/aa/b:b/cc)", ""), // Test each of the characters that normally aren't allowed
std::make_tuple(R"(/aa/b*b/cc)", ""),
std::make_tuple(R"(/aa/b?b/cc)", ""),
std::make_tuple(R"(/aa/b<b/cc)", ""),
std::make_tuple(R"(/aa/b>b/cc)", ""),
std::make_tuple(R"(/aa/b|b/cc)", ""),
std::make_tuple(R"(/aa/b:b/cc)", "C"),
std::make_tuple(R"(/aa/b*b/cc)", "C"),
std::make_tuple(R"(/aa/b?b/cc)", "C"),
std::make_tuple(R"(/aa/b<b/cc)", "C"),
std::make_tuple(R"(/aa/b>b/cc)", "C"),
std::make_tuple(R"(/aa/b|b/cc)", "C"),
std::make_tuple(R"(/aa/b'b/cc)", ""), // Test some symbols that are normally allowed
std::make_tuple(R"(/aa/b"b/cc)", ""),
std::make_tuple(R"(/aa/b(b/cc)", ""),
std::make_tuple(R"(/aa/b)b/cc)", ""),
std::make_tuple(R"(/aa/b'b/cc)", "C"),
std::make_tuple(R"(/aa/b"b/cc)", "C"),
std::make_tuple(R"(/aa/b(b/cc)", "C"),
std::make_tuple(R"(/aa/b)b/cc)", "C"),
std::make_tuple(R"(mount:/aa/b<b/cc)", "MC"),
std::make_tuple(R"(mo>unt:/aa/bb/cc)", "MC") // Invalid character in mount name
);
static constexpr const auto TestData_PathFormatterNormalize_All = make_array( 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)", "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)", "WRM"), // Path with backslashes
@ -215,14 +287,15 @@ static constexpr const auto TestData_PathFormatterNormalize_All = make_array(
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"), // 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"(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"), // 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"(..\\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:\\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"), // Backslashes are never allowed directly after a mount name even with AllowBackslashes
std::make_tuple(R"(mount:/aa\bb)", "BM"), 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)", "RW"), // Relative path followed by a Windows path won't work
std::make_tuple(R"(./aa/bb)", "R"), std::make_tuple(R"(./aa/bb)", "R"),
std::make_tuple(R"(./c:/aa/bb)", "RW") std::make_tuple(R"(./c:/aa/bb)", "RW"),
std::make_tuple(R"(mount:./aa/b:b\cc/dd)", "WRMBC") // This path is considered normalized but the backslashes still normalize to forward slashes
); );
void CreateTest_PathFormatterNormalize(char const* path, char const* pathFlags) { void CreateTest_PathFormatterNormalize(char const* path, char const* pathFlags) {
@ -232,7 +305,7 @@ void CreateTest_PathFormatterNormalize(char const* path, char const* pathFlags)
nn::Result result = nn::fs::PathFormatter::Normalize(normalized, 0x200, path, 0x200, flags); nn::Result result = nn::fs::PathFormatter::Normalize(normalized, 0x200, path, 0x200, flags);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", @\"%s\", %s},\n", BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", @\"%s\", %s},\n",
path, pathFlags, normalized, GetResultName(result)); GetEscaped(path).c_str(), pathFlags, GetEscaped(normalized).c_str(), GetResultName(result));
} }
void CreateTest_PathFormatterIsNormalized(char const* path, char const* pathFlags) { void CreateTest_PathFormatterIsNormalized(char const* path, char const* pathFlags) {
@ -243,7 +316,7 @@ void CreateTest_PathFormatterIsNormalized(char const* path, char const* pathFlag
nn::Result result = nn::fs::PathFormatter::IsNormalized(&isNormalized, &normalizedLength, path, flags); nn::Result result = nn::fs::PathFormatter::IsNormalized(&isNormalized, &normalizedLength, path, flags);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", %s, %ld, %s},\n", BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", %s, %ld, %s},\n",
path, pathFlags, BoolStr(isNormalized), normalizedLength, GetResultName(result)); GetEscaped(path).c_str(), pathFlags, BoolStr(isNormalized), normalizedLength, GetResultName(result));
} }
static constexpr const auto TestData_PathFormatterNormalize_SmallBuffer = make_array( static constexpr const auto TestData_PathFormatterNormalize_SmallBuffer = make_array(
@ -259,68 +332,72 @@ void CreateTest_PathFormatterNormalize_SmallBuffer(char const* path, char const*
char normalized[0x200] = { 0 }; char normalized[0x200] = { 0 };
nn::fs::PathFlags flags = GetPathFlags(pathFlags); nn::fs::PathFlags flags = GetPathFlags(pathFlags);
svcOutputDebugString(path, strnlen(path, 0x200));
nn::Result result = nn::fs::PathFormatter::Normalize(normalized, bufferSize, path, 0x200, flags); nn::Result result = nn::fs::PathFormatter::Normalize(normalized, bufferSize, path, 0x200, flags);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", %d, @\"%s\", %s},\n", BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", %d, @\"%s\", %s},\n",
path, pathFlags, bufferSize, normalized, GetResultName(result)); GetEscaped(path).c_str(), pathFlags, bufferSize, GetEscaped(normalized).c_str(), GetResultName(result));
} }
static constexpr const auto TestData_PathNormalizerNormalize = make_array( static constexpr const auto TestData_PathNormalizerNormalize = make_array(
std::make_tuple("/aa/bb/c/", false, true), std::make_tuple("/aa/bb/c/", false, true, false),
std::make_tuple("aa/bb/c/", false, false), std::make_tuple("aa/bb/c/", false, false, false),
std::make_tuple("aa/bb/c/", false, true), std::make_tuple("aa/bb/c/", false, true, false),
std::make_tuple("mount:a/b", false, true), std::make_tuple("mount:a/b", false, true, false),
std::make_tuple("/aa/bb/../..", true, false), std::make_tuple("mo|unt:a/b", false, true, true),
std::make_tuple("/aa/bb/../../..", true, false), std::make_tuple("/aa/bb/../..", true, false, false), // Windows paths won't error when trying to navigate to the parent of the root directory
std::make_tuple("/aa/bb/../../..", false, false), std::make_tuple("/aa/bb/../../..", true, false, false),
std::make_tuple("aa/bb/../../..", true, true), std::make_tuple("/aa/bb/../../..", false, false, false),
std::make_tuple("aa/bb/../../..", false, true), std::make_tuple("aa/bb/../../..", true, true, false),
std::make_tuple("", false, false), std::make_tuple("aa/bb/../../..", false, true, false),
std::make_tuple("/", false, false), std::make_tuple("mount:a/b", false, true, true), // Test allowing invalid characters
std::make_tuple("/.", false, false), std::make_tuple("/a|/bb/cc", false, false, true),
std::make_tuple("/./", false, false), std::make_tuple("/>a/bb/cc", false, false, true),
std::make_tuple("/..", false, false), std::make_tuple("/aa/.</cc", false, false, true),
std::make_tuple("//.", false, false), std::make_tuple("/aa/..</cc", false, false, true),
std::make_tuple("/ ..", false, false), std::make_tuple("", false, false, false),
std::make_tuple("/.. /", false, false), std::make_tuple("/", false, false, false),
std::make_tuple("/. /.", false, false), std::make_tuple("/.", false, false, false),
std::make_tuple("/aa/bb/cc/dd/./.././../..", false, false), std::make_tuple("/./", false, false, false),
std::make_tuple("/aa/bb/cc/dd/./.././../../..", false, false), std::make_tuple("/..", false, false, false),
std::make_tuple("/./aa/./bb/./cc/./dd/.", false, false), std::make_tuple("//.", false, false, false),
std::make_tuple("/aa\\bb/cc", false, false), std::make_tuple("/ ..", false, false, false),
std::make_tuple("/aa\\bb/cc", false, false), std::make_tuple("/.. /", false, false, false),
std::make_tuple("/a|/bb/cc", false, false), std::make_tuple("/. /.", false, false, false),
std::make_tuple("/>a/bb/cc", false, false), std::make_tuple("/aa/bb/cc/dd/./.././../..", false, false, false),
std::make_tuple("/aa/.</cc", false, false), std::make_tuple("/aa/bb/cc/dd/./.././../../..", false, false, false),
std::make_tuple("/aa/..</cc", false, false), std::make_tuple("/./aa/./bb/./cc/./dd/.", false, false, false),
std::make_tuple("\\\\aa/bb/cc", false, false), std::make_tuple("/aa\\bb/cc", false, false, false),
std::make_tuple("\\\\aa\\bb\\cc", false, false), std::make_tuple("/aa\\bb/cc", false, false, false),
std::make_tuple("/aa/bb/..\\cc", false, false), std::make_tuple("/a|/bb/cc", false, false, false),
std::make_tuple("/aa/bb\\..\\cc", false, false), std::make_tuple("/>a/bb/cc", false, false, false),
std::make_tuple("/aa/bb\\..", false, false), std::make_tuple("/aa/.</cc", false, false, false),
std::make_tuple("/aa\\bb/../cc", false, false) std::make_tuple("/aa/..</cc", false, false, false),
std::make_tuple("\\\\aa/bb/cc", false, false, false),
std::make_tuple("\\\\aa\\bb\\cc", false, false, false),
std::make_tuple("/aa/bb/..\\cc", false, false, false),
std::make_tuple("/aa/bb\\..\\cc", false, false, false),
std::make_tuple("/aa/bb\\..", false, false, false),
std::make_tuple("/aa\\bb/../cc", false, false, false)
); );
void CreateTest_PathNormalizerNormalize(char const* path, bool isWindowsPath, bool isRelativePath) { void CreateTest_PathNormalizerNormalize(char const* path, bool isWindowsPath, bool isRelativePath, bool allowAllCharacters) {
char normalized[0x200] = { 0 }; char normalized[0x200] = { 0 };
uint64_t normalizedLength = 0; uint64_t normalizedLength = 0;
nn::Result result = nn::fs::PathNormalizer::Normalize(normalized, &normalizedLength, path, 0x200, isWindowsPath, isRelativePath); nn::Result result = nn::fs::PathNormalizer::Normalize(normalized, &normalizedLength, path, 0x200, isWindowsPath, isRelativePath, allowAllCharacters);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %s, @\"%s\", %ld, %s},\n", BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %s, %s, @\"%s\", %ld, %s},\n",
path, BoolStr(isWindowsPath), BoolStr(isRelativePath), normalized, normalizedLength, GetResultName(result)); GetEscaped(path).c_str(), BoolStr(isWindowsPath), BoolStr(isRelativePath), BoolStr(allowAllCharacters), GetEscaped(normalized).c_str(), normalizedLength, GetResultName(result));
} }
void CreateTest_PathNormalizerIsNormalized(char const* path, bool isWindowsPath, bool isRelativePath) { void CreateTest_PathNormalizerIsNormalized(char const* path, bool isWindowsPath, bool isRelativePath, bool allowAllCharacters) {
bool isNormalized = false; bool isNormalized = false;
uint64_t normalizedLength = 0; uint64_t normalizedLength = 0;
nn::Result result = nn::fs::PathNormalizer::IsNormalized(&isNormalized, &normalizedLength, path); nn::Result result = nn::fs::PathNormalizer::IsNormalized(&isNormalized, &normalizedLength, path, allowAllCharacters);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %ld, %s},\n", BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %s, %ld, %s},\n",
path, BoolStr(isNormalized), normalizedLength, GetResultName(result)); GetEscaped(path).c_str(), BoolStr(allowAllCharacters), BoolStr(isNormalized), normalizedLength, GetResultName(result));
} }
static constexpr const auto TestData_PathNormalizerNormalize_SmallBuffer = make_array( static constexpr const auto TestData_PathNormalizerNormalize_SmallBuffer = make_array(
@ -350,7 +427,7 @@ void CreateTest_PathNormalizerNormalize_SmallBuffer(char const* path, int buffer
nn::Result result = nn::fs::PathNormalizer::Normalize(normalized, &normalizedLength, path, bufferSize, false, false); nn::Result result = nn::fs::PathNormalizer::Normalize(normalized, &normalizedLength, path, bufferSize, false, false);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %d, @\"%s\", %ld, %s},\n", BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %d, @\"%s\", %ld, %s},\n",
path, bufferSize, normalized, normalizedLength, GetResultName(result)); GetEscaped(path).c_str(), bufferSize, GetEscaped(normalized).c_str(), normalizedLength, GetResultName(result));
} }
static constexpr const auto TestData_PathUtility_IsSubPath = make_array( static constexpr const auto TestData_PathUtility_IsSubPath = make_array(
@ -377,9 +454,62 @@ void CreateTest_PathUtility_IsSubPath(const char* path1, const char* path2) {
bool result = nn::fs::IsSubPath(path1, path2); bool result = nn::fs::IsSubPath(path1, path2);
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", @\"%s\", %s},\n", BufPos += sprintf(&Buf[BufPos], "{@\"%s\", @\"%s\", %s},\n",
path1, path2, BoolStr(result)); GetEscaped(path1).c_str(), GetEscaped(path2).c_str(), BoolStr(result));
} }
void RunTest_Path_RemoveChild() {
auto path = nn::fs::Path();
nn::Result result = path.InitializeWithReplaceUnc("/aa/bb/./cc");
BufPos += sprintf(&Buf[BufPos], "%s\n", GetResultName(result));
BufPos += sprintf(&Buf[BufPos], "%s\n", path.m_String);
result = path.InitializeWithReplaceUnc("//aa/bb");
BufPos += sprintf(&Buf[BufPos], "%s\n", GetResultName(result));
BufPos += sprintf(&Buf[BufPos], "%s\n", path.m_String);
result = path.InitializeWithReplaceUnc("@Host://aa/bb");
BufPos += sprintf(&Buf[BufPos], "%s\n", GetResultName(result));
BufPos += sprintf(&Buf[BufPos], "%s\n", path.m_String);
result = path.InitializeWithReplaceUnc("mount:///aa/bb");
BufPos += sprintf(&Buf[BufPos], "%s\n", GetResultName(result));
BufPos += sprintf(&Buf[BufPos], "%s\n", path.m_String);
result = path.InitializeWithReplaceUnc("//mount:///aa/bb");
BufPos += sprintf(&Buf[BufPos], "%s\n", GetResultName(result));
BufPos += sprintf(&Buf[BufPos], "%s\n", path.m_String);
svcOutputDebugString(Buf, BufPos);
};
void RunTest_Path_InsertParent() {
auto path = nn::fs::Path();
nn::Result result1 = path.Initialize("/cc/dd");
nn::Result result2 = path.InsertParent("/aa/bb");
BufPos += sprintf(&Buf[BufPos], "%s\n%s\n%s\n", GetResultName(result1), GetResultName(result2), path.m_String);
result1 = path.Initialize("/cc/dd");
result2 = path.InsertParent("aa/bb");
BufPos += sprintf(&Buf[BufPos], "%s\n%s\n%s\n", GetResultName(result1), GetResultName(result2), path.m_String);
result1 = path.Initialize("/cc/dd/");
result2 = path.InsertParent("aa/bb");
BufPos += sprintf(&Buf[BufPos], "%s\n%s\n%s\n", GetResultName(result1), GetResultName(result2), path.m_String);
result1 = path.Initialize("/cc/dd/");
result2 = path.InsertParent("/aa/bb");
BufPos += sprintf(&Buf[BufPos], "%s\n%s\n%s\n", GetResultName(result1), GetResultName(result2), path.m_String);
result1 = path.Initialize("/cc/dd/");
result2 = path.Normalize(GetPathFlags(""));
BufPos += sprintf(&Buf[BufPos], "%s\n%s\n%s\n", GetResultName(result1), GetResultName(result2), BoolStr(path.m_IsNormalized));
result2 = path.InsertParent("/aa/../bb");
BufPos += sprintf(&Buf[BufPos], "%s\n%s\n%s\n", GetResultName(result2), BoolStr(path.m_IsNormalized), path.m_String);
svcOutputDebugString(Buf, BufPos);
};
extern "C" void nnMain(void) { extern "C" void nnMain(void) {
// nn::fs::detail::IsEnabledAccessLog(); // Adds the sdk version to the output // nn::fs::detail::IsEnabledAccessLog(); // Adds the sdk version to the output
@ -388,6 +518,7 @@ extern "C" void nnMain(void) {
CreateTest("TestData_PathFormatter_Normalize_WindowsPath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_WindowsPath); CreateTest("TestData_PathFormatter_Normalize_WindowsPath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_WindowsPath);
CreateTest("TestData_PathFormatter_Normalize_RelativePath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_RelativePath); CreateTest("TestData_PathFormatter_Normalize_RelativePath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_RelativePath);
CreateTest("TestData_PathFormatter_Normalize_Backslash", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_Backslash); CreateTest("TestData_PathFormatter_Normalize_Backslash", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_Backslash);
CreateTest("TestData_PathFormatter_Normalize_AllowAllChars", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_AllowAllChars);
CreateTest("TestData_PathFormatter_Normalize_All", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_All); CreateTest("TestData_PathFormatter_Normalize_All", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_All);
CreateTest("TestData_PathFormatter_Normalize_SmallBuffer", CreateTest_PathFormatterNormalize_SmallBuffer, TestData_PathFormatterNormalize_SmallBuffer); CreateTest("TestData_PathFormatter_Normalize_SmallBuffer", CreateTest_PathFormatterNormalize_SmallBuffer, TestData_PathFormatterNormalize_SmallBuffer);
@ -396,6 +527,7 @@ extern "C" void nnMain(void) {
CreateTest("TestData_PathFormatter_IsNormalized_WindowsPath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_WindowsPath); CreateTest("TestData_PathFormatter_IsNormalized_WindowsPath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_WindowsPath);
CreateTest("TestData_PathFormatter_IsNormalized_RelativePath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_RelativePath); CreateTest("TestData_PathFormatter_IsNormalized_RelativePath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_RelativePath);
CreateTest("TestData_PathFormatter_IsNormalized_Backslash", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_Backslash); CreateTest("TestData_PathFormatter_IsNormalized_Backslash", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_Backslash);
CreateTest("TestData_PathFormatter_IsNormalized_AllowAllChars", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_AllowAllChars);
CreateTest("TestData_PathFormatter_IsNormalized_All", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_All); CreateTest("TestData_PathFormatter_IsNormalized_All", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_All);
CreateTest("TestData_PathNormalizer_Normalize", CreateTest_PathNormalizerNormalize, TestData_PathNormalizerNormalize); CreateTest("TestData_PathNormalizer_Normalize", CreateTest_PathNormalizerNormalize, TestData_PathNormalizerNormalize);

View File

@ -9,51 +9,57 @@ namespace LibHac.Tests.Fs;
public class PathNormalizerTests public class PathNormalizerTests
{ {
public static TheoryData<string, bool, bool, string, long, Result> TestData_Normalize => new() public static TheoryData<string, bool, bool, bool, string, long, Result> TestData_Normalize => new()
{ {
{ @"/aa/bb/c/", false, true, @"/aa/bb/c", 8, Result.Success }, { @"/aa/bb/c/", false, true, false, @"/aa/bb/c", 8, Result.Success },
{ @"aa/bb/c/", false, false, @"", 0, ResultFs.InvalidPathFormat.Value }, { @"aa/bb/c/", false, false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
{ @"aa/bb/c/", false, true, @"/aa/bb/c", 8, Result.Success }, { @"aa/bb/c/", false, true, false, @"/aa/bb/c", 8, Result.Success },
{ @"mount:a/b", false, true, @"/mount:a/b", 0, ResultFs.InvalidCharacter.Value }, { @"mount:a/b", false, true, false, @"/", 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/bb/../..", true, false, @"/", 1, Result.Success }, { @"mo|unt:a/b", false, true, true, @"/mo|unt:a/b", 11, Result.Success },
{ @"/aa/bb/../../..", true, false, @"/", 1, Result.Success }, { @"/aa/bb/../..", true, false, false, @"/", 1, Result.Success },
{ @"/aa/bb/../../..", false, false, @"/aa/bb/", 0, ResultFs.DirectoryUnobtainable.Value }, { @"/aa/bb/../../..", true, false, false, @"/", 1, Result.Success },
{ @"aa/bb/../../..", true, true, @"/", 1, Result.Success }, { @"/aa/bb/../../..", false, false, false, @"/aa/bb/", 0, ResultFs.DirectoryUnobtainable.Value },
{ @"aa/bb/../../..", false, true, @"/aa/bb/", 0, ResultFs.DirectoryUnobtainable.Value }, { @"aa/bb/../../..", true, true, false, @"/", 1, Result.Success },
{ @"", false, false, @"", 0, ResultFs.InvalidPathFormat.Value }, { @"aa/bb/../../..", false, true, false, @"/aa/bb/", 0, ResultFs.DirectoryUnobtainable.Value },
{ @"/", false, false, @"/", 1, Result.Success }, { @"mount:a/b", false, true, true, @"/mount:a/b", 10, Result.Success },
{ @"/.", false, false, @"/", 1, Result.Success }, { @"/a|/bb/cc", false, false, true, @"/a|/bb/cc", 9, Result.Success },
{ @"/./", false, false, @"/", 1, Result.Success }, { @"/>a/bb/cc", false, false, true, @"/>a/bb/cc", 9, Result.Success },
{ @"/..", false, false, @"/", 0, ResultFs.DirectoryUnobtainable.Value }, { @"/aa/.</cc", false, false, true, @"/aa/.</cc", 9, Result.Success },
{ @"//.", false, false, @"/", 1, Result.Success }, { @"/aa/..</cc", false, false, true, @"/aa/..</cc", 10, Result.Success },
{ @"/ ..", false, false, @"/ ..", 4, Result.Success }, { @"", false, false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
{ @"/.. /", false, false, @"/.. ", 4, Result.Success }, { @"/", false, false, false, @"/", 1, Result.Success },
{ @"/. /.", false, false, @"/. ", 3, Result.Success }, { @"/.", false, false, false, @"/", 1, Result.Success },
{ @"/aa/bb/cc/dd/./.././../..", false, false, @"/aa", 3, Result.Success }, { @"/./", false, false, false, @"/", 1, Result.Success },
{ @"/aa/bb/cc/dd/./.././../../..", false, false, @"/", 1, Result.Success }, { @"/..", false, false, false, @"/", 0, ResultFs.DirectoryUnobtainable.Value },
{ @"/./aa/./bb/./cc/./dd/.", false, false, @"/aa/bb/cc/dd", 12, Result.Success }, { @"//.", false, false, false, @"/", 1, Result.Success },
{ @"/aa\bb/cc", false, false, @"/aa\bb/cc", 9, Result.Success }, { @"/ ..", false, false, false, @"/ ..", 4, Result.Success },
{ @"/aa\bb/cc", false, false, @"/aa\bb/cc", 9, Result.Success }, { @"/.. /", false, false, false, @"/.. ", 4, Result.Success },
{ @"/a|/bb/cc", false, false, @"/a|/bb/cc", 0, ResultFs.InvalidCharacter.Value }, { @"/. /.", false, false, false, @"/. ", 3, Result.Success },
{ @"/>a/bb/cc", false, false, @"/>a/bb/cc", 0, ResultFs.InvalidCharacter.Value }, { @"/aa/bb/cc/dd/./.././../..", false, false, false, @"/aa", 3, Result.Success },
{ @"/aa/.</cc", false, false, @"/aa/.</cc", 0, ResultFs.InvalidCharacter.Value }, { @"/aa/bb/cc/dd/./.././../../..", false, false, false, @"/", 1, Result.Success },
{ @"/aa/..</cc", false, false, @"/aa/..</cc", 0, ResultFs.InvalidCharacter.Value }, { @"/./aa/./bb/./cc/./dd/.", false, false, false, @"/aa/bb/cc/dd", 12, Result.Success },
{ @"\\aa/bb/cc", false, false, @"", 0, ResultFs.InvalidPathFormat.Value }, { @"/aa\bb/cc", false, false, false, @"/aa\bb/cc", 9, Result.Success },
{ @"\\aa\bb\cc", false, false, @"", 0, ResultFs.InvalidPathFormat.Value }, { @"/aa\bb/cc", false, false, false, @"/aa\bb/cc", 9, Result.Success },
{ @"/aa/bb/..\cc", false, false, @"/aa/cc", 6, Result.Success }, { @"/a|/bb/cc", false, false, false, @"/", 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/bb\..\cc", false, false, @"/aa/cc", 6, Result.Success }, { @"/>a/bb/cc", false, false, false, @"/", 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/bb\..", false, false, @"/aa", 3, Result.Success }, { @"/aa/.</cc", false, false, false, @"/aa/", 0, ResultFs.InvalidCharacter.Value },
{ @"/aa\bb/../cc", false, false, @"/cc", 3, Result.Success } { @"/aa/..</cc", false, false, false, @"/aa/", 0, ResultFs.InvalidCharacter.Value },
{ @"\\aa/bb/cc", false, false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
{ @"\\aa\bb\cc", false, false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
{ @"/aa/bb/..\cc", false, false, false, @"/aa/cc", 6, Result.Success },
{ @"/aa/bb\..\cc", false, false, false, @"/aa/cc", 6, Result.Success },
{ @"/aa/bb\..", false, false, false, @"/aa", 3, Result.Success },
{ @"/aa\bb/../cc", false, false, false, @"/cc", 3, Result.Success }
}; };
[Theory, MemberData(nameof(TestData_Normalize))] [Theory, MemberData(nameof(TestData_Normalize))]
public static void Normalize(string path, bool isWindowsPath, bool isDriveRelativePath, string expectedNormalized, public static void Normalize(string path, bool isWindowsPath, bool isDriveRelativePath, bool allowAllCharacters,
long expectedLength, Result expectedResult) string expectedNormalized, long expectedLength, Result expectedResult)
{ {
byte[] buffer = new byte[0x301]; byte[] buffer = new byte[0x301];
Result result = PathNormalizer.Normalize(buffer, out int normalizedLength, path.ToU8Span(), isWindowsPath, Result result = PathNormalizer.Normalize(buffer, out int normalizedLength, path.ToU8Span(), isWindowsPath,
isDriveRelativePath); isDriveRelativePath, allowAllCharacters);
Assert.Equal(expectedResult, result); Assert.Equal(expectedResult, result);
Assert.Equal(expectedNormalized, StringUtils.Utf8ZToString(buffer)); Assert.Equal(expectedNormalized, StringUtils.Utf8ZToString(buffer));
@ -93,47 +99,53 @@ public class PathNormalizerTests
Assert.Equal(expectedLength, normalizedLength); Assert.Equal(expectedLength, normalizedLength);
} }
public static TheoryData<string, bool, long, Result> TestData_IsNormalized => new() public static TheoryData<string, bool, bool, long, Result> TestData_IsNormalized => new()
{ {
{ @"/aa/bb/c/", false, 9, Result.Success }, { @"/aa/bb/c/", false, false, 9, Result.Success },
{ @"aa/bb/c/", false, 0, ResultFs.InvalidPathFormat.Value }, { @"aa/bb/c/", false, false, 0, ResultFs.InvalidPathFormat.Value },
{ @"aa/bb/c/", false, 0, ResultFs.InvalidPathFormat.Value }, { @"aa/bb/c/", false, false, 0, ResultFs.InvalidPathFormat.Value },
{ @"mount:a/b", false, 0, ResultFs.InvalidPathFormat.Value }, { @"mount:a/b", false, false, 0, ResultFs.InvalidPathFormat.Value },
{ @"/aa/bb/../..", false, 0, Result.Success }, { @"mo|unt:a/b", true, false, 0, ResultFs.InvalidPathFormat.Value },
{ @"/aa/bb/../../..", false, 0, Result.Success }, { @"/aa/bb/../..", false, false, 0, Result.Success },
{ @"/aa/bb/../../..", false, 0, Result.Success }, { @"/aa/bb/../../..", false, false, 0, Result.Success },
{ @"aa/bb/../../..", false, 0, ResultFs.InvalidPathFormat.Value }, { @"/aa/bb/../../..", false, false, 0, Result.Success },
{ @"aa/bb/../../..", false, 0, ResultFs.InvalidPathFormat.Value }, { @"aa/bb/../../..", false, false, 0, ResultFs.InvalidPathFormat.Value },
{ @"", false, 0, ResultFs.InvalidPathFormat.Value }, { @"aa/bb/../../..", false, false, 0, ResultFs.InvalidPathFormat.Value },
{ @"/", true, 1, Result.Success }, { @"mount:a/b", true, false, 0, ResultFs.InvalidPathFormat.Value },
{ @"/.", false, 2, Result.Success }, { @"/a|/bb/cc", true, true, 9, Result.Success },
{ @"/./", false, 0, Result.Success }, { @"/>a/bb/cc", true, true, 9, Result.Success },
{ @"/..", false, 3, Result.Success }, { @"/aa/.</cc", true, true, 9, Result.Success },
{ @"//.", false, 0, Result.Success }, { @"/aa/..</cc", true, true, 10, Result.Success },
{ @"/ ..", true, 4, Result.Success }, { @"", false, false, 0, ResultFs.InvalidPathFormat.Value },
{ @"/.. /", false, 5, Result.Success }, { @"/", false, true, 1, Result.Success },
{ @"/. /.", false, 5, Result.Success }, { @"/.", false, false, 2, Result.Success },
{ @"/aa/bb/cc/dd/./.././../..", false, 0, Result.Success }, { @"/./", false, false, 0, Result.Success },
{ @"/aa/bb/cc/dd/./.././../../..", false, 0, Result.Success }, { @"/..", false, false, 3, Result.Success },
{ @"/./aa/./bb/./cc/./dd/.", false, 0, Result.Success }, { @"//.", false, false, 0, Result.Success },
{ @"/aa\bb/cc", true, 9, Result.Success }, { @"/ ..", false, true, 4, Result.Success },
{ @"/aa\bb/cc", true, 9, Result.Success }, { @"/.. /", false, false, 5, Result.Success },
{ @"/a|/bb/cc", false, 0, ResultFs.InvalidCharacter.Value }, { @"/. /.", false, false, 5, Result.Success },
{ @"/>a/bb/cc", false, 0, ResultFs.InvalidCharacter.Value }, { @"/aa/bb/cc/dd/./.././../..", false, false, 0, Result.Success },
{ @"/aa/.</cc", false, 0, ResultFs.InvalidCharacter.Value }, { @"/aa/bb/cc/dd/./.././../../..", false, false, 0, Result.Success },
{ @"/aa/..</cc", false, 0, ResultFs.InvalidCharacter.Value }, { @"/./aa/./bb/./cc/./dd/.", false, false, 0, Result.Success },
{ @"\\aa/bb/cc", false, 0, ResultFs.InvalidPathFormat.Value }, { @"/aa\bb/cc", false, true, 9, Result.Success },
{ @"\\aa\bb\cc", false, 0, ResultFs.InvalidPathFormat.Value }, { @"/aa\bb/cc", false, true, 9, Result.Success },
{ @"/aa/bb/..\cc", true, 12, Result.Success }, { @"/a|/bb/cc", false, false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/bb\..\cc", true, 12, Result.Success }, { @"/>a/bb/cc", false, false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/bb\..", true, 9, Result.Success }, { @"/aa/.</cc", false, false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa\bb/../cc", false, 0, Result.Success } { @"/aa/..</cc", false, false, 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, true, 12, Result.Success },
{ @"/aa/bb\..\cc", false, true, 12, Result.Success },
{ @"/aa/bb\..", false, true, 9, Result.Success },
{ @"/aa\bb/../cc", false, false, 0, Result.Success }
}; };
[Theory, MemberData(nameof(TestData_IsNormalized))] [Theory, MemberData(nameof(TestData_IsNormalized))]
public static void IsNormalized(string path, bool expectedIsNormalized, long expectedLength, Result expectedResult) public static void IsNormalized(string path, bool allowAllCharacters, bool expectedIsNormalized, long expectedLength, Result expectedResult)
{ {
Result result = PathNormalizer.IsNormalized(out bool isNormalized, out int length, path.ToU8Span()); Result result = PathNormalizer.IsNormalized(out bool isNormalized, out int length, path.ToU8Span(), allowAllCharacters);
Assert.Equal(expectedResult, result); Assert.Equal(expectedResult, result);
Assert.Equal(expectedLength, length); Assert.Equal(expectedLength, length);
@ -143,4 +155,4 @@ public class PathNormalizerTests
Assert.Equal(expectedIsNormalized, isNormalized); Assert.Equal(expectedIsNormalized, isNormalized);
} }
} }
} }