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 AllowMountName() => _value |= 1 << 3;
public void AllowBackslash() => _value |= 1 << 4;
public void AllowAllCharacters() => _value |= 1 << 5;
public bool IsWindowsPathAllowed() => (_value & (1 << 0)) != 0;
public bool IsRelativePathAllowed() => (_value & (1 << 1)) != 0;
public bool IsEmptyPathAllowed() => (_value & (1 << 2)) != 0;
public bool IsMountNameAllowed() => (_value & (1 << 3)) != 0;
public bool IsBackslashAllowed() => (_value & (1 << 4)) != 0;
public bool AreAllCharactersAllowed() => (_value & (1 << 5)) != 0;
}
/// <summary>

View File

@ -13,9 +13,19 @@ namespace LibHac.Fs;
/// <summary>
/// Contains functions for working with path formatting and normalization.
/// </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
{
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)]
private static Result CheckHostName(ReadOnlySpan<byte> name)
{
@ -24,8 +34,11 @@ public static class PathFormatter
for (int i = 0; i < name.Length; i++)
{
if (name[i] == ':' || name[i] == '$')
return ResultFs.InvalidPathFormat.Log();
foreach (byte c in InvalidCharacterForHostName)
{
if (name[i] == c)
return ResultFs.InvalidCharacter.Log();
}
}
return Result.Success;
@ -41,8 +54,11 @@ public static class PathFormatter
for (int i = 0; i < name.Length; i++)
{
if (name[i] == ':')
return ResultFs.InvalidPathFormat.Log();
foreach (byte c in InvalidCharacter)
{
if (name[i] == c)
return ResultFs.InvalidCharacter.Log();
}
}
return Result.Success;
@ -90,8 +106,11 @@ public static class PathFormatter
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();
foreach (byte c in InvalidCharacterForMountName)
{
if (path.At(i) == c)
return ResultFs.InvalidCharacter.Log();
}
}
if (!outMountNameBuffer.IsEmpty)
@ -150,6 +169,12 @@ public static class PathFormatter
int 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 ||
currentPath[winPathLength] == AltDirectorySeparator)
{
@ -478,8 +503,11 @@ public static class PathFormatter
}
}
if (PathNormalizer.IsParentDirectoryPathReplacementNeeded(buffer))
return ResultFs.DirectoryUnobtainable.Log();
if (flags.IsBackslashAllowed() && PathNormalizer.IsParentDirectoryPathReplacementNeeded(buffer))
{
isNormalized = false;
return Result.Success;
}
rc = PathUtility.CheckInvalidBackslash(out bool isBackslashContained, buffer,
flags.IsWindowsPathAllowed() || flags.IsBackslashAllowed());
@ -491,7 +519,7 @@ public static class PathFormatter
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;
totalLength += length;
@ -612,7 +640,8 @@ public static class PathFormatter
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;
return Result.Success;

View File

@ -10,7 +10,7 @@ namespace LibHac.Fs;
/// <summary>
/// Contains functions for doing with basic path normalization.
/// </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
{
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,
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);
@ -32,7 +38,7 @@ public static class PathNormalizer
int totalLength = 0;
int i = 0;
if (!IsSeparator(path.At(0)))
if (path.At(0) != DirectorySeparator)
{
if (!isDriveRelativePath)
return ResultFs.InvalidPathFormat.Log();
@ -43,6 +49,7 @@ public static class PathNormalizer
var convertedPath = new RentedArray<byte>();
try
{
Result rc;
// Check if parent directory path replacement is needed.
if (IsParentDirectoryPathReplacementNeeded(currentPath))
{
@ -58,20 +65,22 @@ public static class PathNormalizer
bool skipNextSeparator = false;
while (!IsNul(currentPath.At(i)))
while (currentPath.At(i) != NullTerminator)
{
if (IsSeparator(currentPath[i]))
if (currentPath[i] == DirectorySeparator)
{
do
{
i++;
} while (IsSeparator(currentPath.At(i)));
} while (currentPath.At(i) == DirectorySeparator);
if (IsNul(currentPath.At(i)))
if (currentPath.At(i) == NullTerminator)
break;
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)
{
outputBuffer[totalLength] = NullTerminator;
@ -87,8 +96,14 @@ public static class PathNormalizer
}
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++;
}
@ -163,12 +178,14 @@ public static class PathNormalizer
}
// 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();
outputBuffer[totalLength] = NullTerminator;
Result rc = IsNormalized(out bool isNormalized, out _, outputBuffer);
rc = IsNormalized(out bool isNormalized, out _, outputBuffer, allowAllCharacters);
if (rc.IsFailure()) return rc;
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.InvalidPathFormat"/>: The path is not in a valid format.</returns>
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);
@ -213,7 +236,7 @@ public static class PathNormalizer
pathLength++;
if (state != PathState.Initial)
if (!allowAllCharacters && state != PathState.Initial)
{
Result rc = CheckInvalidCharacter(c);
if (rc.IsFailure()) return rc;
@ -292,7 +315,6 @@ public static class PathNormalizer
return Result.Success;
}
/// <summary>
/// Checks if a path begins with / or \ and contains any of these patterns:
/// "/..\", "\..\", "\../", "\..0" where '0' is the null terminator.

