From 0c255e0f49a95c4d76da10a295ecf05517746450 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 28 Mar 2021 15:50:29 -0700 Subject: [PATCH] Update common path handling code - Updates path handling code to system version 11.0.0. - Changes InMemoryFileSystem to normalize all incoming paths. --- src/LibHac/Fs/Common/PathNormalizer.cs | 796 ++++++++++++++++++ src/LibHac/Fs/Common/PathUtility.cs | 129 +++ src/LibHac/Fs/Common/WindowsPath.cs | 113 +++ src/LibHac/Fs/FileSystemClient.cs | 2 + src/LibHac/Fs/Fsa/MountUtility.cs | 8 +- .../Fs/Impl/FileSystemServiceObjectAdapter.cs | 4 +- src/LibHac/Fs/InMemoryFileSystem.cs | 117 ++- src/LibHac/Fs/PathTool.cs | 761 +---------------- src/LibHac/Fs/PathUtility.cs | 111 --- src/LibHac/Fs/Shim/Bis.cs | 2 +- src/LibHac/Fs/Shim/Host.cs | 19 +- src/LibHac/FsSrv/BaseFileSystemService.cs | 3 +- .../FsSrv/Impl/FileSystemInterfaceAdapter.cs | 34 +- src/LibHac/FsSrv/Impl/PathNormalizer.cs | 106 +++ src/LibHac/FsSrv/NcaFileSystemService.cs | 15 +- src/LibHac/FsSrv/PathNormalizer.cs | 75 -- src/LibHac/FsSrv/SaveDataFileSystemService.cs | 3 +- src/LibHac/FsSrv/Util.cs | 7 +- .../FsSystem/DirectorySaveDataFileSystem.cs | 2 +- src/LibHac/FsSystem/LocalFileSystem.cs | 8 +- .../FsSystem/Save/SaveDataFileSystemCore.cs | 26 +- src/LibHac/FsSystem/SubdirectoryFileSystem.cs | 6 +- src/LibHac/FsSystem/Utility.cs | 4 +- tests/LibHac.Tests/DebugAssertHandler.cs | 2 +- .../LibHac.Tests/Fs/PathToolTestGenerator.cpp | 19 +- tests/LibHac.Tests/Fs/PathToolTests.cs | 8 +- .../LibHac.Tests/FsSrv/PathNormalizerTests.cs | 18 +- tests/LibHac.Tests/LibHacTestFramework.cs | 16 + 28 files changed, 1360 insertions(+), 1054 deletions(-) create mode 100644 src/LibHac/Fs/Common/PathNormalizer.cs create mode 100644 src/LibHac/Fs/Common/PathUtility.cs create mode 100644 src/LibHac/Fs/Common/WindowsPath.cs delete mode 100644 src/LibHac/Fs/PathUtility.cs create mode 100644 src/LibHac/FsSrv/Impl/PathNormalizer.cs delete mode 100644 src/LibHac/FsSrv/PathNormalizer.cs diff --git a/src/LibHac/Fs/Common/PathNormalizer.cs b/src/LibHac/Fs/Common/PathNormalizer.cs new file mode 100644 index 00000000..f0ab0d96 --- /dev/null +++ b/src/LibHac/Fs/Common/PathNormalizer.cs @@ -0,0 +1,796 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using LibHac.Common; +using LibHac.Diag; +using LibHac.FsSystem; +using static LibHac.Fs.StringTraits; + +// ReSharper disable once CheckNamespace +namespace LibHac.Fs +{ + // Previous normalization code can be found in commit 1acdd86e27de16703fdb1c77f50ed8fd71bd3ad7 + public static class PathNormalizer + { + private enum NormalizeState + { + Initial, + Normal, + FirstSeparator, + Separator, + Dot, + DoubleDot + } + + /// + /// Checks if a host-name or share-name in a UNC path are "." or ".." + /// + /// Up to the first two characters in a segment of a UNC path. + /// : The operation was successful.
+ /// : A buffer could not be allocated.
+ private static Result CheckSharedName(U8Span path) + { + if (path.Length == 1 && path.GetUnsafe(0) == Dot) + return ResultFs.InvalidPathFormat.Log(); + + if (path.Length == 2 && path.GetUnsafe(0) == Dot && path.GetUnsafe(1) == Dot) + return ResultFs.InvalidPathFormat.Log(); + + return Result.Success; + } + + private static Result ParseWindowsPath(out U8Span newPath, Span buffer, out long windowsPathLength, + out bool isUncNormalized, U8Span path, bool hasMountName) + { + UnsafeHelpers.SkipParamInit(out windowsPathLength, out isUncNormalized); + newPath = default; + + U8Span currentPath = path; + + if (!Unsafe.IsNullRef(ref isUncNormalized)) + isUncNormalized = true; + + bool skippedMount = false; + int prefixLength = 0; + + if (hasMountName) + { + if (path.GetOrNull(0) == DirectorySeparator && path.GetOrNull(1) == AltDirectorySeparator && + path.GetOrNull(1) == AltDirectorySeparator) + { + currentPath = currentPath.Slice(1); + skippedMount = true; + } + else + { + int separatorCount = 0; + + while (IsSeparator(currentPath.GetOrNull(separatorCount))) + { + separatorCount++; + } + + if (separatorCount != 0) + { + if (separatorCount == 1 || WindowsPath.IsWindowsDrive(currentPath.Slice(separatorCount))) + { + currentPath = currentPath.Slice(separatorCount); + skippedMount = true; + } + else + { + if (separatorCount > 2 && !Unsafe.IsNullRef(ref isUncNormalized)) + { + isUncNormalized = false; + return Result.Success; + } + + currentPath = currentPath.Slice(separatorCount - 2); + prefixLength = 1; + } + } + } + } + else if (path.GetOrNull(0) == DirectorySeparator && !WindowsPath.IsUnc(path)) + { + currentPath = currentPath.Slice(1); + skippedMount = true; + } + + U8Span trimmedPath = path; + + if (WindowsPath.IsWindowsDrive(currentPath)) + { + int i; + for (i = 2; currentPath.GetOrNull(i) != NullTerminator; i++) + { + if (currentPath[i] == DirectorySeparator || currentPath[i] == AltDirectorySeparator) + { + trimmedPath = currentPath.Slice(i); + break; + } + } + + if (trimmedPath.Value == path.Value) + trimmedPath = currentPath.Slice(i); + + ref byte pathStart = ref MemoryMarshal.GetReference(path.Value); + ref byte trimmedPathStart = ref MemoryMarshal.GetReference(trimmedPath.Value); + int winPathLength = (int)Unsafe.ByteOffset(ref pathStart, ref trimmedPathStart); + + if (!buffer.IsEmpty) + { + if (winPathLength > buffer.Length) + return ResultFs.TooLongPath.Log(); + + path.Value.Slice(0, winPathLength).CopyTo(buffer); + } + + newPath = trimmedPath; + windowsPathLength = winPathLength; + return Result.Success; + } + // A UNC path should be in the format "\\" host-name "\" share-name [ "\" object-name ] + else if (WindowsPath.IsUnc(currentPath)) + { + if (currentPath.GetOrNull(2) == DirectorySeparator || currentPath.GetOrNull(2) == AltDirectorySeparator) + { + Assert.SdkAssert(!hasMountName); + return ResultFs.InvalidPathFormat.Log(); + } + else + { + bool needsSeparatorFix = false; + int currentComponentOffset = 0; + + for (int i = 2; currentPath.GetOrNull(i) != NullTerminator; i++) + { + byte c = currentPath.GetUnsafe(i); + + // Check if we need to fix the separators + if (currentComponentOffset == 0 && c == AltDirectorySeparator) + { + needsSeparatorFix = true; + + if (!Unsafe.IsNullRef(ref isUncNormalized)) + { + isUncNormalized = false; + return Result.Success; + } + } + + if (c == DirectorySeparator || c == AltDirectorySeparator) + { + if (c == AltDirectorySeparator) + needsSeparatorFix = true; + + if (currentComponentOffset != 0) + break; + + if (IsSeparator(currentPath.GetOrNull(i + 1))) + return ResultFs.InvalidPathFormat.Log(); + + Result rc = CheckSharedName(currentPath.Slice(2, i - 2)); + if (rc.IsFailure()) return rc; + + currentComponentOffset = i + 1; + } + + if (c == (byte)'$' || c == DriveSeparator) + { + if (currentComponentOffset == 0) + return ResultFs.InvalidCharacter.Log(); + + // A '$' or ':' must be the last character in share-name + byte nextChar = currentPath.GetOrNull(i + 1); + if (nextChar != DirectorySeparator && nextChar != AltDirectorySeparator && + nextChar != NullTerminator) + { + return ResultFs.InvalidPathFormat.Log(); + } + + trimmedPath = currentPath.Slice(i + 1); + break; + } + } + + if (trimmedPath.Value == path.Value) + { + int trimmedPartOffset = 0; + + int i; + for (i = 2; currentPath.GetOrNull(i) != NullTerminator; i++) + { + byte c = currentPath.GetUnsafe(i); + + if (c == DirectorySeparator || c == AltDirectorySeparator) + { + Result rc; + + if (trimmedPartOffset != 0) + { + rc = CheckSharedName(currentPath.Slice(trimmedPartOffset, i - trimmedPartOffset)); + if (rc.IsFailure()) return rc; + + trimmedPath = currentPath.Slice(i); + break; + } + + if (IsSeparator(currentPath.GetOrNull(i + 1))) + { + return ResultFs.InvalidPathFormat.Log(); + } + + rc = CheckSharedName(currentPath.Slice(2, i - 2)); + if (rc.IsFailure()) return rc; + + trimmedPartOffset = i + 1; + } + } + + if (trimmedPartOffset != 0 && trimmedPath.Value == path.Value) + { + Result rc = CheckSharedName(currentPath.Slice(trimmedPartOffset, i - trimmedPartOffset)); + if (rc.IsFailure()) return rc; + + trimmedPath = currentPath.Slice(i); + } + } + + ref byte trimmedPathStart = ref MemoryMarshal.GetReference(trimmedPath.Value); + ref byte currentPathStart = ref MemoryMarshal.GetReference(currentPath.Value); + int mountLength = (int)Unsafe.ByteOffset(ref currentPathStart, ref trimmedPathStart); + bool prependSeparator = prefixLength != 0 || skippedMount; + + if (!buffer.IsEmpty) + { + if (mountLength > buffer.Length) + { + return ResultFs.TooLongPath.Log(); + } + + Span currentBuffer = buffer; + if (prependSeparator) + { + currentBuffer[0] = DirectorySeparator; + currentBuffer = currentBuffer.Slice(1); + } + + currentPath.Value.Slice(0, mountLength).CopyTo(currentBuffer); + currentBuffer[0] = AltDirectorySeparator; + currentBuffer[1] = AltDirectorySeparator; + + if (needsSeparatorFix) + { + for (int i = 2; i < mountLength; i++) + { + if (currentBuffer[i] == AltDirectorySeparator) + currentBuffer[i] = DirectorySeparator; + } + } + } + + newPath = trimmedPath; + windowsPathLength = mountLength + (prependSeparator ? 1 : 0); + return Result.Success; + } + } + else + { + newPath = trimmedPath; + return Result.Success; + } + } + + private static Result SkipWindowsPath(out U8Span newPath, out bool isUncNormalized, U8Span path, + bool hasMountName) + { + return ParseWindowsPath(out newPath, Span.Empty, out _, out isUncNormalized, path, hasMountName); + } + + private static Result ParseMountName(out U8Span newPath, Span outMountNameBuffer, + out long mountNameLength, U8Span path) + { + UnsafeHelpers.SkipParamInit(out mountNameLength); + newPath = default; + + int mountStart = IsSeparator(path.GetOrNull(0)) ? 1 : 0; + int mountEnd; + + int maxMountLength = Math.Min(PathTools.MountNameLengthMax, path.Length - mountStart); + + for (mountEnd = mountStart; mountEnd <= maxMountLength; mountEnd++) + { + byte c = path[mountEnd]; + + if (IsSeparator(c)) + { + newPath = path; + mountNameLength = 0; + + return Result.Success; + } + + if (c == DriveSeparator) + { + mountEnd++; + break; + } + + if (c == NullTerminator) + { + break; + } + } + + if (mountStart >= mountEnd - 1 || path[mountEnd - 1] != DriveSeparator) + return ResultFs.InvalidPathFormat.Log(); + + if (mountEnd != mountStart) + { + for (int i = mountStart; i < mountEnd; i++) + { + if (path[i] == Dot) + return ResultFs.InvalidCharacter.Log(); + } + } + + if (!outMountNameBuffer.IsEmpty) + { + if (mountEnd - mountStart > outMountNameBuffer.Length) + return ResultFs.TooLongPath.Log(); + + path.Value.Slice(0, mountEnd).CopyTo(outMountNameBuffer); + } + + newPath = path.Slice(mountEnd); + mountNameLength = mountEnd - mountStart; + return Result.Success; + } + + private static Result SkipMountName(out U8Span newPath, U8Span path) + { + return ParseMountName(out newPath, Span.Empty, out _, path); + } + + /// + /// Checks if a path begins with / or \ and contains any of these patterns: + /// "/..\", "\..\", "\../", "\..0" where '0' is the null terminator. + /// + private static bool IsParentDirectoryPathReplacementNeeded(U8Span path) + { + if (!IsAnySeparator(path.GetOrNull(0))) + return false; + + for (int i = 0; i < path.Length - 2 && path[i] != NullTerminator; i++) + { + byte c3 = path.GetOrNull(i + 3); + + if (path[i] == AltDirectorySeparator && + path[i + 1] == Dot && + path[i + 2] == Dot && + (IsAnySeparator(c3) || c3 == NullTerminator)) + { + return true; + } + + if (IsAnySeparator(path[i]) && + path[i + 1] == Dot && + path[i + 2] == Dot && + c3 == AltDirectorySeparator) + { + return true; + } + } + + return false; + } + + private static void ReplaceParentDirectoryPath(Span dest, ReadOnlySpan source) + { + dest[0] = DirectorySeparator; + + int i = 1; + while (source.Length > i && source[i] != NullTerminator) + { + if (source.Length > i + 2 && + IsAnySeparator(source[i - 1]) && + source[i + 0] == Dot && + source[i + 1] == Dot && + IsAnySeparator(source[i + 2])) + { + dest[i - 1] = DirectorySeparator; + dest[i + 0] = Dot; + dest[i + 1] = Dot; + dest[i + 2] = DirectorySeparator; + i += 3; + } + else + { + if (source.Length > i + 1 && + source[i - 1] == AltDirectorySeparator && + source[i + 0] == Dot && + source[i + 1] == Dot && + (source.Length == i + 2 || source[i + 2] == NullTerminator)) + { + dest[i - 1] = DirectorySeparator; + dest[i + 0] = Dot; + dest[i + 1] = Dot; + i += 2; + break; + } + + dest[i] = source[i]; + i++; + } + } + + dest[i] = NullTerminator; + } + + public static Result Normalize(Span outputBuffer, out long normalizedLength, U8Span path, + bool preserveUnc, bool hasMountName) + { + UnsafeHelpers.SkipParamInit(out normalizedLength); + + U8Span currentPath = path; + int prefixLength = 0; + bool isUncPath = false; + + if (hasMountName) + { + Result rc = ParseMountName(out currentPath, outputBuffer, out long mountNameLength, currentPath); + if (rc.IsFailure()) return rc; + + prefixLength += (int)mountNameLength; + } + + if (preserveUnc) + { + U8Span originalPath = currentPath; + + Result rc = ParseWindowsPath(out currentPath, outputBuffer.Slice(prefixLength), + out long windowsPathLength, out Unsafe.NullRef(), currentPath, hasMountName); + if (rc.IsFailure()) return rc; + + prefixLength += (int)windowsPathLength; + if (originalPath.Value != currentPath.Value) + { + isUncPath = true; + } + } + + // Paths must start with / + if (prefixLength == 0 && !IsSeparator(currentPath.GetOrNull(0))) + return ResultFs.InvalidPathFormat.Log(); + + var convertedPath = new RentedArray(); + try + { + // Check if parent directory path replacement is needed. + if (IsParentDirectoryPathReplacementNeeded(currentPath)) + { + // Allocate a buffer to hold the replacement path. + convertedPath = new RentedArray(PathTools.MaxPathLength + 1); + + // Replace the path. + ReplaceParentDirectoryPath(convertedPath.Span, currentPath); + + // Set current path to be the replacement path. + currentPath = new U8Span(convertedPath.Span); + } + + bool skipNextSep = false; + int i = 0; + int totalLength = prefixLength; + + while (!IsNul(currentPath.GetOrNull(i))) + { + if (IsSeparator(currentPath[i])) + { + do + { + i++; + } while (IsSeparator(currentPath.GetOrNull(i))); + + if (IsNul(currentPath.GetOrNull(i))) + break; + + if (!skipNextSep) + { + if (totalLength + 1 == outputBuffer.Length) + { + outputBuffer[totalLength] = NullTerminator; + normalizedLength = totalLength; + + return ResultFs.TooLongPath.Log(); + } + + outputBuffer[totalLength++] = DirectorySeparator; + } + + skipNextSep = false; + } + + int dirLen = 0; + while (!IsSeparator(currentPath.GetOrNull(i + dirLen)) && !IsNul(currentPath.GetOrNull(i + dirLen))) + { + dirLen++; + } + + if (IsCurrentDirectory(currentPath.Slice(i))) + { + skipNextSep = true; + } + else if (IsParentDirectory(currentPath.Slice(i))) + { + Assert.SdkAssert(outputBuffer[totalLength - 1] == DirectorySeparator); + Assert.SdkAssert(outputBuffer[prefixLength] == DirectorySeparator); + + if (totalLength == prefixLength + 1) + { + if (!isUncPath) + return ResultFs.DirectoryUnobtainable.Log(); + + totalLength--; + } + else + { + totalLength -= 2; + + do + { + if (outputBuffer[totalLength] == DirectorySeparator) + break; + + totalLength--; + } while (totalLength != prefixLength); + } + + if (!isUncPath) + Assert.SdkAssert(outputBuffer[totalLength] == DirectorySeparator); + + Assert.SdkAssert(totalLength < outputBuffer.Length); + } + else + { + if (totalLength + dirLen + 1 > outputBuffer.Length) + { + int copyLen = outputBuffer.Length - 1 - totalLength; + + for (int j = 0; j < copyLen; j++) + { + outputBuffer[totalLength++] = currentPath[i + j]; + } + + outputBuffer[totalLength] = NullTerminator; + normalizedLength = totalLength; + return ResultFs.TooLongPath.Log(); + } + else + { + for (int j = 0; j < dirLen; j++) + { + outputBuffer[totalLength++] = currentPath[i + j]; + } + } + } + + i += dirLen; + } + + if (skipNextSep) + totalLength--; + + if (!isUncPath && totalLength == prefixLength && totalLength < outputBuffer.Length) + { + outputBuffer[prefixLength] = DirectorySeparator; + totalLength++; + } + + if (totalLength - 1 > outputBuffer.Length) + { + return ResultFs.TooLongPath.Log(); + } + else + { + outputBuffer[totalLength] = NullTerminator; + normalizedLength = totalLength; + + Assert.SdkAssert(IsNormalized(out bool normalized, new U8Span(outputBuffer), preserveUnc, hasMountName).IsSuccess()); + Assert.SdkAssert(normalized); + } + + return Result.Success; + } + finally + { + convertedPath.Dispose(); + } + } + + public static Result IsNormalized(out bool isNormalized, U8Span path, bool preserveUnc, bool hasMountName) + { + UnsafeHelpers.SkipParamInit(out isNormalized); + + U8Span currentPath = path; + U8Span originalPath = path; + bool isUncPath = false; + + if (hasMountName) + { + Result rc = SkipMountName(out currentPath, originalPath); + if (rc.IsFailure()) return rc; + + if (currentPath.GetOrNull(0) != DirectorySeparator) + return ResultFs.InvalidPathFormat.Log(); + } + + if (preserveUnc) + { + originalPath = currentPath; + + Result rc = SkipWindowsPath(out currentPath, out bool isUncNormalized, currentPath, hasMountName); + if (rc.IsFailure()) return rc; + + if (!isUncNormalized) + { + isNormalized = false; + return Result.Success; + } + + // Path is a UNC path if the new path skips part of the original + isUncPath = originalPath.Value != currentPath.Value; + + if (isUncPath) + { + if (IsSeparator(originalPath.GetOrNull(0)) && IsSeparator(originalPath.GetOrNull(1))) + { + isNormalized = false; + return Result.Success; + } + + if (IsNul(currentPath.GetOrNull(0))) + { + isNormalized = true; + return Result.Success; + } + } + } + + if (IsParentDirectoryPathReplacementNeeded(currentPath)) + { + isNormalized = false; + return Result.Success; + } + + var state = NormalizeState.Initial; + + for (int i = 0; i < currentPath.Length; i++) + { + byte c = currentPath[i]; + if (c == NullTerminator) break; + + switch (state) + { + case NormalizeState.Initial: + if (c == DirectorySeparator) + { + state = NormalizeState.FirstSeparator; + } + else + { + if (currentPath.Value == originalPath.Value) + return ResultFs.InvalidPathFormat.Log(); + + state = c == Dot ? NormalizeState.Dot : NormalizeState.Normal; + } + + break; + case NormalizeState.Normal: + if (c == DirectorySeparator) + { + state = NormalizeState.Separator; + } + + break; + case NormalizeState.FirstSeparator: + case NormalizeState.Separator: + if (c == DirectorySeparator) + { + isNormalized = false; + return Result.Success; + } + + state = c == Dot ? NormalizeState.Dot : NormalizeState.Normal; + break; + case NormalizeState.Dot: + if (c == DirectorySeparator) + { + isNormalized = false; + return Result.Success; + } + + state = c == Dot ? NormalizeState.DoubleDot : NormalizeState.Normal; + break; + case NormalizeState.DoubleDot: + if (c == DirectorySeparator) + { + isNormalized = false; + return Result.Success; + } + + state = NormalizeState.Normal; + break; + // ReSharper disable once UnreachableSwitchCaseDueToIntegerAnalysis + default: + Abort.UnexpectedDefault(); + break; + } + } + + switch (state) + { + case NormalizeState.Initial: + return ResultFs.InvalidPathFormat.Log(); + case NormalizeState.Normal: + isNormalized = true; + break; + case NormalizeState.FirstSeparator: + isNormalized = !isUncPath; + break; + case NormalizeState.Separator: + case NormalizeState.Dot: + case NormalizeState.DoubleDot: + isNormalized = false; + break; + // ReSharper disable once UnreachableSwitchCaseDueToIntegerAnalysis + default: + Abort.UnexpectedDefault(); + break; + } + + return Result.Success; + } + + public static bool IsCurrentDirectory(ReadOnlySpan p) + { + if (p.Length < 1) + return false; + + ref byte b = ref MemoryMarshal.GetReference(p); + + return b == Dot && + (p.Length == 1 || Unsafe.Add(ref b, 1) == NullTerminator || + Unsafe.Add(ref b, 1) == DirectorySeparator); + } + + public static bool IsParentDirectory(ReadOnlySpan p) + { + if (p.Length < 2) + return false; + + ref byte b = ref MemoryMarshal.GetReference(p); + + return b == Dot && + Unsafe.Add(ref b, 1) == Dot && + (p.Length == 2 || Unsafe.Add(ref b, 2) == NullTerminator || + Unsafe.Add(ref b, 2) == DirectorySeparator); + } + + public static bool IsNul(byte c) + { + return c == NullTerminator; + } + + public static bool IsSeparator(byte c) + { + return c == DirectorySeparator; + } + + public static bool IsAnySeparator(byte c) + { + return c == DirectorySeparator || c == AltDirectorySeparator; + } + } +} diff --git a/src/LibHac/Fs/Common/PathUtility.cs b/src/LibHac/Fs/Common/PathUtility.cs new file mode 100644 index 00000000..0b27dcee --- /dev/null +++ b/src/LibHac/Fs/Common/PathUtility.cs @@ -0,0 +1,129 @@ +using System; +using System.Diagnostics; +using LibHac.Common; +using LibHac.Diag; +using static LibHac.Fs.StringTraits; + +// ReSharper disable once CheckNamespace +namespace LibHac.Fs +{ + internal struct PathUtilityGlobals + { + public PathVerifier PathVerifier; + + public void Initialize(FileSystemClient _) + { + PathVerifier.Initialize(); + } + } + + internal struct PathVerifier + { + public void Initialize() + { + // Todo + } + + public static Result Verify(U8Span path, int maxPathLength, int maxNameLength) + { + Debug.Assert(!path.IsNull()); + + int nameLength = 0; + + for (int i = 0; i < path.Length && i <= maxPathLength && nameLength <= maxNameLength; i++) + { + byte c = path[i]; + + if (c == 0) + return Result.Success; + + // todo: Compare path based on their Unicode code points + + if (c == ':' || c == '*' || c == '?' || c == '<' || c == '>' || c == '|') + return ResultFs.InvalidCharacter.Log(); + + nameLength++; + if (c == '\\' || c == '/') + { + nameLength = 0; + } + } + + return ResultFs.TooLongPath.Log(); + } + } + + public static class PathUtility + { + public static void Replace(Span buffer, byte currentChar, byte newChar) + { + Assert.SdkRequiresNotNull(buffer); + + for (int i = 0; i < buffer.Length; i++) + { + if (buffer[i] == currentChar) + { + buffer[i] = newChar; + } + } + } + + /// + /// Performs the extra functions that nn::fs::FspPathPrintf does on the string buffer. + /// + /// The string builder to process. + /// The of the operation. + public static Result ToSfPath(in this U8StringBuilder builder) + { + if (builder.Overflowed) + return ResultFs.TooLongPath.Log(); + + Replace(builder.Buffer.Slice(builder.Capacity), + AltDirectorySeparator, + DirectorySeparator); + + return Result.Success; + } + + public static Result VerifyPath(this FileSystemClient fs, U8Span path, int maxPathLength, int maxNameLength) + { + return PathVerifier.Verify(path, maxPathLength, maxNameLength); + } + + public static bool IsSubPath(U8Span lhs, U8Span rhs) + { + Assert.SdkRequires(!lhs.IsNull()); + Assert.SdkRequires(!rhs.IsNull()); + + bool isUncLhs = WindowsPath.IsUnc(lhs); + bool isUncRhs = WindowsPath.IsUnc(rhs); + + if (isUncLhs && !isUncRhs || !isUncLhs && isUncRhs) + return false; + + if (lhs.GetOrNull(0) == DirectorySeparator && lhs.GetOrNull(1) == NullTerminator && + rhs.GetOrNull(0) == DirectorySeparator && rhs.GetOrNull(1) != NullTerminator) + return true; + + if (rhs.GetOrNull(0) == DirectorySeparator && rhs.GetOrNull(1) == NullTerminator && + lhs.GetOrNull(0) == DirectorySeparator && lhs.GetOrNull(1) != NullTerminator) + return true; + + for (int i = 0; ; i++) + { + if (lhs.GetOrNull(i) == NullTerminator) + { + return rhs.GetOrNull(i) == DirectorySeparator; + } + else if (rhs.GetOrNull(i) == NullTerminator) + { + return lhs.GetOrNull(i) == DirectorySeparator; + } + else if (lhs.GetOrNull(i) != rhs.GetOrNull(i)) + { + return false; + } + } + } + } +} diff --git a/src/LibHac/Fs/Common/WindowsPath.cs b/src/LibHac/Fs/Common/WindowsPath.cs new file mode 100644 index 00000000..6a96ef50 --- /dev/null +++ b/src/LibHac/Fs/Common/WindowsPath.cs @@ -0,0 +1,113 @@ +using System; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Diag; +using static LibHac.Fs.StringTraits; + +// ReSharper disable once CheckNamespace +namespace LibHac.Fs +{ + public static class WindowsPath + { + private const int WindowsDriveLength = 2; + private const int UncPathPrefixLength = 2; + private const int DosDevicePathPrefixLength = 4; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsWindowsDrive(U8Span path) + { + Assert.SdkRequires(!path.IsNull()); + + return (uint)path.Length > 1 && + IsWindowsDriveCharacter(path[0]) && + path[1] == DriveSeparator; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsWindowsDriveCharacter(byte c) + { + // Mask lowercase letters to uppercase and check if it's in range + return (0b1101_1111 & c) - 'A' <= 'Z' - 'A'; + // return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z'; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUnc(U8Span path) + { + return (uint)path.Length >= UncPathPrefixLength && + (path.GetUnsafe(0) == DirectorySeparator && path.GetUnsafe(1) == DirectorySeparator || + path.GetUnsafe(0) == AltDirectorySeparator && path.GetUnsafe(1) == AltDirectorySeparator); + } + + public static int GetWindowsPathSkipLength(U8Span path) + { + if (IsWindowsDrive(path)) + return WindowsDriveLength; + + if (!IsUnc(path)) + return 0; + + for (int i = UncPathPrefixLength; i < path.Length && path[i] != NullTerminator; i++) + { + if (path[i] == (byte)'$' || path[i] == DriveSeparator) + { + return i + 1; + } + } + + return 0; + } + + public static bool IsDosDelimiter(char c) + { + return c == '/' || c == '\\'; + } + + public static bool IsDosDevicePath(ReadOnlySpan path) + { + return path.Length >= DosDevicePathPrefixLength + && IsDosDelimiter(path[0]) + && path[1] == '\\' + && (path[2] == '.' || path[2] == '?') + && IsDosDelimiter(path[3]); + } + + public static int GetDosDevicePathPrefixLength() + { + return DosDevicePathPrefixLength; + } + + public static bool IsDriveName(ReadOnlySpan path) + { + return path.Length == WindowsDriveLength && path[1] == ':'; + } + + public static bool IsUncPath(ReadOnlySpan path) + { + return !IsDosDevicePath(path) && path.Length >= UncPathPrefixLength && IsDosDelimiter(path[0]) && + IsDosDelimiter(path[1]); + } + + public static int GetUncPathPrefixLength(ReadOnlySpan path) + { + int i; + for (i = 0; i < path.Length; i++) + { + if (path[i] == '\0') + break; + + if (IsDosDelimiter(path[i]) && i + 1 == DosDevicePathPrefixLength) + break; + } + + return i; + } + + public static bool IsPathRooted(ReadOnlySpan path) + { + return IsDriveName(path.Slice(0, Math.Min(WindowsDriveLength, path.Length))) + || IsDosDevicePath(path.Slice(0, Math.Min(DosDevicePathPrefixLength, path.Length))) + || IsUncPath(path.Slice(0, Math.Min(DosDevicePathPrefixLength, path.Length))); + } + } +} diff --git a/src/LibHac/Fs/FileSystemClient.cs b/src/LibHac/Fs/FileSystemClient.cs index 2360ea7a..ad68e681 100644 --- a/src/LibHac/Fs/FileSystemClient.cs +++ b/src/LibHac/Fs/FileSystemClient.cs @@ -25,6 +25,7 @@ namespace LibHac.Fs public FileSystemProxyServiceObjectGlobals FileSystemProxyServiceObject; public FsContextHandlerGlobals FsContextHandler; public ResultHandlingUtilityGlobals ResultHandlingUtility; + public PathUtilityGlobals PathUtility; public void Initialize(FileSystemClient fsClient, HorizonClient horizonClient) { @@ -33,6 +34,7 @@ namespace LibHac.Fs AccessLog.Initialize(fsClient); UserMountTable.Initialize(fsClient); FsContextHandler.Initialize(fsClient); + PathUtility.Initialize(fsClient); } } diff --git a/src/LibHac/Fs/Fsa/MountUtility.cs b/src/LibHac/Fs/Fsa/MountUtility.cs index a588b933..c3b02763 100644 --- a/src/LibHac/Fs/Fsa/MountUtility.cs +++ b/src/LibHac/Fs/Fsa/MountUtility.cs @@ -5,6 +5,7 @@ using LibHac.Fs.Impl; using LibHac.FsSystem; using LibHac.Os; using LibHac.Util; +using static LibHac.Fs.StringTraits; using static LibHac.Fs.Impl.AccessLogStrings; namespace LibHac.Fs.Fsa @@ -19,7 +20,7 @@ namespace LibHac.Fs.Fsa int mountLen = 0; int maxMountLen = Math.Min(path.Length, PathTools.MountNameLengthMax); - if (PathUtility.IsWindowsDrive(path) || PathUtility.IsUnc(path)) + if (WindowsPath.IsWindowsDrive(path) || WindowsPath.IsUnc(path)) { StringUtils.Copy(mountName.Name, CommonPaths.HostRootFileSystemMountName); mountName.Name[PathTools.MountNameLengthMax] = StringTraits.NullTerminator; @@ -48,7 +49,8 @@ namespace LibHac.Fs.Fsa U8Span subPathTemp = path.Slice(mountLen + 1); - if (subPathTemp.Length == 0 || !PathTool.IsAnySeparator(subPathTemp[0])) + if (subPathTemp.Length == 0 || + (subPathTemp[0] != DirectorySeparator && subPathTemp[0] != AltDirectorySeparator)) return ResultFs.InvalidPathFormat.Log(); path.Value.Slice(0, mountLen).CopyTo(mountName.Name); @@ -74,7 +76,7 @@ namespace LibHac.Fs.Fsa int length = 0; for (int i = 0; i < name.Length && name[i] != 0; i++) { - if (PathTool.IsDriveSeparator(name[i]) || PathTool.IsSeparator(name[i])) + if (name[i] == DriveSeparator || name[i] == DirectorySeparator) return false; if (++length > PathTools.MountNameLengthMax) diff --git a/src/LibHac/Fs/Impl/FileSystemServiceObjectAdapter.cs b/src/LibHac/Fs/Impl/FileSystemServiceObjectAdapter.cs index f324527f..15627179 100644 --- a/src/LibHac/Fs/Impl/FileSystemServiceObjectAdapter.cs +++ b/src/LibHac/Fs/Impl/FileSystemServiceObjectAdapter.cs @@ -222,10 +222,10 @@ namespace LibHac.Fs.Impl PathUtility.Replace(outPath, StringTraits.AltDirectorySeparator, StringTraits.DirectorySeparator); // Get lengths - int windowsSkipLength = PathUtility.GetWindowsPathSkipLength(path); + int windowsSkipLength = WindowsPath.GetWindowsPathSkipLength(path); var nonWindowsPath = new U8Span(sfPath.Str.Slice(windowsSkipLength)); int maxLength = PathTool.EntryNameLengthMax - windowsSkipLength; - return PathUtility.VerifyPath(nonWindowsPath, maxLength, maxLength); + return PathUtility.VerifyPath(null, nonWindowsPath, maxLength, maxLength); } } } diff --git a/src/LibHac/Fs/InMemoryFileSystem.cs b/src/LibHac/Fs/InMemoryFileSystem.cs index fde1e1b9..7fa0fd7c 100644 --- a/src/LibHac/Fs/InMemoryFileSystem.cs +++ b/src/LibHac/Fs/InMemoryFileSystem.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; using LibHac.Common; using LibHac.Fs.Fsa; using LibHac.FsSystem; @@ -22,15 +23,23 @@ namespace LibHac.Fs protected override Result DoCreateDirectory(U8Span path) { - return FsTable.AddDirectory(new U8Span(path)); + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + return FsTable.AddDirectory(normalizedPath); } protected override Result DoCreateDirectory(U8Span path, NxFileAttributes archiveAttribute) { - Result rc = FsTable.AddDirectory(path); + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; - rc = FsTable.GetDirectory(path, out DirectoryNode dir); + rc = FsTable.AddDirectory(normalizedPath); + if (rc.IsFailure()) return rc; + + rc = FsTable.GetDirectory(normalizedPath, out DirectoryNode dir); if (rc.IsFailure()) return rc; dir.Attributes = archiveAttribute; @@ -39,10 +48,14 @@ namespace LibHac.Fs protected override Result DoCreateFile(U8Span path, long size, CreateFileOptions options) { - Result rc = FsTable.AddFile(path); + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; - rc = FsTable.GetFile(path, out FileNode file); + rc = FsTable.AddFile(normalizedPath); + if (rc.IsFailure()) return rc; + + rc = FsTable.GetFile(normalizedPath, out FileNode file); if (rc.IsFailure()) return rc; return file.File.SetSize(size); @@ -50,30 +63,50 @@ namespace LibHac.Fs protected override Result DoDeleteDirectory(U8Span path) { - return FsTable.DeleteDirectory(new U8Span(path), false); + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + return FsTable.DeleteDirectory(normalizedPath, false); } protected override Result DoDeleteDirectoryRecursively(U8Span path) { - return FsTable.DeleteDirectory(new U8Span(path), true); + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + return FsTable.DeleteDirectory(normalizedPath, true); } protected override Result DoCleanDirectoryRecursively(U8Span path) { - return FsTable.CleanDirectory(new U8Span(path)); + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + return FsTable.CleanDirectory(normalizedPath); } protected override Result DoDeleteFile(U8Span path) { - return FsTable.DeleteFile(new U8Span(path)); + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + return FsTable.DeleteFile(normalizedPath); } protected override Result DoOpenDirectory(out IDirectory directory, U8Span path, OpenDirectoryMode mode) { UnsafeHelpers.SkipParamInit(out directory); - Result rs = FsTable.GetDirectory(new U8Span(path), out DirectoryNode dirNode); - if (rs.IsFailure()) return rs; + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + rc = FsTable.GetDirectory(normalizedPath, out DirectoryNode dirNode); + if (rc.IsFailure()) return rc; directory = new MemoryDirectory(dirNode, mode); return Result.Success; @@ -83,7 +116,11 @@ namespace LibHac.Fs { UnsafeHelpers.SkipParamInit(out file); - Result rc = FsTable.GetFile(path, out FileNode fileNode); + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + rc = FsTable.GetFile(normalizedPath, out FileNode fileNode); if (rc.IsFailure()) return rc; file = new MemoryFile(mode, fileNode.File); @@ -91,27 +128,49 @@ namespace LibHac.Fs return Result.Success; } - protected override Result DoRenameDirectory(U8Span oldPath, U8Span newPath) + protected override Result DoRenameDirectory(U8Span currentPath, U8Span newPath) { - return FsTable.RenameDirectory(new U8Span(oldPath), new U8Span(newPath)); + Unsafe.SkipInit(out FsPath normalizedCurrentPath); + Unsafe.SkipInit(out FsPath normalizedNewPath); + + Result rc = PathNormalizer.Normalize(normalizedCurrentPath.Str, out _, currentPath, false, false); + if (rc.IsFailure()) return rc; + + rc = PathNormalizer.Normalize(normalizedNewPath.Str, out _, newPath, false, false); + if (rc.IsFailure()) return rc; + + return FsTable.RenameDirectory(normalizedCurrentPath, normalizedNewPath); } - protected override Result DoRenameFile(U8Span oldPath, U8Span newPath) + protected override Result DoRenameFile(U8Span currentPath, U8Span newPath) { - return FsTable.RenameFile(new U8Span(oldPath), new U8Span(newPath)); + Unsafe.SkipInit(out FsPath normalizedCurrentPath); + Unsafe.SkipInit(out FsPath normalizedNewPath); + + Result rc = PathNormalizer.Normalize(normalizedCurrentPath.Str, out _, currentPath, false, false); + if (rc.IsFailure()) return rc; + + rc = PathNormalizer.Normalize(normalizedNewPath.Str, out _, newPath, false, false); + if (rc.IsFailure()) return rc; + + return FsTable.RenameFile(normalizedCurrentPath, normalizedNewPath); } protected override Result DoGetEntryType(out DirectoryEntryType entryType, U8Span path) { UnsafeHelpers.SkipParamInit(out entryType); - if (FsTable.GetFile(path, out _).IsSuccess()) + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + if (FsTable.GetFile(normalizedPath, out _).IsSuccess()) { entryType = DirectoryEntryType.File; return Result.Success; } - if (FsTable.GetDirectory(path, out _).IsSuccess()) + if (FsTable.GetDirectory(normalizedPath, out _).IsSuccess()) { entryType = DirectoryEntryType.Directory; return Result.Success; @@ -129,13 +188,17 @@ namespace LibHac.Fs { UnsafeHelpers.SkipParamInit(out attributes); - if (FsTable.GetFile(path, out FileNode file).IsSuccess()) + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + if (FsTable.GetFile(normalizedPath, out FileNode file).IsSuccess()) { attributes = file.Attributes; return Result.Success; } - if (FsTable.GetDirectory(path, out DirectoryNode dir).IsSuccess()) + if (FsTable.GetDirectory(normalizedPath, out DirectoryNode dir).IsSuccess()) { attributes = dir.Attributes; return Result.Success; @@ -146,13 +209,17 @@ namespace LibHac.Fs protected override Result DoSetFileAttributes(U8Span path, NxFileAttributes attributes) { - if (FsTable.GetFile(path, out FileNode file).IsSuccess()) + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + if (FsTable.GetFile(normalizedPath, out FileNode file).IsSuccess()) { file.Attributes = attributes; return Result.Success; } - if (FsTable.GetDirectory(path, out DirectoryNode dir).IsSuccess()) + if (FsTable.GetDirectory(normalizedPath, out DirectoryNode dir).IsSuccess()) { dir.Attributes = attributes; return Result.Success; @@ -165,7 +232,11 @@ namespace LibHac.Fs { UnsafeHelpers.SkipParamInit(out fileSize); - if (FsTable.GetFile(path, out FileNode file).IsSuccess()) + Unsafe.SkipInit(out FsPath normalizedPath); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); + if (rc.IsFailure()) return rc; + + if (FsTable.GetFile(normalizedPath, out FileNode file).IsSuccess()) { return file.File.GetSize(out fileSize); } diff --git a/src/LibHac/Fs/PathTool.cs b/src/LibHac/Fs/PathTool.cs index 6e088f18..56707d6c 100644 --- a/src/LibHac/Fs/PathTool.cs +++ b/src/LibHac/Fs/PathTool.cs @@ -1,768 +1,9 @@ -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using LibHac.Common; -using LibHac.FsSystem; - -namespace LibHac.Fs +namespace LibHac.Fs { public static class PathTool { // These are kept in nn::fs, but C# requires them to be inside a class internal const int EntryNameLengthMax = 0x300; internal const int MountNameLengthMax = 15; - - public static bool IsSeparator(byte c) - { - return c == StringTraits.DirectorySeparator; - } - - public static bool IsAltSeparator(byte c) - { - return c == StringTraits.AltDirectorySeparator; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsAnySeparator(byte c) - { - return IsSeparator(c) || IsAltSeparator(c); - } - - public static bool IsNullTerminator(byte c) - { - return c == StringTraits.NullTerminator; - } - - public static bool IsDot(byte c) - { - return c == StringTraits.Dot; - } - - public static bool IsDriveSeparator(byte c) - { - return c == StringTraits.DriveSeparator; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsCurrentDirectory(ReadOnlySpan p) - { - if ((uint)p.Length < 1) return false; - - ref byte b = ref MemoryMarshal.GetReference(p); - - return IsDot(b) && (p.Length == 1 || IsSeparator(Unsafe.Add(ref b, 1)) || IsNullTerminator(Unsafe.Add(ref b, 1))); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsParentDirectory(ReadOnlySpan p) - { - if ((uint)p.Length < 2) return false; - - ref byte b = ref MemoryMarshal.GetReference(p); - - return IsDot(b) && IsDot(Unsafe.Add(ref b, 1)) && - (p.Length == 2 || IsSeparator(Unsafe.Add(ref b, 2)) || IsNullTerminator(Unsafe.Add(ref b, 2))); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsParentDirectoryAlt(ReadOnlySpan p) - { - if ((uint)p.Length < 3) return false; - - ref byte b = ref MemoryMarshal.GetReference(p); - - return IsAnySeparator(b) && IsDot(Unsafe.Add(ref b, 1)) && IsDot(Unsafe.Add(ref b, 2)) && - (p.Length == 3 || IsAnySeparator(Unsafe.Add(ref b, 3)) || IsNullTerminator(Unsafe.Add(ref b, 3))); - } - - /// - /// Checks if a path begins with / or \ and contains any of these patterns: - /// "/..\", "\..\", "\../", "\..0" where '0' is the null terminator. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ContainsParentDirectoryAlt(U8Span path2) - { - if (!IsAnySeparator(path2.GetOrNull(0))) return false; - - for (int i = 0; i < path2.Length - 2; i++) - { - byte c = path2[i]; - - if (IsSeparator(c) && - IsDot(path2[i + 1]) && - IsDot(path2[i + 2]) && - IsAltSeparator(path2.GetOrNull(i + 3))) - { - return true; - } - - if (IsAltSeparator(c) && - IsDot(path2[i + 1]) && - IsDot(path2[i + 2])) - { - byte c3 = path2.GetOrNull(i + 3); - - if (IsNullTerminator(c3) || IsAnySeparator(c3)) - { - return true; - } - } - else if (IsNullTerminator(c)) - { - return false; - } - } - - return false; - } - - public static Result Normalize(Span outputBuffer, out long normalizedLength, U8Span path, - bool preserveUnc, bool hasMountName) - { - UnsafeHelpers.SkipParamInit(out normalizedLength); - - U8Span path2 = path; - int prefixLength = 0; - bool isUncPath = false; - - if (hasMountName) - { - Result rc = ParseMountName(out path2, outputBuffer, out long mountNameLength, path2); - if (rc.IsFailure()) return rc; - - prefixLength += (int)mountNameLength; - } - - if (preserveUnc) - { - U8Span originalPath = path2; - - Result rc = ParseWindowsPath(out path2, outputBuffer.Slice(prefixLength), out long windowsPathLength, - out _, false, path2, hasMountName); - if (rc.IsFailure()) return rc; - - prefixLength += (int)windowsPathLength; - if (originalPath.Value != path2.Value) - { - isUncPath = true; - } - } - - if (prefixLength == 0 && !IsSeparator(path2.GetOrNull(0))) - return ResultFs.InvalidPathFormat.Log(); - - if (ContainsParentDirectoryAlt(path2)) - { - byte[] buffer2 = new byte[PathTools.MaxPathLength + 1]; - - buffer2[0] = StringTraits.DirectorySeparator; - int j; - - for (j = 1; j < path2.Length; j++) - { - byte c = path2[j]; - if (IsNullTerminator(c)) - break; - - // Current char is a dot. Check the surrounding chars for the /../ pattern - if (IsDot(c) && IsParentDirectoryAlt(path2.Value.Slice(j - 1))) - { - buffer2[j - 1] = StringTraits.DirectorySeparator; - buffer2[j] = StringTraits.Dot; - buffer2[j + 1] = StringTraits.Dot; - - j += 2; - - if (!IsNullTerminator(path2.GetOrNull(j))) - { - buffer2[j] = StringTraits.DirectorySeparator; - } - } - else - { - buffer2[j] = c; - } - } - - buffer2[j] = StringTraits.NullTerminator; - path2 = new U8Span(buffer2); - } - - int i = 0; - bool skipNextSep = false; - int totalLength = prefixLength; - - while (i < path2.Length && !IsNullTerminator(path2[i])) - { - if (IsSeparator(path2[i])) - { - do - { - i++; - } while (i < path2.Length && IsSeparator(path2[i])); - - if (i >= path2.Length || IsNullTerminator(path2[i])) - { - break; - } - - if (!skipNextSep) - { - if (totalLength + 1 == outputBuffer.Length) - { - outputBuffer[totalLength] = StringTraits.NullTerminator; - normalizedLength = totalLength; - - return ResultFs.TooLongPath.Log(); - } - - outputBuffer[totalLength++] = StringTraits.DirectorySeparator; - } - - skipNextSep = false; - } - - int dirLen = 0; - while (path2.Length > i + dirLen && !IsNullTerminator(path2[i + dirLen]) && !IsSeparator(path2[i + dirLen])) - { - dirLen++; - } - - if (IsCurrentDirectory(path2.Slice(i))) - { - skipNextSep = true; - } - else if (IsParentDirectory(path2.Slice(i))) - { - Debug.Assert(IsSeparator(outputBuffer[totalLength - 1])); - Debug.Assert(IsSeparator(outputBuffer[prefixLength])); - - if (totalLength == prefixLength + 1) - { - if (isUncPath) - { - totalLength--; - } - else - { - return ResultFs.DirectoryUnobtainable.Log(); - } - } - else - { - totalLength -= 2; - while (!IsSeparator(outputBuffer[totalLength])) - { - totalLength--; - - if (totalLength == prefixLength) - { - break; - } - } - } - - Debug.Assert(IsSeparator(outputBuffer[totalLength])); - Debug.Assert(totalLength < outputBuffer.Length); - } - else - { - if (totalLength + dirLen + 1 <= outputBuffer.Length) - { - for (int j = 0; j < dirLen; j++) - { - outputBuffer[totalLength++] = path2[i + j]; - } - } - else - { - int copyLen = outputBuffer.Length - 1 - totalLength; - - for (int j = 0; j < copyLen; j++) - { - outputBuffer[totalLength++] = path2[i + j]; - } - - outputBuffer[totalLength] = StringTraits.NullTerminator; - normalizedLength = totalLength; - return ResultFs.TooLongPath.Log(); - } - } - - i += dirLen; - } - - if (skipNextSep) - totalLength--; - - if (totalLength < outputBuffer.Length && totalLength == prefixLength && !isUncPath) - { - outputBuffer[prefixLength] = StringTraits.DirectorySeparator; - totalLength++; - } - - if (totalLength - 1 > outputBuffer.Length) - { - return ResultFs.TooLongPath.Log(); - } - - outputBuffer[totalLength] = StringTraits.NullTerminator; - - Debug.Assert(IsNormalized(out bool isNormalized, new U8Span(outputBuffer), preserveUnc, hasMountName).IsSuccess()); - Debug.Assert(isNormalized); - - normalizedLength = totalLength; - return Result.Success; - } - - public static Result IsNormalized(out bool isNormalized, U8Span path, bool preserveUnc, bool hasMountName) - { - UnsafeHelpers.SkipParamInit(out isNormalized); - U8Span path2 = path; - bool isUncPath = false; - - if (hasMountName) - { - Result rc = ParseMountName(out path2, Span.Empty, out _, path); - if (rc.IsFailure()) return rc; - - if (path2.Length == 0 || !IsSeparator(path2[0])) - return ResultFs.InvalidPathFormat.Log(); - } - - if (preserveUnc) - { - U8Span originalPath = path2; - - Result rc = ParseWindowsPath(out path2, Span.Empty, out _, out bool isNormalizedWin, - true, originalPath, hasMountName); - if (rc.IsFailure()) return rc; - - if (!isNormalizedWin) - { - isNormalized = false; - return Result.Success; - } - - // Path is a UNC path if the new path skips part of the original - isUncPath = originalPath.Value != path2.Value; - - if (isUncPath) - { - if (IsSeparator(originalPath.GetOrNull(0)) && IsSeparator(originalPath.GetOrNull(1))) - { - isNormalized = false; - return Result.Success; - } - - if (IsNullTerminator(path2.GetOrNull(0))) - { - isNormalized = true; - return Result.Success; - } - } - } - - if (path2.IsEmpty()) - return ResultFs.InvalidPathFormat.Log(); - - if (ContainsParentDirectoryAlt(path2)) - { - isNormalized = false; - return Result.Success; - } - - bool pathWasSkipped = path.Value != path2.Value; - var state = NormalizeState.Initial; - - for (int i = 0; i < path2.Length; i++) - { - byte c = path2[i]; - if (IsNullTerminator(c)) break; - - switch (state) - { - // I don't think this first case can actually be triggered, but Nintendo has it there anyway - case NormalizeState.Initial when pathWasSkipped && IsDot(c): state = NormalizeState.Dot; break; - case NormalizeState.Initial when IsSeparator(c): state = NormalizeState.FirstSeparator; break; - case NormalizeState.Initial when !pathWasSkipped: return ResultFs.InvalidPathFormat.Log(); - case NormalizeState.Initial: state = NormalizeState.Normal; break; - - case NormalizeState.Normal when IsSeparator(c): state = NormalizeState.Separator; break; - - case NormalizeState.FirstSeparator when IsDot(c): - case NormalizeState.Separator when IsDot(c): - state = NormalizeState.Dot; - break; - - case NormalizeState.FirstSeparator when IsSeparator(c): - case NormalizeState.Separator when IsSeparator(c): - isNormalized = false; - return Result.Success; - - case NormalizeState.FirstSeparator: - case NormalizeState.Separator: - state = NormalizeState.Normal; - break; - - case NormalizeState.Dot when IsSeparator(c): - isNormalized = false; - return Result.Success; - - case NormalizeState.Dot when IsDot(c): state = NormalizeState.DoubleDot; break; - case NormalizeState.Dot: state = NormalizeState.Normal; break; - - case NormalizeState.DoubleDot when IsSeparator(c): - isNormalized = false; - return Result.Success; - - case NormalizeState.DoubleDot: state = NormalizeState.Normal; break; - } - } - - switch (state) - { - case NormalizeState.Initial: - return ResultFs.InvalidPathFormat.Log(); - case NormalizeState.Normal: - isNormalized = true; - break; - case NormalizeState.FirstSeparator: - isNormalized = !isUncPath; - break; - case NormalizeState.Separator: - case NormalizeState.Dot: - case NormalizeState.DoubleDot: - isNormalized = false; - break; - } - - return Result.Success; - } - - public static bool IsSubpath(U8Span lhs, U8Span rhs) - { - bool isUncLhs = PathUtility.IsUnc(lhs); - bool isUncRhs = PathUtility.IsUnc(rhs); - - if (isUncLhs && !isUncRhs || !isUncLhs && isUncRhs) - return false; - - if (IsSeparator(lhs.GetOrNull(0)) && IsNullTerminator(lhs.GetOrNull(1)) && - IsSeparator(rhs.GetOrNull(0)) && !IsNullTerminator(rhs.GetOrNull(1))) - return true; - - if (IsSeparator(rhs.GetOrNull(0)) && IsNullTerminator(rhs.GetOrNull(1)) && - IsSeparator(lhs.GetOrNull(0)) && !IsNullTerminator(lhs.GetOrNull(1))) - return true; - - for (int i = 0; ; i++) - { - if (IsNullTerminator(lhs.GetOrNull(i))) - { - return IsSeparator(rhs.GetOrNull(i)); - } - else if (IsNullTerminator(rhs.GetOrNull(i))) - { - return IsSeparator(lhs.GetOrNull(i)); - } - else if (lhs.GetOrNull(i) != rhs.GetOrNull(i)) - { - return false; - } - } - } - - private static Result ParseMountName(out U8Span pathAfterMount, Span outMountNameBuffer, - out long mountNameLength, U8Span path) - { - pathAfterMount = default; - UnsafeHelpers.SkipParamInit(out mountNameLength); - - int mountStart = IsSeparator(path.GetOrNull(0)) ? 1 : 0; - int mountEnd; - - int maxMountLength = Math.Min(PathTools.MountNameLengthMax, path.Length - mountStart); - - for (mountEnd = mountStart; mountEnd <= maxMountLength; mountEnd++) - { - byte c = path[mountEnd]; - - if (IsSeparator(c)) - { - pathAfterMount = path; - mountNameLength = 0; - - return Result.Success; - } - - if (IsDriveSeparator(c)) - { - mountEnd++; - break; - } - - if (IsNullTerminator(c)) - { - break; - } - } - - if (mountStart >= mountEnd - 1 || !IsDriveSeparator(path[mountEnd - 1])) - return ResultFs.InvalidPathFormat.Log(); - - for (int i = mountStart; i < mountEnd; i++) - { - if (IsDot(path[i])) - return ResultFs.InvalidCharacter.Log(); - } - - if (!outMountNameBuffer.IsEmpty) - { - if (mountEnd - mountStart > outMountNameBuffer.Length) - return ResultFs.TooLongPath.Log(); - - path.Value.Slice(0, mountEnd).CopyTo(outMountNameBuffer); - } - - pathAfterMount = path.Slice(mountEnd); - mountNameLength = mountEnd - mountStart; - return Result.Success; - } - - private static Result ParseWindowsPath(out U8Span newPath, Span buffer, out long windowsPathLength, - out bool isNormalized, bool checkIfNormalized, U8Span path, bool hasMountName) - { - newPath = default; - windowsPathLength = 0; - isNormalized = checkIfNormalized; - - int winPathLen = 0; - int mountNameLen = 0; - bool skippedMount = false; - - bool needsSeparatorFixup = false; - - if (hasMountName) - { - if (IsSeparator(path.GetOrNull(0)) && IsAltSeparator(path.GetOrNull(1)) && - IsAltSeparator(path.GetOrNull(2))) - { - mountNameLen = 1; - skippedMount = true; - } - else - { - int separatorCount = 0; - - while (IsSeparator(path.GetOrNull(separatorCount))) - { - separatorCount++; - } - - if (separatorCount == 1 || PathUtility.IsWindowsDrive(path.Slice(separatorCount))) - { - mountNameLen = separatorCount; - skippedMount = true; - } - else if (separatorCount > 2 && checkIfNormalized) - { - isNormalized = false; - return Result.Success; - } - else if (separatorCount > 1) - { - mountNameLen = separatorCount - 2; - skippedMount = true; - } - } - } - else - { - if (IsSeparator(path.GetOrNull(0)) && !PathUtility.IsUnc(path)) - { - mountNameLen = 1; - skippedMount = true; - } - } - - U8Span pathTrimmed = path.Slice(mountNameLen); - - if (PathUtility.IsWindowsDrive(pathTrimmed)) - { - int i = 2; - - while (!IsAnySeparator(pathTrimmed.GetOrNull(i)) && !IsNullTerminator(pathTrimmed.GetOrNull(i))) - { - i++; - } - - winPathLen = mountNameLen + i; - - if (!buffer.IsEmpty) - { - if (winPathLen > buffer.Length) - { - return ResultFs.TooLongPath.Log(); - } - - path.Value.Slice(0, winPathLen).CopyTo(buffer); - } - - newPath = path.Slice(winPathLen); - windowsPathLength = winPathLen; - return Result.Success; - } - - // A UNC path should be in the format "\\" host-name "\" share-name [ "\" object-name ] - if (PathUtility.IsUnc(pathTrimmed)) - { - if (IsAnySeparator(pathTrimmed.GetOrNull(2))) - { - return ResultFs.InvalidPathFormat.Log(); - } - - int currentComponentStart = 2; - - for (int i = 2; ; i++) - { - byte c = pathTrimmed.GetOrNull(i); - - if (IsAnySeparator(c)) - { - // Nintendo feels the need to change the '\' separators to '/'s - if (IsAltSeparator(c)) - { - needsSeparatorFixup = true; - - if (checkIfNormalized) - { - isNormalized = false; - return Result.Success; - } - } - - // make sure share-name is not empty - if (currentComponentStart == 2 && IsSeparator(pathTrimmed.GetOrNull(i + 1))) - { - return ResultFs.InvalidPathFormat.Log(); - } - - int componentLength = i - currentComponentStart; - - // neither host-name nor share-name can be "." or ".." - if (componentLength == 1 && IsDot(pathTrimmed[currentComponentStart])) - { - return ResultFs.InvalidPathFormat.Log(); - } - - if (componentLength == 2 && IsDot(pathTrimmed[currentComponentStart]) && - IsDot(pathTrimmed[currentComponentStart + 1])) - { - return ResultFs.InvalidPathFormat.Log(); - } - - // If we're currently processing the share-name path component - if (currentComponentStart != 2) - { - winPathLen = mountNameLen + i; - break; - } - - currentComponentStart = i + 1; - } - else if (c == (byte)'$' || IsDriveSeparator(c)) - { - // '$' and ':' are not allowed in the host-name path component - if (currentComponentStart == 2) - { - return ResultFs.InvalidCharacter.Log(); - } - - // A '$' or ':' must be the last character in share-name - byte nextChar = pathTrimmed.GetOrNull(i + 1); - if (!IsSeparator(nextChar) && !IsNullTerminator(nextChar)) - { - return ResultFs.InvalidPathFormat.Log(); - } - - winPathLen = mountNameLen + i + 1; - break; - } - else if (IsNullTerminator(c)) - { - if (currentComponentStart != 2) - { - int componentLength = i - currentComponentStart; - - // neither host-name nor share-name can be "." or ".." - if (componentLength == 1 && IsDot(pathTrimmed[currentComponentStart])) - { - return ResultFs.InvalidPathFormat.Log(); - } - - if (componentLength == 2 && IsDot(pathTrimmed[currentComponentStart]) && - IsDot(pathTrimmed[currentComponentStart + 1])) - { - return ResultFs.InvalidPathFormat.Log(); - } - - winPathLen = mountNameLen + i; - } - - break; - } - } - - if (!buffer.IsEmpty) - { - if (winPathLen - mountNameLen > buffer.Length) - { - return ResultFs.TooLongPath.Log(); - } - - int outPos = 0; - - if (skippedMount) - { - buffer[0] = StringTraits.DirectorySeparator; - outPos++; - } - - pathTrimmed.Value.Slice(0, winPathLen - mountNameLen).CopyTo(buffer.Slice(outPos)); - - buffer[outPos] = StringTraits.AltDirectorySeparator; - buffer[outPos + 1] = StringTraits.AltDirectorySeparator; - - if (needsSeparatorFixup) - { - for (int i = mountNameLen + 2; i < winPathLen; i++) - { - if (IsAltSeparator(buffer[i])) - { - buffer[i] = StringTraits.DirectorySeparator; - } - } - } - - newPath = path.Slice(winPathLen); - windowsPathLength = outPos + winPathLen - mountNameLen; - return Result.Success; - } - } - newPath = path.Slice(winPathLen); - return Result.Success; - } - - private enum NormalizeState - { - Initial, - Normal, - FirstSeparator, - Separator, - Dot, - DoubleDot - } } } diff --git a/src/LibHac/Fs/PathUtility.cs b/src/LibHac/Fs/PathUtility.cs deleted file mode 100644 index 23fd693f..00000000 --- a/src/LibHac/Fs/PathUtility.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using LibHac.Common; -using static LibHac.Fs.PathTool; - -namespace LibHac.Fs -{ - public static class PathUtility - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsWindowsDrive(U8Span path) - { - return (uint)path.Length > 1 && - (IsDriveSeparator(path[1]) && - IsWindowsDriveCharacter(path[0])); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsWindowsDriveCharacter(byte c) - { - // Mask lowercase letters to uppercase and check if it's in range - return (0b1101_1111 & c) - 'A' <= 'Z' - 'A'; - //return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z'; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsUnc(U8Span path) - { - return (uint)path.Length > 1 && - (IsSeparator(path.GetUnsafe(0)) && IsSeparator(path.GetUnsafe(1)) || - IsAltSeparator(path.GetUnsafe(0)) && IsAltSeparator(path.GetUnsafe(1))); - } - - public static int GetWindowsPathSkipLength(U8Span path) - { - if (IsWindowsDrive(path)) - return 2; - - if (!IsUnc(path)) - return 0; - - for (int i = 2; i < path.Length && !IsNullTerminator(path[i]); i++) - { - byte c = path[i]; - if (c == (byte)'$' || IsDriveSeparator(c)) - { - return i + 1; - } - } - - return 0; - } - - public static Result VerifyPath(U8Span path, int maxPathLength, int maxNameLength) - { - Debug.Assert(!path.IsNull()); - - int nameLength = 0; - - for (int i = 0; i < path.Length && i <= maxPathLength && nameLength <= maxNameLength; i++) - { - byte c = path[i]; - - if (IsNullTerminator(c)) - return Result.Success; - - // todo: Compare path based on their Unicode code points - - if (c == ':' || c == '*' || c == '?' || c == '<' || c == '>' || c == '|') - return ResultFs.InvalidCharacter.Log(); - - nameLength++; - if (c == '\\' || c == '/') - { - nameLength = 0; - } - } - - return ResultFs.TooLongPath.Log(); - } - - public static void Replace(Span buffer, byte oldChar, byte newChar) - { - for (int i = 0; i < buffer.Length; i++) - { - if (buffer[i] == oldChar) - { - buffer[i] = newChar; - } - } - } - - /// - /// Performs the extra functions that nn::fs::FspPathPrintf does on the string buffer. - /// - /// The string builder to process. - /// The of the operation. - public static Result ToSfPath(in this U8StringBuilder builder) - { - if (builder.Overflowed) - return ResultFs.TooLongPath.Log(); - - Replace(builder.Buffer.Slice(builder.Capacity), - StringTraits.AltDirectorySeparator, - StringTraits.DirectorySeparator); - - return Result.Success; - } - } -} diff --git a/src/LibHac/Fs/Shim/Bis.cs b/src/LibHac/Fs/Shim/Bis.cs index 5ba8fd60..b7c6e3dc 100644 --- a/src/LibHac/Fs/Shim/Bis.cs +++ b/src/LibHac/Fs/Shim/Bis.cs @@ -169,7 +169,7 @@ namespace LibHac.Fs.Shim if (pathLen > 0) { - byte endingSeparator = PathTool.IsSeparator(rootPath[pathLen - 1]) + byte endingSeparator = rootPath[pathLen - 1] == StringTraits.DirectorySeparator ? StringTraits.NullTerminator : StringTraits.DirectorySeparator; diff --git a/src/LibHac/Fs/Shim/Host.cs b/src/LibHac/Fs/Shim/Host.cs index 9581ad33..0695365c 100644 --- a/src/LibHac/Fs/Shim/Host.cs +++ b/src/LibHac/Fs/Shim/Host.cs @@ -8,6 +8,7 @@ using LibHac.FsSrv.Sf; using LibHac.FsSystem; using LibHac.Os; using LibHac.Util; +using static LibHac.Fs.StringTraits; using static LibHac.Fs.Impl.AccessLogStrings; using static LibHac.Fs.Impl.CommonMountNames; using IFileSystem = LibHac.Fs.Fsa.IFileSystem; @@ -74,9 +75,9 @@ namespace LibHac.Fs.Shim StringUtils.Copy(_path.Str, path); int pathLength = StringUtils.GetLength(_path.Str); - if (pathLength != 0 && _path.Str[pathLength - 1] == StringTraits.DirectorySeparator) + if (pathLength != 0 && _path.Str[pathLength - 1] == DirectorySeparator) { - _path.Str[pathLength - 1] = StringTraits.NullTerminator; + _path.Str[pathLength - 1] = NullTerminator; } } @@ -138,7 +139,7 @@ namespace LibHac.Fs.Shim if (path.IsNull()) return ResultFs.NullptrArgument.Log(); - if (PathUtility.IsWindowsDrive(mountName)) + if (WindowsPath.IsWindowsDrive(mountName)) return ResultFs.InvalidMountName.Log(); if (fs.Impl.IsUsedReservedMountName(mountName)) @@ -147,7 +148,7 @@ namespace LibHac.Fs.Shim bool needsTrailingSeparator = false; int pathLength = StringUtils.GetLength(path, PathTools.MaxPathLength + 1); - if (pathLength != 0 && PathTool.IsSeparator(path[pathLength - 1])) + if (pathLength != 0 && path[pathLength - 1] == DirectorySeparator) { needsTrailingSeparator = true; pathLength++; @@ -159,22 +160,22 @@ namespace LibHac.Fs.Shim Unsafe.SkipInit(out FsPath fullPath); var sb = new U8StringBuilder(fullPath.Str); - sb.Append(StringTraits.DirectorySeparator).Append(path); + sb.Append(DirectorySeparator).Append(path); if (needsTrailingSeparator) { - sb.Append(StringTraits.DirectorySeparator); + sb.Append(DirectorySeparator); } if (sb.Overflowed) return ResultFs.TooLongPath.Log(); // If the input path begins with "//", change any leading '/' characters to '\' - if (PathTool.IsSeparator(fullPath.Str[1]) && PathTool.IsSeparator(fullPath.Str[2])) + if (fullPath.Str[1] == DirectorySeparator && fullPath.Str[2] == DirectorySeparator) { - for (int i = 1; PathTool.IsSeparator(fullPath.Str[i]); i++) + for (int i = 1; fullPath.Str[i] == DirectorySeparator; i++) { - fullPath.Str[i] = StringTraits.AltDirectorySeparator; + fullPath.Str[i] = AltDirectorySeparator; } } diff --git a/src/LibHac/FsSrv/BaseFileSystemService.cs b/src/LibHac/FsSrv/BaseFileSystemService.cs index d5492012..2ae389a8 100644 --- a/src/LibHac/FsSrv/BaseFileSystemService.cs +++ b/src/LibHac/FsSrv/BaseFileSystemService.cs @@ -6,6 +6,7 @@ using LibHac.FsSrv.Sf; using LibHac.Sf; using IFileSystem = LibHac.Fs.Fsa.IFileSystem; using IFileSystemSf = LibHac.FsSrv.Sf.IFileSystem; +using PathNormalizer = LibHac.FsSrv.Impl.PathNormalizer; namespace LibHac.FsSrv { @@ -112,7 +113,7 @@ namespace LibHac.FsSrv return ResultFs.PermissionDenied.Log(); // Normalize the path - var normalizer = new PathNormalizer(rootPath, PathNormalizer.Option.AcceptEmpty); + using var normalizer = new PathNormalizer(rootPath, PathNormalizer.Option.AcceptEmpty); if (normalizer.Result.IsFailure()) return normalizer.Result; ReferenceCountedDisposable fs = null; diff --git a/src/LibHac/FsSrv/Impl/FileSystemInterfaceAdapter.cs b/src/LibHac/FsSrv/Impl/FileSystemInterfaceAdapter.cs index 851d7464..2e99bd62 100644 --- a/src/LibHac/FsSrv/Impl/FileSystemInterfaceAdapter.cs +++ b/src/LibHac/FsSrv/Impl/FileSystemInterfaceAdapter.cs @@ -88,7 +88,7 @@ namespace LibHac.FsSrv.Impl if (size < 0) return ResultFs.InvalidSize.Log(); - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; return BaseFileSystem.Target.CreateFile(normalizer.Path, size, (CreateFileOptions)option); @@ -96,7 +96,7 @@ namespace LibHac.FsSrv.Impl public Result DeleteFile(in Path path) { - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; return BaseFileSystem.Target.DeleteFile(normalizer.Path); @@ -104,7 +104,7 @@ namespace LibHac.FsSrv.Impl public Result CreateDirectory(in Path path) { - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; if (StringUtils.Compare(RootDir, normalizer.Path) == 0) @@ -115,7 +115,7 @@ namespace LibHac.FsSrv.Impl public Result DeleteDirectory(in Path path) { - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; if (StringUtils.Compare(RootDir, normalizer.Path) == 0) @@ -126,7 +126,7 @@ namespace LibHac.FsSrv.Impl public Result DeleteDirectoryRecursively(in Path path) { - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; if (StringUtils.Compare(RootDir, normalizer.Path) == 0) @@ -137,10 +137,10 @@ namespace LibHac.FsSrv.Impl public Result RenameFile(in Path oldPath, in Path newPath) { - var normalizerOldPath = new PathNormalizer(new U8Span(oldPath.Str), GetPathNormalizerOption()); + using var normalizerOldPath = new PathNormalizer(new U8Span(oldPath.Str), GetPathNormalizerOption()); if (normalizerOldPath.Result.IsFailure()) return normalizerOldPath.Result; - var normalizerNewPath = new PathNormalizer(new U8Span(newPath.Str), GetPathNormalizerOption()); + using var normalizerNewPath = new PathNormalizer(new U8Span(newPath.Str), GetPathNormalizerOption()); if (normalizerNewPath.Result.IsFailure()) return normalizerNewPath.Result; return BaseFileSystem.Target.RenameFile(new U8Span(normalizerOldPath.Path), @@ -149,13 +149,13 @@ namespace LibHac.FsSrv.Impl public Result RenameDirectory(in Path oldPath, in Path newPath) { - var normalizerOldPath = new PathNormalizer(new U8Span(oldPath.Str), GetPathNormalizerOption()); + using var normalizerOldPath = new PathNormalizer(new U8Span(oldPath.Str), GetPathNormalizerOption()); if (normalizerOldPath.Result.IsFailure()) return normalizerOldPath.Result; - var normalizerNewPath = new PathNormalizer(new U8Span(newPath.Str), GetPathNormalizerOption()); + using var normalizerNewPath = new PathNormalizer(new U8Span(newPath.Str), GetPathNormalizerOption()); if (normalizerNewPath.Result.IsFailure()) return normalizerNewPath.Result; - if (PathTool.IsSubpath(normalizerOldPath.Path, normalizerNewPath.Path)) + if (PathUtility.IsSubPath(normalizerOldPath.Path, normalizerNewPath.Path)) return ResultFs.DirectoryNotRenamable.Log(); return BaseFileSystem.Target.RenameDirectory(normalizerOldPath.Path, normalizerNewPath.Path); @@ -165,7 +165,7 @@ namespace LibHac.FsSrv.Impl { UnsafeHelpers.SkipParamInit(out entryType); - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; ref DirectoryEntryType type = ref Unsafe.As(ref entryType); @@ -178,7 +178,7 @@ namespace LibHac.FsSrv.Impl const int maxTryCount = 2; UnsafeHelpers.SkipParamInit(out file); - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; Result rc = Result.Success; @@ -207,7 +207,7 @@ namespace LibHac.FsSrv.Impl const int maxTryCount = 2; UnsafeHelpers.SkipParamInit(out directory); - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; Result rc = Result.Success; @@ -240,7 +240,7 @@ namespace LibHac.FsSrv.Impl { UnsafeHelpers.SkipParamInit(out freeSpace); - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; return BaseFileSystem.Target.GetFreeSpaceSize(out freeSpace, normalizer.Path); @@ -250,7 +250,7 @@ namespace LibHac.FsSrv.Impl { UnsafeHelpers.SkipParamInit(out totalSpace); - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; return BaseFileSystem.Target.GetTotalSpaceSize(out totalSpace, normalizer.Path); @@ -258,7 +258,7 @@ namespace LibHac.FsSrv.Impl public Result CleanDirectoryRecursively(in Path path) { - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; return BaseFileSystem.Target.CleanDirectoryRecursively(normalizer.Path); @@ -268,7 +268,7 @@ namespace LibHac.FsSrv.Impl { UnsafeHelpers.SkipParamInit(out timeStamp); - var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); + using var normalizer = new PathNormalizer(new U8Span(path.Str), GetPathNormalizerOption()); if (normalizer.Result.IsFailure()) return normalizer.Result; return BaseFileSystem.Target.GetFileTimeStampRaw(out timeStamp, normalizer.Path); diff --git a/src/LibHac/FsSrv/Impl/PathNormalizer.cs b/src/LibHac/FsSrv/Impl/PathNormalizer.cs new file mode 100644 index 00000000..ffc33d54 --- /dev/null +++ b/src/LibHac/FsSrv/Impl/PathNormalizer.cs @@ -0,0 +1,106 @@ +using System; +using System.Buffers; +using LibHac.Common; +using LibHac.Diag; +using LibHac.Fs; +using LibHac.Util; + +namespace LibHac.FsSrv.Impl +{ + public ref struct PathNormalizer + { + private readonly U8Span _path; + private byte[] _rentedArray; + + public U8Span Path => _path; + + public Result Result { get; } + + public PathNormalizer(U8Span path, Option option) + { + + if (option.HasFlag(Option.AcceptEmpty) && path.IsEmpty()) + { + _path = path; + _rentedArray = null; + Result = Result.Success; + } + else + { + bool preserveUnc = option.HasFlag(Option.PreserveUnc); + bool preserveTrailingSeparator = option.HasFlag(Option.PreserveTrailingSeparator); + bool hasMountName = option.HasFlag(Option.HasMountName); + Result = Normalize(out _path, out _rentedArray, path, preserveUnc, preserveTrailingSeparator, + hasMountName); + } + } + + public void Dispose() + { + if (_rentedArray is not null) + ArrayPool.Shared.Return(_rentedArray); + } + + private static Result Normalize(out U8Span normalizedPath, out byte[] rentedBuffer, U8Span path, + bool preserveUnc, bool preserveTailSeparator, bool hasMountName) + { + Assert.SdkRequiresNotNullOut(out rentedBuffer); + + normalizedPath = default; + rentedBuffer = null; + + Result rc = Fs.PathNormalizer.IsNormalized(out bool isNormalized, path, preserveUnc, hasMountName); + if (rc.IsFailure()) return rc; + + if (isNormalized) + { + normalizedPath = path; + } + else + { + byte[] buffer = null; + try + { + buffer = ArrayPool.Shared.Rent(PathTool.EntryNameLengthMax + 1); + + rc = Fs.PathNormalizer.Normalize(buffer.AsSpan(0, PathTool.EntryNameLengthMax + 1), + out long normalizedLength, path, preserveUnc, hasMountName); + if (rc.IsFailure()) return rc; + + // Add the tail separator if needed + if (preserveTailSeparator) + { + int pathLength = StringUtils.GetLength(path, PathTool.EntryNameLengthMax); + if (Fs.PathNormalizer.IsSeparator(path[pathLength - 1]) && + !Fs.PathNormalizer.IsSeparator(buffer[normalizedLength - 1])) + { + Assert.SdkLess(normalizedLength, PathTool.EntryNameLengthMax); + + buffer[(int)normalizedLength] = StringTraits.DirectorySeparator; + buffer[(int)normalizedLength + 1] = StringTraits.NullTerminator; + } + } + + normalizedPath = new U8Span(Shared.Move(ref buffer)); + } + finally + { + if (buffer is not null) + ArrayPool.Shared.Return(buffer); + } + } + + return Result.Success; + } + + [Flags] + public enum Option + { + None = 0, + PreserveUnc = (1 << 0), + PreserveTrailingSeparator = (1 << 1), + HasMountName = (1 << 2), + AcceptEmpty = (1 << 3) + } + } +} diff --git a/src/LibHac/FsSrv/NcaFileSystemService.cs b/src/LibHac/FsSrv/NcaFileSystemService.cs index 4cff02b3..78eac53e 100644 --- a/src/LibHac/FsSrv/NcaFileSystemService.cs +++ b/src/LibHac/FsSrv/NcaFileSystemService.cs @@ -15,6 +15,7 @@ using IStorage = LibHac.Fs.IStorage; using IFileSystemSf = LibHac.FsSrv.Sf.IFileSystem; using IStorageSf = LibHac.FsSrv.Sf.IStorage; using Path = LibHac.Lr.Path; +using PathNormalizer = LibHac.FsSrv.Impl.PathNormalizer; namespace LibHac.FsSrv { @@ -141,7 +142,7 @@ namespace LibHac.FsSrv } // Normalize the path to the patch file system - var patchPathNormalizer = new PathNormalizer(patchPath, GetPathNormalizerOptions(patchPath)); + using var patchPathNormalizer = new PathNormalizer(patchPath, GetPathNormalizerOptions(patchPath)); if (patchPathNormalizer.Result.IsFailure()) return patchPathNormalizer.Result; if (patchResult.IsFailure()) @@ -250,7 +251,7 @@ namespace LibHac.FsSrv bool canMountSystemDataPrivate = ac.GetAccessibilityFor(AccessibilityType.MountSystemDataPrivate).CanRead; - var normalizer = new PathNormalizer(path, GetPathNormalizerOptions(path)); + using var normalizer = new PathNormalizer(path, GetPathNormalizerOptions(path)); if (normalizer.Result.IsFailure()) return normalizer.Result; ReferenceCountedDisposable fs = null; @@ -348,7 +349,7 @@ namespace LibHac.FsSrv rc = ServiceImpl.ResolveProgramPath(out Path programPath, programId, storageId); if (rc.IsFailure()) return rc; - var normalizer = new PathNormalizer(programPath, GetPathNormalizerOptions(programPath)); + using var normalizer = new PathNormalizer(programPath, GetPathNormalizerOptions(programPath)); if (normalizer.Result.IsFailure()) return normalizer.Result; rc = ServiceImpl.GetRightsId(out RightsId tempRightsId, out _, normalizer.Path, programId); @@ -370,7 +371,7 @@ namespace LibHac.FsSrv if (!programInfo.AccessControl.CanCall(OperationType.GetRightsId)) return ResultFs.PermissionDenied.Log(); - var normalizer = new PathNormalizer(path, GetPathNormalizerOptions(path)); + using var normalizer = new PathNormalizer(path, GetPathNormalizerOptions(path)); if (normalizer.Result.IsFailure()) return normalizer.Result; rc = ServiceImpl.GetRightsId(out RightsId tempRightsId, out byte tempKeyGeneration, normalizer.Path, @@ -396,7 +397,7 @@ namespace LibHac.FsSrv Result rc = ServiceImpl.ResolveRomPath(out Path romPath, programId, storageId); if (rc.IsFailure()) return rc; - var normalizer = new PathNormalizer(romPath, GetPathNormalizerOptions(romPath)); + using var normalizer = new PathNormalizer(romPath, GetPathNormalizerOptions(romPath)); if (normalizer.Result.IsFailure()) return normalizer.Result; isHostFs = IsHostFs(romPath); @@ -501,7 +502,7 @@ namespace LibHac.FsSrv rc = ServiceImpl.ResolveRomPath(out Path romPath, programInfo.ProgramIdValue, programInfo.StorageId); if (rc.IsFailure()) return rc; - var normalizer = new PathNormalizer(romPath, GetPathNormalizerOptions(romPath)); + using var normalizer = new PathNormalizer(romPath, GetPathNormalizerOptions(romPath)); if (normalizer.Result.IsFailure()) return normalizer.Result; return ServiceImpl.RegisterUpdatePartition(programInfo.ProgramIdValue, normalizer.Path); @@ -623,7 +624,7 @@ namespace LibHac.FsSrv { // Set the PreserveUnc flag if the path is on the host file system PathNormalizer.Option hostOption = IsHostFs(path) ? PathNormalizer.Option.PreserveUnc : PathNormalizer.Option.None; - return PathNormalizer.Option.HasMountName | PathNormalizer.Option.PreserveTailSeparator | hostOption; + return PathNormalizer.Option.HasMountName | PathNormalizer.Option.PreserveTrailingSeparator | hostOption; } private bool IsHostFs(U8Span path) diff --git a/src/LibHac/FsSrv/PathNormalizer.cs b/src/LibHac/FsSrv/PathNormalizer.cs deleted file mode 100644 index 8c5004ea..00000000 --- a/src/LibHac/FsSrv/PathNormalizer.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using LibHac.Common; -using LibHac.Fs; -using LibHac.FsSystem; -using LibHac.Util; - -namespace LibHac.FsSrv -{ - public ref struct PathNormalizer - { - private readonly U8Span _path; - public U8Span Path => _path; - - public Result Result { get; } - - public PathNormalizer(U8Span path, Option option) - { - if (option.HasFlag(Option.AcceptEmpty) && path.IsEmpty()) - { - _path = path; - Result = Result.Success; - } - else - { - bool preserveUnc = option.HasFlag(Option.PreserveUnc); - bool preserveTailSeparator = option.HasFlag(Option.PreserveTailSeparator); - bool hasMountName = option.HasFlag(Option.HasMountName); - Result = Normalize(out _path, path, preserveUnc, preserveTailSeparator, hasMountName); - } - } - - private static Result Normalize(out U8Span normalizedPath, U8Span path, bool preserveUnc, - bool preserveTailSeparator, bool hasMountName) - { - normalizedPath = default; - - Result rc = PathTool.IsNormalized(out bool isNormalized, path, preserveUnc, hasMountName); - if (rc.IsFailure()) return rc; - - if (isNormalized) - { - normalizedPath = path; - } - else - { - byte[] buffer = new byte[PathTools.MaxPathLength + 1]; - - rc = PathTool.Normalize(buffer, out long normalizedLength, path, preserveUnc, hasMountName); - if (rc.IsFailure()) return rc; - - // GetLength is capped at MaxPathLength bytes to leave room for the null terminator - if (preserveTailSeparator && - PathTool.IsSeparator(path[StringUtils.GetLength(path, PathTools.MaxPathLength) - 1])) - { - buffer[(int)normalizedLength] = StringTraits.DirectorySeparator; - buffer[(int)normalizedLength + 1] = StringTraits.NullTerminator; - } - - normalizedPath = new U8Span(buffer); - } - - return Result.Success; - } - - [Flags] - public enum Option - { - None = 0, - PreserveUnc = (1 << 0), - PreserveTailSeparator = (1 << 1), - HasMountName = (1 << 2), - AcceptEmpty = (1 << 3) - } - } -} diff --git a/src/LibHac/FsSrv/SaveDataFileSystemService.cs b/src/LibHac/FsSrv/SaveDataFileSystemService.cs index 0bc1d22c..fe785669 100644 --- a/src/LibHac/FsSrv/SaveDataFileSystemService.cs +++ b/src/LibHac/FsSrv/SaveDataFileSystemService.cs @@ -14,6 +14,7 @@ using IFileSystem = LibHac.Fs.Fsa.IFileSystem; using IFileSystemSf = LibHac.FsSrv.Sf.IFileSystem; using IFile = LibHac.Fs.Fsa.IFile; using IFileSf = LibHac.FsSrv.Sf.IFile; +using PathNormalizer = LibHac.FsSrv.Impl.PathNormalizer; using Utility = LibHac.FsSystem.Utility; namespace LibHac.FsSrv @@ -567,7 +568,7 @@ namespace LibHac.FsSrv } } - var normalizer = new PathNormalizer(SaveDataRootPath, PathNormalizer.Option.PreserveUnc); + using var normalizer = new PathNormalizer(SaveDataRootPath, PathNormalizer.Option.PreserveUnc); if (normalizer.Result.IsFailure()) return normalizer.Result; diff --git a/src/LibHac/FsSrv/Util.cs b/src/LibHac/FsSrv/Util.cs index 51fd0c2f..356bb92f 100644 --- a/src/LibHac/FsSrv/Util.cs +++ b/src/LibHac/FsSrv/Util.cs @@ -2,6 +2,7 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; +using PathNormalizer = LibHac.FsSrv.Impl.PathNormalizer; namespace LibHac.FsSrv { @@ -58,13 +59,13 @@ namespace LibHac.FsSrv if (path2.IsEmpty()) return Result.Success; - int skipLength = PathUtility.GetWindowsPathSkipLength(path2); + int skipLength = WindowsPath.GetWindowsPathSkipLength(path2); int remainingLength = PathTools.MaxPathLength - skipLength; - Result rc = PathUtility.VerifyPath(path2.Slice(skipLength), remainingLength, remainingLength); + Result rc = PathUtility.VerifyPath(null, path2.Slice(skipLength), remainingLength, remainingLength); if (rc.IsFailure()) return rc; - var normalizer = new PathNormalizer(path, PathNormalizer.Option.PreserveUnc); + using var normalizer = new PathNormalizer(path, PathNormalizer.Option.PreserveUnc); return normalizer.Result; } diff --git a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs index 1203a43b..53cc1fdf 100644 --- a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs +++ b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs @@ -360,7 +360,7 @@ namespace LibHac.FsSystem StringUtils.Copy(outPath, WorkingDirectoryBytes); outPath[outPath.Length - 1] = StringTraits.NullTerminator; - return PathTool.Normalize(outPath.Slice(2), out _, relativePath, false, false); + return PathNormalizer.Normalize(outPath.Slice(2), out _, relativePath, false, false); } /// diff --git a/src/LibHac/FsSystem/LocalFileSystem.cs b/src/LibHac/FsSystem/LocalFileSystem.cs index 5e4ba88d..718017c7 100644 --- a/src/LibHac/FsSystem/LocalFileSystem.cs +++ b/src/LibHac/FsSystem/LocalFileSystem.cs @@ -39,7 +39,7 @@ namespace LibHac.FsSystem Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; fullPath = PathTools.Combine(BasePath, normalizedPath.ToString()); @@ -51,13 +51,13 @@ namespace LibHac.FsSystem Unsafe.SkipInit(out FsPath normalizedPath1); Unsafe.SkipInit(out FsPath normalizedPath2); - Result rc = PathTool.Normalize(normalizedPath1.Str, out _, path1, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath1.Str, out _, path1, false, false); if (rc.IsFailure()) return rc; - rc = PathTool.Normalize(normalizedPath2.Str, out _, path2, false, false); + rc = PathNormalizer.Normalize(normalizedPath2.Str, out _, path2, false, false); if (rc.IsFailure()) return rc; - if (PathTool.IsSubpath(normalizedPath1, normalizedPath2)) + if (PathUtility.IsSubPath(normalizedPath1, normalizedPath2)) { return ResultFs.DirectoryNotRenamable.Log(); } diff --git a/src/LibHac/FsSystem/Save/SaveDataFileSystemCore.cs b/src/LibHac/FsSystem/Save/SaveDataFileSystemCore.cs index d6ec8512..bf6c18bc 100644 --- a/src/LibHac/FsSystem/Save/SaveDataFileSystemCore.cs +++ b/src/LibHac/FsSystem/Save/SaveDataFileSystemCore.cs @@ -35,7 +35,7 @@ namespace LibHac.FsSystem.Save { Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; FileTable.AddDirectory(normalizedPath); @@ -47,7 +47,7 @@ namespace LibHac.FsSystem.Save { Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; if (size == 0) @@ -77,7 +77,7 @@ namespace LibHac.FsSystem.Save { Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; FileTable.DeleteDirectory(normalizedPath); @@ -89,7 +89,7 @@ namespace LibHac.FsSystem.Save { Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; rc = CleanDirectoryRecursively(normalizedPath); @@ -105,7 +105,7 @@ namespace LibHac.FsSystem.Save { Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; FileSystemExtensions.CleanDirectoryRecursivelyGeneric(this, normalizedPath.ToString()); @@ -117,7 +117,7 @@ namespace LibHac.FsSystem.Save { Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; if (!FileTable.TryOpenFile(normalizedPath, out SaveFileInfo fileInfo)) @@ -141,7 +141,7 @@ namespace LibHac.FsSystem.Save Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; if (!FileTable.TryOpenDirectory(normalizedPath, out SaveFindPosition position)) @@ -160,7 +160,7 @@ namespace LibHac.FsSystem.Save Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; if (!FileTable.TryOpenFile(normalizedPath, out SaveFileInfo fileInfo)) @@ -180,10 +180,10 @@ namespace LibHac.FsSystem.Save Unsafe.SkipInit(out FsPath normalizedCurrentPath); Unsafe.SkipInit(out FsPath normalizedNewPath); - Result rc = PathTool.Normalize(normalizedCurrentPath.Str, out _, oldPath, false, false); + Result rc = PathNormalizer.Normalize(normalizedCurrentPath.Str, out _, oldPath, false, false); if (rc.IsFailure()) return rc; - rc = PathTool.Normalize(normalizedNewPath.Str, out _, newPath, false, false); + rc = PathNormalizer.Normalize(normalizedNewPath.Str, out _, newPath, false, false); if (rc.IsFailure()) return rc; return FileTable.RenameDirectory(normalizedCurrentPath, normalizedNewPath); @@ -194,10 +194,10 @@ namespace LibHac.FsSystem.Save Unsafe.SkipInit(out FsPath normalizedCurrentPath); Unsafe.SkipInit(out FsPath normalizedNewPath); - Result rc = PathTool.Normalize(normalizedCurrentPath.Str, out _, oldPath, false, false); + Result rc = PathNormalizer.Normalize(normalizedCurrentPath.Str, out _, oldPath, false, false); if (rc.IsFailure()) return rc; - rc = PathTool.Normalize(normalizedNewPath.Str, out _, newPath, false, false); + rc = PathNormalizer.Normalize(normalizedNewPath.Str, out _, newPath, false, false); if (rc.IsFailure()) return rc; FileTable.RenameFile(normalizedCurrentPath, normalizedNewPath); @@ -211,7 +211,7 @@ namespace LibHac.FsSystem.Save Unsafe.SkipInit(out FsPath normalizedPath); - Result rc = PathTool.Normalize(normalizedPath.Str, out _, path, false, false); + Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false); if (rc.IsFailure()) return rc; if (FileTable.TryOpenFile(normalizedPath, out SaveFileInfo _)) diff --git a/src/LibHac/FsSystem/SubdirectoryFileSystem.cs b/src/LibHac/FsSystem/SubdirectoryFileSystem.cs index 3acf365b..a3bc8e69 100644 --- a/src/LibHac/FsSystem/SubdirectoryFileSystem.cs +++ b/src/LibHac/FsSystem/SubdirectoryFileSystem.cs @@ -61,11 +61,11 @@ namespace LibHac.FsSystem Span normalizedPath = stackalloc byte[PathTools.MaxPathLength + 2]; - Result rc = PathTool.Normalize(normalizedPath, out long normalizedPathLen, rootPath, PreserveUnc, false); + Result rc = PathNormalizer.Normalize(normalizedPath, out long normalizedPathLen, rootPath, PreserveUnc, false); if (rc.IsFailure()) return rc; // Ensure a trailing separator - if (!PathTool.IsSeparator(normalizedPath[(int)normalizedPathLen - 1])) + if (!PathNormalizer.IsSeparator(normalizedPath[(int)normalizedPathLen - 1])) { Debug.Assert(normalizedPathLen + 2 <= normalizedPath.Length); @@ -90,7 +90,7 @@ namespace LibHac.FsSystem RootPath.Value.CopyTo(outPath); // Copy the normalized relative path to the output - return PathTool.Normalize(outPath.Slice(RootPath.Length - 2), out _, relativePath, PreserveUnc, false); + return PathNormalizer.Normalize(outPath.Slice(RootPath.Length - 2), out _, relativePath, PreserveUnc, false); } protected override Result DoCreateDirectory(U8Span path) diff --git a/src/LibHac/FsSystem/Utility.cs b/src/LibHac/FsSystem/Utility.cs index 2fc3a4cf..89ac8b56 100644 --- a/src/LibHac/FsSystem/Utility.cs +++ b/src/LibHac/FsSystem/Utility.cs @@ -28,7 +28,7 @@ namespace LibHac.FsSystem // Copy root path in, add a / if necessary. rootPath.Value.Slice(0, rootPathLen).CopyTo(workPath); - if (!PathTool.IsSeparator(workPath[rootPathLen - 1])) + if (workPath[rootPathLen - 1] != StringTraits.DirectorySeparator) { workPath[rootPathLen++] = StringTraits.DirectorySeparator; } @@ -158,7 +158,7 @@ namespace LibHac.FsSystem // Find previous separator, add null terminator int cur = len - 2; - while (!PathTool.IsSeparator(SpanHelpers.AsByteSpan(ref destPathBuf)[cur]) && cur > 0) + while (SpanHelpers.AsByteSpan(ref destPathBuf)[cur] != StringTraits.DirectorySeparator && cur > 0) { cur--; } diff --git a/tests/LibHac.Tests/DebugAssertHandler.cs b/tests/LibHac.Tests/DebugAssertHandler.cs index e7819464..17a29969 100644 --- a/tests/LibHac.Tests/DebugAssertHandler.cs +++ b/tests/LibHac.Tests/DebugAssertHandler.cs @@ -6,7 +6,7 @@ namespace LibHac.Tests { public override void Fail(string message, string detailMessage) { - // throw new System.NotImplementedException(message + detailMessage); + throw new LibHacException(message + detailMessage); } } } diff --git a/tests/LibHac.Tests/Fs/PathToolTestGenerator.cpp b/tests/LibHac.Tests/Fs/PathToolTestGenerator.cpp index cb81a948..d873f840 100644 --- a/tests/LibHac.Tests/Fs/PathToolTestGenerator.cpp +++ b/tests/LibHac.Tests/Fs/PathToolTestGenerator.cpp @@ -15,13 +15,24 @@ namespace nn::fs::PathTool { nn::Result Normalize(char* buffer, uint64_t* outNormalizedPathLength, const char* path, uint64_t bufferLength, bool preserveUnc); nn::Result IsNormalized(bool* outIsNormalized, const char* path); - // SDK >= 7 + // SDK >= 7 < 11 nn::Result Normalize(char* buffer, uint64_t* outNormalizedPathLength, const char* path, uint64_t bufferLength, bool preserveUnc, bool hasMountName); nn::Result IsNormalized(bool* outIsNormalized, const char* path, bool preserveUnc, bool hasMountName); bool IsSubpath(const char* path1, const char* path2); } +namespace nn::fs { + // SDK >= 11 + bool IsSubPath(const char* path1, const char* path2); +} + +namespace nn::fs::PathNormalizer { + // SDK >= 11 + nn::Result Normalize(char* buffer, uint64_t* outNormalizedPathLength, const char* path, uint64_t bufferLength, bool preserveUnc, bool hasMountName); + nn::Result IsNormalized(bool* outIsNormalized, const char* path, bool preserveUnc, bool hasMountName); +} + void* allocate(size_t size) { void* ptr = malloc(size); @@ -67,7 +78,7 @@ void CreateNormalizeTestItem(char const* path, bool preserveUnc, bool hasMountNa //svcOutputDebugString(path, strnlen(path, 0x200)); - nn::Result result = nn::fs::PathTool::Normalize(normalized, &normalizedLen, path, 0x200, preserveUnc, hasMountName); + nn::Result result = nn::fs::PathNormalizer::Normalize(normalized, &normalizedLen, path, 0x200, preserveUnc, hasMountName); const char* preserveUncStr = preserveUnc ? "true" : "false"; const char* hasMountNameStr = hasMountName ? "true" : "false"; @@ -78,7 +89,7 @@ void CreateNormalizeTestItem(char const* path, bool preserveUnc, bool hasMountNa void CreateIsNormalizedTestItem(char const* path, bool preserveUnc, bool hasMountName) { bool isNormalized = false; - nn::Result result = nn::fs::PathTool::IsNormalized(&isNormalized, path, preserveUnc, hasMountName); + nn::Result result = nn::fs::PathNormalizer::IsNormalized(&isNormalized, path, preserveUnc, hasMountName); const char* preserveUncStr = preserveUnc ? "true" : "false"; const char* hasMountNameStr = hasMountName ? "true" : "false"; @@ -88,7 +99,7 @@ void CreateIsNormalizedTestItem(char const* path, bool preserveUnc, bool hasMoun } void CreateIsSubpathTestItem(const char* path1, const char* path2) { - bool result = nn::fs::PathTool::IsSubpath(path1, path2); + bool result = nn::fs::IsSubPath(path1, path2); const char* resultStr = result ? "true" : "false"; BufPos += sprintf(&Buf[BufPos], "new object[] {@\"%s\", @\"%s\", %s},\n", diff --git a/tests/LibHac.Tests/Fs/PathToolTests.cs b/tests/LibHac.Tests/Fs/PathToolTests.cs index c6ed8ef7..e2e6dd2c 100644 --- a/tests/LibHac.Tests/Fs/PathToolTests.cs +++ b/tests/LibHac.Tests/Fs/PathToolTests.cs @@ -14,7 +14,7 @@ namespace LibHac.Tests.Fs { byte[] buffer = new byte[0x301]; - Result result = PathTool.Normalize(buffer, out long normalizedLength, path.ToU8Span(), preserveUnc, + Result result = PathNormalizer.Normalize(buffer, out long normalizedLength, path.ToU8Span(), preserveUnc, hasMountName); Assert.Equal(expectedResult, result); @@ -27,7 +27,7 @@ namespace LibHac.Tests.Fs public static void IsNormalized(string path, bool preserveUnc, bool hasMountName, bool expectedIsNormalized, Result expectedResult) { - Result result = PathTool.IsNormalized(out bool isNormalized, path.ToU8Span(), preserveUnc, hasMountName); + Result result = PathNormalizer.IsNormalized(out bool isNormalized, path.ToU8Span(), preserveUnc, hasMountName); Assert.Equal(expectedResult, result); @@ -39,9 +39,9 @@ namespace LibHac.Tests.Fs [Theory] [MemberData(nameof(SubpathTestItems))] - public static void IsSubpath(string path1, string path2, bool expectedResult) + public static void IsSubPath(string path1, string path2, bool expectedResult) { - bool result = PathTool.IsSubpath(path1.ToU8Span(), path2.ToU8Span()); + bool result = PathUtility.IsSubPath(path1.ToU8Span(), path2.ToU8Span()); Assert.Equal(expectedResult, result); } diff --git a/tests/LibHac.Tests/FsSrv/PathNormalizerTests.cs b/tests/LibHac.Tests/FsSrv/PathNormalizerTests.cs index b3123808..7cfea181 100644 --- a/tests/LibHac.Tests/FsSrv/PathNormalizerTests.cs +++ b/tests/LibHac.Tests/FsSrv/PathNormalizerTests.cs @@ -1,7 +1,7 @@ using LibHac.Common; using LibHac.Fs; -using LibHac.FsSrv; using Xunit; +using PathNormalizer = LibHac.FsSrv.Impl.PathNormalizer; namespace LibHac.Tests.FsSrv { @@ -10,7 +10,7 @@ namespace LibHac.Tests.FsSrv [Fact] public static void Ctor_EmptyPathWithAcceptEmptyOption_ReturnsEmptyPathWithSuccess() { - var normalizer = new PathNormalizer("".ToU8Span(), PathNormalizer.Option.AcceptEmpty); + using var normalizer = new PathNormalizer("".ToU8Span(), PathNormalizer.Option.AcceptEmpty); Assert.Equal(Result.Success, normalizer.Result); Assert.True(normalizer.Path.IsEmpty()); @@ -19,7 +19,7 @@ namespace LibHac.Tests.FsSrv [Fact] public static void Normalize_PreserveTailSeparatorOption_KeepsExistingTailSeparator() { - var normalizer = new PathNormalizer("/a/./b/".ToU8Span(), PathNormalizer.Option.PreserveTailSeparator); + using var normalizer = new PathNormalizer("/a/./b/".ToU8Span(), PathNormalizer.Option.PreserveTrailingSeparator); Assert.Equal(Result.Success, normalizer.Result); Assert.Equal("/a/b/", normalizer.Path.ToString()); @@ -28,7 +28,7 @@ namespace LibHac.Tests.FsSrv [Fact] public static void Normalize_PreserveTailSeparatorOption_IgnoresMissingTailSeparator() { - var normalizer = new PathNormalizer("/a/./b".ToU8Span(), PathNormalizer.Option.PreserveTailSeparator); + using var normalizer = new PathNormalizer("/a/./b".ToU8Span(), PathNormalizer.Option.PreserveTrailingSeparator); Assert.Equal(Result.Success, normalizer.Result); Assert.Equal("/a/b", normalizer.Path.ToString()); @@ -38,7 +38,7 @@ namespace LibHac.Tests.FsSrv public static void Normalize_PathAlreadyNormalized_ReturnsSameBuffer() { var originalPath = "/a/b".ToU8Span(); - var normalizer = new PathNormalizer(originalPath, PathNormalizer.Option.PreserveTailSeparator); + using var normalizer = new PathNormalizer(originalPath, PathNormalizer.Option.PreserveTrailingSeparator); Assert.Equal(Result.Success, normalizer.Result); @@ -49,7 +49,7 @@ namespace LibHac.Tests.FsSrv [Fact] public static void Normalize_PreserveUncOptionOn_PreservesUncPath() { - var normalizer = new PathNormalizer("//aa/bb/..".ToU8Span(), PathNormalizer.Option.PreserveUnc); + using var normalizer = new PathNormalizer("//aa/bb/..".ToU8Span(), PathNormalizer.Option.PreserveUnc); Assert.Equal(Result.Success, normalizer.Result); Assert.Equal(@"\\aa/bb", normalizer.Path.ToString()); @@ -58,7 +58,7 @@ namespace LibHac.Tests.FsSrv [Fact] public static void Normalize_PreserveUncOptionOff_DoesNotPreserveUncPath() { - var normalizer = new PathNormalizer("//aa/bb/..".ToU8Span(), PathNormalizer.Option.None); + using var normalizer = new PathNormalizer("//aa/bb/..".ToU8Span(), PathNormalizer.Option.None); Assert.Equal(Result.Success, normalizer.Result); Assert.Equal(@"/aa", normalizer.Path.ToString()); @@ -67,7 +67,7 @@ namespace LibHac.Tests.FsSrv [Fact] public static void Normalize_MountNameOptionOn_ParsesMountName() { - var normalizer = new PathNormalizer("mount:/a/./b".ToU8Span(), PathNormalizer.Option.HasMountName); + using var normalizer = new PathNormalizer("mount:/a/./b".ToU8Span(), PathNormalizer.Option.HasMountName); Assert.Equal(Result.Success, normalizer.Result); Assert.Equal("mount:/a/b", normalizer.Path.ToString()); @@ -76,7 +76,7 @@ namespace LibHac.Tests.FsSrv [Fact] public static void Normalize_MountNameOptionOff_DoesNotParseMountName() { - var normalizer = new PathNormalizer("mount:/a/./b".ToU8Span(), PathNormalizer.Option.None); + using var normalizer = new PathNormalizer("mount:/a/./b".ToU8Span(), PathNormalizer.Option.None); Assert.Equal(ResultFs.InvalidPathFormat.Value, normalizer.Result); } diff --git a/tests/LibHac.Tests/LibHacTestFramework.cs b/tests/LibHac.Tests/LibHacTestFramework.cs index f9c03b66..d7864490 100644 --- a/tests/LibHac.Tests/LibHacTestFramework.cs +++ b/tests/LibHac.Tests/LibHacTestFramework.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using System.Linq; +using LibHac.Diag; using LibHac.Tests; using Xunit.Abstractions; using Xunit.Sdk; @@ -16,8 +18,22 @@ namespace LibHac.Tests SetResultNames(); } + // Todo: Catch assertions in PathToolTestGenerator.cpp + private static readonly string[] SkipAbortFunctions = { "Normalize" }; + private static void SetDebugHandler() { + AssertionFailureHandler handler = (in AssertionInfo info) => + { + if (SkipAbortFunctions.Contains(info.FunctionName)) + { + return AssertionFailureOperation.Continue; + } + + return AssertionFailureOperation.Abort; + }; + + Assert.SetAssertionFailureHandler(handler); Trace.Listeners.Clear(); Trace.Listeners.Add(new DebugAssertHandler()); }