From 49ec3d6427467975f0af0d9d6efe76e0a3164718 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 14 Oct 2019 11:58:43 -0500 Subject: [PATCH] Add a path normalizer that behaves like the one in FS --- src/LibHac/Common/PathBuilder.cs | 123 ++++++++++++++++ src/LibHac/Fs/FileBase.cs | 4 +- src/LibHac/FsSystem/PathTools.cs | 206 ++++++++++++++++++++++++++- tests/LibHac.Tests/PathToolsTests.cs | 114 ++++++++++++++- 4 files changed, 438 insertions(+), 9 deletions(-) create mode 100644 src/LibHac/Common/PathBuilder.cs diff --git a/src/LibHac/Common/PathBuilder.cs b/src/LibHac/Common/PathBuilder.cs new file mode 100644 index 00000000..39041711 --- /dev/null +++ b/src/LibHac/Common/PathBuilder.cs @@ -0,0 +1,123 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using LibHac.Fs; +using LibHac.FsSystem; + +namespace LibHac.Common +{ + [DebuggerDisplay("{ToString()}")] + internal ref struct PathBuilder + { + private Span _buffer; + private int _pos; + + public int Length + { + get => _pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= Capacity); + _pos = value; + } + } + + public int Capacity => _buffer.Length - 1; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public PathBuilder(Span buffer) + { + _buffer = buffer; + _pos = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Result Append(byte value) + { + int pos = _pos; + if (pos >= Capacity) + { + return ResultFs.TooLongPath.Log(); + } + + _buffer[pos] = value; + _pos = pos + 1; + return Result.Success; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Result Append(ReadOnlySpan value) + { + int pos = _pos; + if (pos + value.Length >= Capacity) + { + return ResultFs.TooLongPath.Log(); + } + + value.CopyTo(_buffer.Slice(pos)); + _pos = pos + value.Length; + return Result.Success; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Result AppendWithPrecedingSeparator(byte value) + { + int pos = _pos; + if (pos + 1 >= Capacity) + { + // Append the separator if there's enough space + if (pos < Capacity) + { + _buffer[pos] = (byte)'/'; + _pos = pos + 1; + } + + return ResultFs.TooLongPath.Log(); + } + + _buffer[pos] = (byte)'/'; + _buffer[pos + 1] = value; + _pos = pos + 2; + return Result.Success; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Result RewindLevels(int count) + { + Debug.Assert(count > 0); + + int separators = 0; + int pos = _pos - 1; + + for (; pos >= 0; pos--) + { + if (PathTools.IsDirectorySeparator(_buffer[pos])) + { + separators++; + + if (separators == count) break; + } + } + + if (separators != count) return ResultFs.DirectoryUnobtainable.Log(); + + _pos = pos; + return Result.Success; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Terminate() + { + if (_buffer.Length > _pos) + { + _buffer[_pos] = 0; + } + } + + public override string ToString() + { + return StringUtils.Utf8ZToString(_buffer.Slice(0, Length)); + } + } +} diff --git a/src/LibHac/Fs/FileBase.cs b/src/LibHac/Fs/FileBase.cs index c8126bd4..57ad398f 100644 --- a/src/LibHac/Fs/FileBase.cs +++ b/src/LibHac/Fs/FileBase.cs @@ -41,7 +41,7 @@ namespace LibHac.Fs { if (options.HasFlag(WriteOption.Flush)) { - return Flush(); + return FlushImpl(); } return Result.Success; @@ -83,7 +83,7 @@ namespace LibHac.Fs { if (IsDisposed) return ResultFs.PreconditionViolation.Log(); - return OperateRange(outBuffer, operationId, offset, size, inBuffer); + return OperateRangeImpl(outBuffer, operationId, offset, size, inBuffer); } public void Dispose() diff --git a/src/LibHac/FsSystem/PathTools.cs b/src/LibHac/FsSystem/PathTools.cs index 742d0b51..f7e641a9 100644 --- a/src/LibHac/FsSystem/PathTools.cs +++ b/src/LibHac/FsSystem/PathTools.cs @@ -12,8 +12,8 @@ namespace LibHac.FsSystem { public static class PathTools { - public static readonly char DirectorySeparator = '/'; - public static readonly char MountSeparator = ':'; + internal const char DirectorySeparator = '/'; + internal const char MountSeparator = ':'; internal const int MountNameLength = 0xF; // Todo: Remove @@ -416,6 +416,12 @@ namespace LibHac.FsSystem return c == DirectorySeparator; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirectorySeparator(byte c) + { + return c == DirectorySeparator; + } + public static Result GetMountName(string path, out string mountName) { Result rc = GetMountNameLength(path, out int length); @@ -444,7 +450,7 @@ namespace LibHac.FsSystem } length = default; - return ResultFs.InvalidMountName; + return ResultFs.InvalidMountName.Log(); } public static bool MatchesPattern(string searchPattern, string name, bool ignoreCase) @@ -466,6 +472,200 @@ namespace LibHac.FsSystem private static bool IsValidMountNameChar(byte c) => IsValidMountNameChar((char)c); + private static Result GetPathRoot(out ReadOnlySpan afterRootPath, Span pathRootBuffer, out int outRootLength, ReadOnlySpan path) + { + outRootLength = 0; + afterRootPath = path; + + if (path.Length == 0) return Result.Success; + + int mountNameStart; + if (IsDirectorySeparator(path[0])) + { + mountNameStart = 1; + } + else + { + mountNameStart = 0; + } + + int rootLength = 0; + + for (int i = mountNameStart; i < mountNameStart + MountNameLength; i++) + { + if (i >= path.Length || path[i] == 0) break; + + // Set the length to 0 if there's no mount name + if (IsDirectorySeparator(path[i])) + { + outRootLength = 0; + return Result.Success; + } + + if (path[i] == MountSeparator) + { + rootLength = i + 1; + break; + } + } + + if (mountNameStart >= rootLength - 1 || path[rootLength - 1] != MountSeparator) + { + return ResultFs.InvalidPathFormat.Log(); + } + + if (mountNameStart < rootLength) + { + for (int i = mountNameStart; i < rootLength; i++) + { + if (path[i] == '.') + { + return ResultFs.InvalidCharacter.Log(); + } + } + } + + if (!pathRootBuffer.IsEmpty) + { + if (rootLength > pathRootBuffer.Length) + { + return ResultFs.TooLongPath.Log(); + } + + path.Slice(0, rootLength).CopyTo(pathRootBuffer); + } + + afterRootPath = path.Slice(rootLength); + outRootLength = rootLength; + return Result.Success; + } + + public static Result Normalize(Span outValue, out int outLength, ReadOnlySpan path, bool hasMountName) + { + outLength = 0; + + int rootLength = 0; + ReadOnlySpan mainPath = path; + + if (hasMountName) + { + Result pathRootRc = GetPathRoot(out mainPath, outValue, out rootLength, path); + if (pathRootRc.IsFailure()) return pathRootRc; + } + + var sb = new PathBuilder(outValue.Slice(rootLength)); + + var state = NormalizeState.Initial; + + for (int i = 0; i < mainPath.Length; i++) + { + Result rc = Result.Success; + byte c = mainPath[i]; + + // Read input strings as null-terminated + if (c == 0) break; + + switch (state) + { + case NormalizeState.Initial when IsDirectorySeparator(c): + state = NormalizeState.Delimiter; + break; + + case NormalizeState.Initial: + return ResultFs.InvalidPathFormat.Log(); + + case NormalizeState.Normal when IsDirectorySeparator(c): + state = NormalizeState.Delimiter; + break; + + case NormalizeState.Normal: + rc = sb.Append(c); + break; + + case NormalizeState.Delimiter when IsDirectorySeparator(c): + break; + + case NormalizeState.Delimiter when c == '.': + state = NormalizeState.Dot; + rc = sb.AppendWithPrecedingSeparator(c); + break; + + case NormalizeState.Delimiter: + state = NormalizeState.Normal; + rc = sb.AppendWithPrecedingSeparator(c); + break; + + case NormalizeState.Dot when IsDirectorySeparator(c): + state = NormalizeState.Delimiter; + rc = sb.RewindLevels(1); + break; + + case NormalizeState.Dot when c == '.': + state = NormalizeState.DoubleDot; + rc = sb.Append(c); + break; + + case NormalizeState.Dot: + state = NormalizeState.Normal; + rc = sb.Append(c); + break; + + case NormalizeState.DoubleDot when IsDirectorySeparator(c): + state = NormalizeState.Delimiter; + rc = sb.RewindLevels(2); + break; + + case NormalizeState.DoubleDot: + state = NormalizeState.Normal; + break; + } + + if (rc.IsFailure()) + { + if (rc == ResultFs.TooLongPath) + { + // Make sure pending delimiters are added to the string if possible + if (state == NormalizeState.Delimiter) + { + sb.Append((byte)DirectorySeparator); + } + } + + outLength = sb.Length; + sb.Terminate(); + return rc; + } + } + + Result finalRc = Result.Success; + + switch (state) + { + case NormalizeState.Dot: + state = NormalizeState.Delimiter; + finalRc = sb.RewindLevels(1); + break; + + case NormalizeState.DoubleDot: + state = NormalizeState.Delimiter; + finalRc = sb.RewindLevels(2); + break; + } + + // Add the pending delimiter if the path is empty + // or if the path has only a mount name with no trailing delimiter + if (state == NormalizeState.Delimiter && sb.Length == 0 || + rootLength > 0 && sb.Length == 0) + { + finalRc = sb.Append((byte)'/'); + } + + outLength = sb.Length; + sb.Terminate(); + + return finalRc; + } + private enum NormalizeState { Initial, diff --git a/tests/LibHac.Tests/PathToolsTests.cs b/tests/LibHac.Tests/PathToolsTests.cs index ff4330fe..b72de33c 100644 --- a/tests/LibHac.Tests/PathToolsTests.cs +++ b/tests/LibHac.Tests/PathToolsTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using LibHac.Common; +using LibHac.Fs; using LibHac.FsSystem; using Xunit; @@ -73,18 +75,18 @@ namespace LibHac.Tests new object[] {"mount:/", "mount:/", false}, new object[] {"mount:/", "mount:/a", true}, new object[] {"mount:/", "mount:/a/", true}, - + new object[] {"mount:/a/b/c", "mount:/a/b/c/d", true}, new object[] {"mount:/a/b/c/", "mount:/a/b/c/d", true}, - + new object[] {"mount:/a/b/c", "mount:/a/b/c", false}, new object[] {"mount:/a/b/c/", "mount:/a/b/c/", false}, new object[] {"mount:/a/b/c/", "mount:/a/b/c", false}, new object[] {"mount:/a/b/c", "mount:/a/b/c/", false}, - + new object[] {"mount:/a/b/c/", "mount:/a/b/cdef", false}, new object[] {"mount:/a/b/c", "mount:/a/b/cdef", false}, - new object[] { "mount:/a/b/c/", "mount:/a/b/cd", false}, + new object[] {"mount:/a/b/c/", "mount:/a/b/cd", false}, }; public static object[][] ParentDirectoryTestItems = @@ -169,5 +171,109 @@ namespace LibHac.Tests return paths.Select(x => new object[] { x }).ToArray(); } + + public static object[][] NormalizedPathTestItemsU8NoMountName = + { + new object[] {"/", "/", Result.Success}, + new object[] {"/.", "/", Result.Success}, + new object[] {"/..", "", ResultFs.DirectoryUnobtainable}, + new object[] {"/abc", "/abc", Result.Success}, + new object[] {"/a/..", "/", Result.Success}, + new object[] {"/a/b/c", "/a/b/c", Result.Success}, + new object[] {"/a/b/../c", "/a/c", Result.Success}, + new object[] {"/a/b/c/..", "/a/b", Result.Success}, + new object[] {"/a/b/c/.", "/a/b/c", Result.Success}, + new object[] {"/a/../../..", "", ResultFs.DirectoryUnobtainable}, + new object[] {"/a/../../../a/b/c", "", ResultFs.DirectoryUnobtainable}, + new object[] {"//a/b//.//c", "/a/b/c", Result.Success}, + new object[] {"/../a/b/c/.", "", ResultFs.DirectoryUnobtainable}, + new object[] {"/./aaa/bbb/ccc/.", "/aaa/bbb/ccc", Result.Success}, + + new object[] {"/a/b/c/", "/a/b/c", Result.Success}, + new object[] {"/aa/./bb/../cc/", "/aa/cc", Result.Success}, + new object[] {"/./b/../c/", "/c", Result.Success}, + new object[] {"/a/../../../", "", ResultFs.DirectoryUnobtainable}, + new object[] {"//a/b//.//c/", "/a/b/c", Result.Success}, + new object[] {"/tmp/../", "/", Result.Success}, + new object[] {"abc", "", ResultFs.InvalidPathFormat} + }; + + public static object[][] NormalizedPathTestItemsU8MountName = + { + new object[] {"mount:/a/b/../c", "mount:/a/c", Result.Success}, + new object[] {"a:/a/b/c", "a:/a/b/c", Result.Success}, + new object[] {"mount:/a/b/../c", "mount:/a/c", Result.Success}, + new object[] {"mount:", "mount:/", Result.Success}, + new object[] {"abc:/a/../../../a/b/c", "", ResultFs.DirectoryUnobtainable}, + new object[] {"abc:/./b/../c/", "abc:/c", Result.Success}, + new object[] {"abc:/.", "abc:/", Result.Success}, + new object[] {"abc:/..", "", ResultFs.DirectoryUnobtainable}, + new object[] {"abc:/", "abc:/", Result.Success}, + new object[] {"abc://a/b//.//c", "abc:/a/b/c", Result.Success}, + new object[] {"abc:/././/././a/b//.//c", "abc:/a/b/c", Result.Success}, + new object[] {"mount:/d./aa", "mount:/d./aa", Result.Success}, + new object[] {"mount:/d/..", "mount:/", Result.Success} + }; + + [Theory] + [MemberData(nameof(NormalizedPathTestItemsU8NoMountName))] + public static void NormalizePathU8NoMountName(string path, string expected, Result expectedResult) + { + U8String u8Path = path.ToU8String(); + Span buffer = stackalloc byte[0x301]; + + Result rc = PathTools.Normalize(buffer, out _, u8Path, false); + + string actual = StringUtils.Utf8ZToString(buffer); + + Assert.Equal(expectedResult, rc); + if (expectedResult == Result.Success) + { + Assert.Equal(expected, actual); + } + } + + [Theory] + [MemberData(nameof(NormalizedPathTestItemsU8MountName))] + public static void NormalizePathU8MountName(string path, string expected, Result expectedResult) + { + U8String u8Path = path.ToU8String(); + Span buffer = stackalloc byte[0x301]; + + Result rc = PathTools.Normalize(buffer, out _, u8Path, true); + + string actual = StringUtils.Utf8ZToString(buffer); + + Assert.Equal(expectedResult, rc); + if (expectedResult == Result.Success) + { + Assert.Equal(expected, actual); + } + } + + public static object[][] NormalizedPathTestItemsU8TooShort = + { + new object[] {"/a/b/c", "", 0}, + new object[] {"/a/b/c", "/a/", 4}, + new object[] {"/a/b/c", "/a/b", 5}, + new object[] {"/a/b/c", "/a/b/", 6} + }; + + [Theory] + [MemberData(nameof(NormalizedPathTestItemsU8TooShort))] + public static void NormalizePathU8TooShortDest(string path, string expected, int destSize) + { + U8String u8Path = path.ToU8String(); + + Span buffer = stackalloc byte[destSize]; + + Result rc = PathTools.Normalize(buffer, out int normalizedLength, u8Path, false); + + string actual = StringUtils.Utf8ZToString(buffer); + + Assert.Equal(ResultFs.TooLongPath, rc); + Assert.Equal(Math.Max(0, destSize - 1), normalizedLength); + Assert.Equal(expected, actual); + } } }