View File

@ -54,17 +54,18 @@ public class PathFormatterTests
{ @"\\?\c:\", "", @"", 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:/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 },
{ @"/mount:/aa/bb", "MW", @"/", 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", "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 },
{ @"\\.\mount:\.\aa", "W", @"\\./mount:/aa", ResultFs.InvalidCharacter.Value },
{ @"\\.\mount:\.\aa", "W", @"\\./", ResultFs.InvalidCharacter.Value },
{ @"\\./.\aa", "W", @"\\./aa", Result.Success },
{ @"\\/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\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 },
{ @"\\ho$st\share\path", "W", @"", ResultFs.InvalidCharacter.Value },
{ @"\\host:\share\path", "W", @"", ResultFs.InvalidCharacter.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\sha:re", "W", @"", ResultFs.InvalidPathFormat.Value },
{ @"\\host\sha:re", "W", @"", ResultFs.InvalidCharacter.Value },
{ @".\\host\share", "RW", @"..\\host\share/", Result.Success }
};
@ -128,14 +129,46 @@ public class PathFormatterTests
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()
{
{ @"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:./.c:/aa/bb", "RM", @"mount:./", ResultFs.InvalidCharacter.Value },
{ @"mount:.c:/aa/bb", "WRM", @"mount:./", 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", "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 },
{ @".//aa/bb", "RW", @"./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))]
@ -219,7 +253,6 @@ public class PathFormatterTests
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 },
@ -228,6 +261,7 @@ public class PathFormatterTests
{ @"\\?\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 },
{ @"c:\aa\..\..\..\bb", "W", false, 0, Result.Success },
{ @"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 },
@ -235,7 +269,7 @@ public class PathFormatterTests
{ @"/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 },
{ @"/a:aa\..\bb", "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 },
@ -247,13 +281,13 @@ public class PathFormatterTests
{ @"\\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 },
{ @"\\ho$st\share\path", "W", false, 0, ResultFs.InvalidCharacter.Value },
{ @"\\host:\share\path", "W", false, 0, ResultFs.InvalidCharacter.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\sha:re", "W", false, 0, ResultFs.InvalidPathFormat.Value },
{ @"\\host\sha:re", "W", false, 0, ResultFs.InvalidCharacter.Value },
{ @".\\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", "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", false, 0, Result.Success },
{ @"/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 }
{ @"/aa/bb\../cc/..\dd\..\ee/..", "", false, 0, ResultFs.InvalidCharacter.Value },
{ @"/aa/bb\../cc/..\dd\..\ee/..", "B", false, 0, Result.Success }
};
[Theory, MemberData(nameof(TestData_IsNormalized_Backslash))]
@ -305,6 +339,39 @@ public class PathFormatterTests
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()
{
{ @"mount:./aa/bb", "WRM", true, 13, Result.Success },
@ -324,7 +391,8 @@ public class PathFormatterTests
{ @"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 }
{ @"./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))]
@ -386,9 +454,14 @@ public class PathFormatterTests
case 'W':
flags.AllowWindowsPath();
break;
case 'C':
flags.AllowAllCharacters();
break;
default:
throw new NotSupportedException();
}
}
return flags;
}
}
}

View File

@ -14,7 +14,7 @@ namespace nn::fs::detail {
bool IsEnabledAccessLog();
}
// SDK 12
// SDK 13
namespace nn::fs {
bool IsSubPath(const char* path1, const char* path2);
@ -29,12 +29,32 @@ namespace nn::fs {
void AllowEmptyPath() { value |= (1 << 2); }
void AllowMountName() { value |= (1 << 3); }
void AllowBackslash() { value |= (1 << 4); }
void AllowAllCharacters() { value |= (1 << 5); }
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; }
bool IsWindowsPathAllowed()const { return (value & (1 << 0)) != 0; }
bool IsRelativePathAllowed()const { return (value & (1 << 1)) != 0; }
bool IsEmptyPathAllowed()const { return (value & (1 << 2)) != 0; }
bool IsMountNameAllowed()const { return (value & (1 << 3)) != 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 {
@ -48,7 +68,9 @@ namespace nn::fs {
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 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, 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) {
switch (result.GetValue()) {
case 0: return "Result.Success";
case 0x177202: return "ResultFs.NotImplemented.Value";
case 0x2EE402: return "ResultFs.InvalidPath.Value";
case 0x2EE602: return "ResultFs.TooLongPath.Value";
case 0x2EE802: return "ResultFs.InvalidCharacter.Value";
@ -111,12 +134,33 @@ nn::fs::PathFlags GetPathFlags(char const* pathFlags) {
case 'W':
flags.AllowWindowsPath();
break;
case 'C':
flags.AllowAllCharacters();
break;
}
}
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(
// Check AllowEmptyPath option
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"), // 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:/c:\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"(\\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"(\\..\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\..\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
@ -204,6 +249,33 @@ static constexpr const auto TestData_PathFormatterNormalize_Backslash = make_arr
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(
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
@ -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"),
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"(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")
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) {
@ -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);
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) {
@ -243,7 +316,7 @@ void CreateTest_PathFormatterIsNormalized(char const* path, char const* pathFlag
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));
GetEscaped(path).c_str(), pathFlags, BoolStr(isNormalized), normalizedLength, GetResultName(result));
}
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 };
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));
GetEscaped(path).c_str(), pathFlags, bufferSize, GetEscaped(normalized).c_str(), 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)
std::make_tuple("/aa/bb/c/", false, true, false),
std::make_tuple("aa/bb/c/", false, false, false),
std::make_tuple("aa/bb/c/", false, true, false),
std::make_tuple("mount:a/b", false, true, false),
std::make_tuple("mo|unt:a/b", false, true, true),
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/../../..", true, false, false),
std::make_tuple("/aa/bb/../../..", false, false, false),
std::make_tuple("aa/bb/../../..", true, true, false),
std::make_tuple("aa/bb/../../..", false, true, false),
std::make_tuple("mount:a/b", false, true, true), // Test allowing invalid characters
std::make_tuple("/a|/bb/cc", false, false, true),
std::make_tuple("/>a/bb/cc", false, false, true),
std::make_tuple("/aa/.</cc", false, false, true),
std::make_tuple("/aa/..</cc", false, false, true),
std::make_tuple("", false, false, false),
std::make_tuple("/", false, false, false),
std::make_tuple("/.", false, false, false),
std::make_tuple("/./", false, false, false),
std::make_tuple("/..", false, false, false),
std::make_tuple("//.", false, false, false),
std::make_tuple("/ ..", false, false, false),
std::make_tuple("/.. /", false, false, false),
std::make_tuple("/. /.", false, false, false),
std::make_tuple("/aa/bb/cc/dd/./.././../..", false, false, false),
std::make_tuple("/aa/bb/cc/dd/./.././../../..", false, false, false),
std::make_tuple("/./aa/./bb/./cc/./dd/.", false, false, false),
std::make_tuple("/aa\\bb/cc", false, false, false),
std::make_tuple("/aa\\bb/cc", false, false, false),
std::make_tuple("/a|/bb/cc", false, false, false),
std::make_tuple("/>a/bb/cc", false, false, false),
std::make_tuple("/aa/.</cc", false, 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 };
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",
path, BoolStr(isWindowsPath), BoolStr(isRelativePath), normalized, normalizedLength, GetResultName(result));
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %s, %s, @\"%s\", %ld, %s},\n",
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;
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",
path, BoolStr(isNormalized), normalizedLength, GetResultName(result));
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %s, %ld, %s},\n",
GetEscaped(path).c_str(), BoolStr(allowAllCharacters), BoolStr(isNormalized), normalizedLength, GetResultName(result));
}
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);
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(
@ -377,9 +454,62 @@ 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));
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) {
// 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_RelativePath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_RelativePath);
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_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_RelativePath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_RelativePath);
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_PathNormalizer_Normalize", CreateTest_PathNormalizerNormalize, TestData_PathNormalizerNormalize);

View File

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