Make path normalizer work with mount names

This commit is contained in:
Alex Barney 2019-06-11 16:41:54 -05:00
parent 5a8744c6b5
commit a44bdf780e
3 changed files with 235 additions and 51 deletions

View file

@ -2,85 +2,189 @@
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using static LibHac.Results;
using static LibHac.Fs.ResultsFs;
namespace LibHac.Fs namespace LibHac.Fs
{ {
public static class PathTools public static class PathTools
{ {
public static readonly char DirectorySeparator = '/'; public static readonly char DirectorySeparator = '/';
public static readonly char MountSeparator = ':';
internal const int MountNameLength = 0xF;
public static string Normalize(string inPath) public static string Normalize(string inPath)
{ {
if (IsNormalized(inPath.AsSpan())) return inPath; if (IsNormalized(inPath.AsSpan())) return inPath;
return NormalizeInternal(inPath);
Span<char> 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. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
public static string NormalizeInternal(string inPath) internal static bool NormalizeInternal(ReadOnlySpan<char> path, int rootLength, ref ValueStringBuilder sb)
{ {
// Relative paths aren't a thing for IFileSystem, so assume all paths are absolute if (rootLength > 0)
// 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; sb.Append(path.Slice(0, rootLength));
}
ReadOnlySpan<char> path = inPath.AsSpan(); bool isNormalized = true;
if (path.Length == 0) return DirectorySeparator.ToString(); var state = NormalizeState.Initial;
Span<char> initialBuffer = stackalloc char[0x200]; for (int i = rootLength; i < path.Length; i++)
var sb = new ValueStringBuilder(initialBuffer);
for (int i = 0; i < path.Length; i++)
{ {
char c = path[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, case NormalizeState.Initial when IsDirectorySeparator(c):
// e.g. "parent//child" => "parent/child" state = NormalizeState.Delimiter;
if (IsDirectorySeparator(path[i + 1])) continue; sb.Append(c);
break;
// Skip this character and the next if it's referring to the current directory, case NormalizeState.Initial when c == '.':
// e.g. "parent/./child" => "parent/child" isNormalized = false;
if (IsCurrentDirectory(path, i)) state = NormalizeState.Dot;
{
i++;
continue;
}
// Skip this character and the next two if it's referring to the parent directory, sb.Append(DirectorySeparator);
// e.g. "parent/child/../grandchild" => "parent/grandchild" sb.Append(c);
if (IsParentDirectory(path, i)) break;
{
// Unwind back to the last slash (and if there isn't one, clear out everything). case NormalizeState.Initial:
for (int s = sb.Length - 1; s >= 0; s--) 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])) if (IsDirectorySeparator(sb[s]))
{ {
sb.Length = s; separators++;
break;
if (separators == 2) break;
} }
} }
i += 2; sb.Length = s + 1;
continue;
} break;
case NormalizeState.DoubleDot:
state = NormalizeState.Normal;
break;
} }
sb.Append(c);
} }
// If we haven't changed the source path, return the original switch (state)
if (sb.Length == inPath.Length)
{ {
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); sb.Append(DirectorySeparator);
return false;
} }
return sb.ToString(); return isNormalized;
} }
public static string GetParentDirectory(string path) public static string GetParentDirectory(string path)
@ -219,7 +323,7 @@ namespace LibHac.Fs
public static string Combine(string path1, string path2) 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(path1)) return path2;
if (string.IsNullOrEmpty(path2)) return path1; if (string.IsNullOrEmpty(path2)) return path1;
@ -240,19 +344,21 @@ namespace LibHac.Fs
return c == DirectorySeparator; return c == DirectorySeparator;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] public static Result GetMountName(string path, out string mountName)
internal static bool IsCurrentDirectory(ReadOnlySpan<char> path, int index)
{ {
return (index + 2 == path.Length || IsDirectorySeparator(path[index + 2])) && int maxLen = Math.Min(path.Length, MountNameLength);
path[index + 1] == '.';
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] for (int i = 0; i < maxLen; i++)
internal static bool IsParentDirectory(ReadOnlySpan<char> path, int index) {
{ if (path[i] == MountSeparator)
return index + 2 < path.Length && {
(index + 3 == path.Length || IsDirectorySeparator(path[index + 3])) && mountName = path.Substring(0, i);
path[index + 1] == '.' && path[index + 2] == '.'; return ResultSuccess;
}
}
mountName = default;
return ResultFsInvalidMountName;
} }
private enum NormalizeState private enum NormalizeState

View file

@ -5,6 +5,7 @@
public const int ModuleFs = 2; public const int ModuleFs = 2;
public static Result ResultFsMountNameAlreadyExists => new Result(ModuleFs, 60); 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 ResultFsWritableFileOpen => new Result(ModuleFs, 6457);
public static Result ResultFsMountNameNotFound => new Result(ModuleFs, 6905); public static Result ResultFsMountNameNotFound => new Result(ModuleFs, 6905);
} }

View file

@ -35,6 +35,17 @@ namespace LibHac.Tests
new object[] {"..", "/"}, new object[] {"..", "/"},
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:/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 = public static object[][] SubPathTestItems =
@ -56,6 +67,63 @@ namespace LibHac.Tests
new object[] {"/a/b/c/", "/a/b/cd", false}, 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] [Theory]
[MemberData(nameof(NormalizedPathTestItems))] [MemberData(nameof(NormalizedPathTestItems))]
public static void NormalizePath(string path, string expected) public static void NormalizePath(string path, string expected)
@ -65,6 +133,15 @@ namespace LibHac.Tests
Assert.Equal(expected, actual); 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] [Theory]
[MemberData(nameof(SubPathTestItems))] [MemberData(nameof(SubPathTestItems))]
public static void TestSubPath(string rootPath, string path, bool expected) public static void TestSubPath(string rootPath, string path, bool expected)