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());
}