diff --git a/src/LibHac/Fs/PathTools.cs b/src/LibHac/Fs/PathTools.cs index d6b2dba6..a9780b64 100644 --- a/src/LibHac/Fs/PathTools.cs +++ b/src/LibHac/Fs/PathTools.cs @@ -2,85 +2,189 @@ using System.Diagnostics; using System.Runtime.CompilerServices; +using static LibHac.Results; +using static LibHac.Fs.ResultsFs; + namespace LibHac.Fs { public static class PathTools { public static readonly char DirectorySeparator = '/'; + public static readonly char MountSeparator = ':'; + internal const int MountNameLength = 0xF; public static string Normalize(string inPath) { if (IsNormalized(inPath.AsSpan())) return inPath; - return NormalizeInternal(inPath); + + Span initialBuffer = stackalloc char[0x200]; + var sb = new ValueStringBuilder(initialBuffer); + + int rootLen = 0; + int maxMountLen = Math.Min(inPath.Length, MountNameLength); + + for (int i = 0; i < maxMountLen; i++) + { + if (inPath[i] == MountSeparator) + { + rootLen = i + 1; + break; + } + + if (IsDirectorySeparator(inPath[i])) + { + break; + } + } + + bool isNormalized = NormalizeInternal(inPath.AsSpan(), rootLen, ref sb); + + string normalized = isNormalized ? inPath : sb.ToString(); + + sb.Dispose(); + + return normalized; } // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. - public static string NormalizeInternal(string inPath) + internal static bool NormalizeInternal(ReadOnlySpan path, int rootLength, ref ValueStringBuilder sb) { - // Relative paths aren't a thing for IFileSystem, so assume all paths are absolute - // and add a '/' to the beginning of the path if it doesn't already begin with one - if (inPath.Length == 0 || !IsDirectorySeparator(inPath[0])) inPath = DirectorySeparator + inPath; + if (rootLength > 0) + { + sb.Append(path.Slice(0, rootLength)); + } - ReadOnlySpan path = inPath.AsSpan(); + bool isNormalized = true; - if (path.Length == 0) return DirectorySeparator.ToString(); + var state = NormalizeState.Initial; - Span initialBuffer = stackalloc char[0x200]; - var sb = new ValueStringBuilder(initialBuffer); - - for (int i = 0; i < path.Length; i++) + for (int i = rootLength; i < path.Length; i++) { char c = path[i]; - if (IsDirectorySeparator(c) && i + 1 < path.Length) + switch (state) { - // Skip this character if it's a directory separator and if the next character is, too, - // e.g. "parent//child" => "parent/child" - if (IsDirectorySeparator(path[i + 1])) continue; + case NormalizeState.Initial when IsDirectorySeparator(c): + state = NormalizeState.Delimiter; + sb.Append(c); + break; - // Skip this character and the next if it's referring to the current directory, - // e.g. "parent/./child" => "parent/child" - if (IsCurrentDirectory(path, i)) - { - i++; - continue; - } + case NormalizeState.Initial when c == '.': + isNormalized = false; + state = NormalizeState.Dot; - // Skip this character and the next two if it's referring to the parent directory, - // e.g. "parent/child/../grandchild" => "parent/grandchild" - if (IsParentDirectory(path, i)) - { - // Unwind back to the last slash (and if there isn't one, clear out everything). - for (int s = sb.Length - 1; s >= 0; s--) + sb.Append(DirectorySeparator); + sb.Append(c); + break; + + case NormalizeState.Initial: + isNormalized = false; + state = NormalizeState.Delimiter; + + sb.Append(DirectorySeparator); + sb.Append(c); + break; + + case NormalizeState.Normal when IsDirectorySeparator(c): + state = NormalizeState.Delimiter; + sb.Append(c); + break; + + case NormalizeState.Delimiter when IsDirectorySeparator(c): + isNormalized = false; + break; + + case NormalizeState.Delimiter when c == '.': + state = NormalizeState.Dot; + sb.Append(c); + break; + + case NormalizeState.Delimiter: + state = NormalizeState.Normal; + sb.Append(c); + break; + + case NormalizeState.Dot when IsDirectorySeparator(c): + isNormalized = false; + state = NormalizeState.Delimiter; + sb.Length -= 1; + break; + + case NormalizeState.Dot when c == '.': + state = NormalizeState.DoubleDot; + sb.Append(c); + break; + + case NormalizeState.Dot: + state = NormalizeState.Normal; + sb.Append(c); + break; + + case NormalizeState.DoubleDot when IsDirectorySeparator(c): + isNormalized = false; + state = NormalizeState.Delimiter; + + int s = sb.Length - 1; + int separators = 0; + + for (; s > rootLength; s--) { if (IsDirectorySeparator(sb[s])) { - sb.Length = s; - break; + separators++; + + if (separators == 2) break; } } - i += 2; - continue; - } + sb.Length = s + 1; + + break; + + case NormalizeState.DoubleDot: + state = NormalizeState.Normal; + break; } - sb.Append(c); } - // If we haven't changed the source path, return the original - if (sb.Length == inPath.Length) + switch (state) { - return inPath; + case NormalizeState.Dot: + isNormalized = false; + sb.Length -= 2; + break; + + case NormalizeState.DoubleDot: + isNormalized = false; + + int s = sb.Length - 1; + int separators = 0; + + for (; s > rootLength; s--) + { + if (IsDirectorySeparator(sb[s])) + { + separators++; + + if (separators == 2) break; + } + } + + sb.Length = s; + + break; } - if (sb.Length == 0) + if (sb.Length == rootLength) { sb.Append(DirectorySeparator); + + return false; } - return sb.ToString(); + return isNormalized; } public static string GetParentDirectory(string path) @@ -219,7 +323,7 @@ namespace LibHac.Fs public static string Combine(string path1, string path2) { - if(path1 == null || path2 == null) throw new NullReferenceException(); + if (path1 == null || path2 == null) throw new NullReferenceException(); if (string.IsNullOrEmpty(path1)) return path2; if (string.IsNullOrEmpty(path2)) return path1; @@ -240,19 +344,21 @@ namespace LibHac.Fs return c == DirectorySeparator; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsCurrentDirectory(ReadOnlySpan path, int index) + public static Result GetMountName(string path, out string mountName) { - return (index + 2 == path.Length || IsDirectorySeparator(path[index + 2])) && - path[index + 1] == '.'; - } + int maxLen = Math.Min(path.Length, MountNameLength); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsParentDirectory(ReadOnlySpan path, int index) - { - return index + 2 < path.Length && - (index + 3 == path.Length || IsDirectorySeparator(path[index + 3])) && - path[index + 1] == '.' && path[index + 2] == '.'; + for (int i = 0; i < maxLen; i++) + { + if (path[i] == MountSeparator) + { + mountName = path.Substring(0, i); + return ResultSuccess; + } + } + + mountName = default; + return ResultFsInvalidMountName; } private enum NormalizeState diff --git a/src/LibHac/Fs/ResultsFs.cs b/src/LibHac/Fs/ResultsFs.cs index 92a20d0c..511d28af 100644 --- a/src/LibHac/Fs/ResultsFs.cs +++ b/src/LibHac/Fs/ResultsFs.cs @@ -5,6 +5,7 @@ public const int ModuleFs = 2; public static Result ResultFsMountNameAlreadyExists => new Result(ModuleFs, 60); + public static Result ResultFsInvalidMountName => new Result(ModuleFs, 6065); public static Result ResultFsWritableFileOpen => new Result(ModuleFs, 6457); public static Result ResultFsMountNameNotFound => new Result(ModuleFs, 6905); } diff --git a/tests/LibHac.Tests/PathToolsTests.cs b/tests/LibHac.Tests/PathToolsTests.cs index 636ac904..9b4961b4 100644 --- a/tests/LibHac.Tests/PathToolsTests.cs +++ b/tests/LibHac.Tests/PathToolsTests.cs @@ -35,6 +35,17 @@ namespace LibHac.Tests new object[] {"..", "/"}, new object[] {"../a/b/c/.", "/a/b/c"}, new object[] {"./a/b/c/.", "/a/b/c"}, + + new object[] {"a:/a/b/c", "a:/a/b/c"}, + new object[] {"mount:/a/b/../c", "mount:/a/c"}, + new object[] {"mount:", "mount:/"}, + new object[] {"abc:/a/../../../a/b/c", "abc:/a/b/c"}, + new object[] {"abc:/./b/../c/", "abc:/c/"}, + new object[] {"abc:/.", "abc:/"}, + new object[] {"abc:/..", "abc:/"}, + new object[] {"abc:/", "abc:/"}, + new object[] {"abc://a/b//.//c", "abc:/a/b/c"}, + new object[] {"abc:/././/././a/b//.//c", "abc:/a/b/c"}, }; public static object[][] SubPathTestItems = @@ -56,6 +67,63 @@ namespace LibHac.Tests new object[] {"/a/b/c/", "/a/b/cd", false}, }; + public static object[][] IsNormalizedTestItems = + { + new object[] {"", "/"}, + new object[] {"/"}, + new object[] {"/a/b/c"}, + new object[] {"/a/c"}, + new object[] {"/a/b"}, + new object[] {"/a/b/c"}, + new object[] {"/"}, + new object[] {"/a/b/c"}, + + new object[] {"/a/b/c/"}, + new object[] {"/a/c/"}, + new object[] {"/c/"}, + + new object[] {"/a"}, + + new object[] {"a:/a/b/c"}, + new object[] {"mount:/a/c"}, + new object[] {"mount:/"}, + }; + + public static object[][] IsNotNormalizedTestItems = + { + new object[] {""}, + new object[] {"/."}, + new object[] {"/a/b/../c", "/a/c"}, + new object[] {"/a/b/c/..", "/a/b"}, + new object[] {"/a/b/c/.", "/a/b/c"}, + new object[] {"/a/../../..", "/"}, + new object[] {"/a/../../../a/b/c", "/a/b/c"}, + new object[] {"//a/b//.//c", "/a/b/c"}, + new object[] {"/../a/b/c/.", "/a/b/c"}, + new object[] {"/./a/b/c/.", "/a/b/c"}, + + new object[] {"/a/b/c/", "/a/b/c/"}, + new object[] {"/a/./b/../c/", "/a/c/"}, + new object[] {"/./b/../c/", "/c/"}, + new object[] {"/a/../../../", "/"}, + new object[] {"//a/b//.//c/", "/a/b/c/"}, + new object[] {"/tmp/../", "/"}, + + new object[] {"a", "/a"}, + new object[] {"a/../../../a/b/c", "/a/b/c"}, + new object[] {"./b/../c/", "/c/"}, + new object[] {".", "/"}, + new object[] {"..", "/"}, + new object[] {"../a/b/c/.", "/a/b/c"}, + new object[] {"./a/b/c/.", "/a/b/c"}, + + new object[] {"a:/a/b/c", "a:/a/b/c"}, + new object[] {"mount:/a/b/../c", "mount:/a/c"}, + new object[] {"mount:/a/b/../c", "mount:/a/c"}, + new object[] {"mount:", "mount:/"}, + new object[] {"abc:/a/../../../a/b/c", "abc:/a/b/c"}, + }; + [Theory] [MemberData(nameof(NormalizedPathTestItems))] public static void NormalizePath(string path, string expected) @@ -65,6 +133,15 @@ namespace LibHac.Tests Assert.Equal(expected, actual); } + [Theory] + [MemberData(nameof(NormalizedPathTestItems))] + public static void IsNormalized(string path, string expected) + { + string actual = PathTools.Normalize(path); + + Assert.Equal(expected, actual); + } + [Theory] [MemberData(nameof(SubPathTestItems))] public static void TestSubPath(string rootPath, string path, bool expected)