diff --git a/src/LibHac/Fs/Common/Path.cs b/src/LibHac/Fs/Common/Path.cs index 3fa70880..49af516f 100644 --- a/src/LibHac/Fs/Common/Path.cs +++ b/src/LibHac/Fs/Common/Path.cs @@ -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; } /// diff --git a/src/LibHac/Fs/Common/PathFormatter.cs b/src/LibHac/Fs/Common/PathFormatter.cs index 24b961f8..08e4ddc2 100644 --- a/src/LibHac/Fs/Common/PathFormatter.cs +++ b/src/LibHac/Fs/Common/PathFormatter.cs @@ -13,9 +13,19 @@ namespace LibHac.Fs; /// /// Contains functions for working with path formatting and normalization. /// -/// Based on FS 12.1.0 (nnSdk 12.3.1) +/// Based on FS 13.1.0 (nnSdk 13.4.0) public static class PathFormatter { + private static ReadOnlySpan InvalidCharacter => + new[] { (byte)':', (byte)'*', (byte)'?', (byte)'<', (byte)'>', (byte)'|' }; + + private static ReadOnlySpan InvalidCharacterForHostName => + new[] { (byte)':', (byte)'*', (byte)'<', (byte)'>', (byte)'|', (byte)'$' }; + + private static ReadOnlySpan InvalidCharacterForMountName => + new[] { (byte)'*', (byte)'?', (byte)'<', (byte)'>', (byte)'|' }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Result CheckHostName(ReadOnlySpan 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; diff --git a/src/LibHac/Fs/Common/PathNormalizer.cs b/src/LibHac/Fs/Common/PathNormalizer.cs index 197728dd..91251b29 100644 --- a/src/LibHac/Fs/Common/PathNormalizer.cs +++ b/src/LibHac/Fs/Common/PathNormalizer.cs @@ -10,7 +10,7 @@ namespace LibHac.Fs; /// /// Contains functions for doing with basic path normalization. /// -/// Based on FS 12.1.0 (nnSdk 12.3.1) +/// Based on FS 13.1.0 (nnSdk 13.4.0) public static class PathNormalizer { private enum PathState @@ -25,6 +25,12 @@ public static class PathNormalizer public static Result Normalize(Span outputBuffer, out int length, ReadOnlySpan path, bool isWindowsPath, bool isDriveRelativePath) + { + return Normalize(outputBuffer, out length, path, isWindowsPath, isDriveRelativePath, false); + } + + public static Result Normalize(Span outputBuffer, out int length, ReadOnlySpan 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(); 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 /// : The path contains an invalid character.
/// : The path is not in a valid format. public static Result IsNormalized(out bool isNormalized, out int length, ReadOnlySpan path) + { + return IsNormalized(out isNormalized, out length, path, false); + } + + public static Result IsNormalized(out bool isNormalized, out int length, ReadOnlySpan 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; } - /// /// Checks if a path begins with / or \ and contains any of these patterns: /// "/..\", "\..\", "\../", "\..0" where '0' is the null terminator. diff --git a/tests/LibHac.Tests/Fs/PathFormatterTests.cs b/tests/LibHac.Tests/Fs/PathFormatterTests.cs index 0cb382d8..c5d0201c 100644 --- a/tests/LibHac.Tests/Fs/PathFormatterTests.cs +++ b/tests/LibHac.Tests/Fs/PathFormatterTests.cs @@ -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 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/bb/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/bb/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/bunt:/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 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 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 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/bb/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/bb/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/bunt:/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 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; } -} +} \ No newline at end of file diff --git a/tests/LibHac.Tests/Fs/PathNormalizationTestGenerator.cpp b/tests/LibHac.Tests/Fs/PathNormalizationTestGenerator.cpp index 5c5e41cc..0e875b40 100644 --- a/tests/LibHac.Tests/Fs/PathNormalizationTestGenerator.cpp +++ b/tests/LibHac.Tests/Fs/PathNormalizationTestGenerator.cpp @@ -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::arrayb/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/bb/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/bunt:/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/.a/bb/cc", false, false, true), + std::make_tuple("/aa/.a/bb/cc", false, false, false), + std::make_tuple("/aa/. TestData_Normalize => new() + public static TheoryData 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/.a/bb/cc", false, false, true, @"/>a/bb/cc", 9, Result.Success }, + { @"/aa/.a/bb/cc", false, false, false, @"/", 0, ResultFs.InvalidCharacter.Value }, + { @"/aa/. TestData_IsNormalized => new() + public static TheoryData 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/.a/bb/cc", true, true, 9, Result.Success }, + { @"/aa/.a/bb/cc", false, false, 0, ResultFs.InvalidCharacter.Value }, + { @"/aa/.