Add a path normalizer that behaves like the one in FS

This commit is contained in:
Alex Barney 2019-10-14 11:58:43 -05:00
parent e05a37bb38
commit 49ec3d6427
4 changed files with 438 additions and 9 deletions

View file

@ -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<byte> _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<byte> 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<byte> 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));
}
}
}

View file

@ -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()

View file

@ -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<byte> afterRootPath, Span<byte> pathRootBuffer, out int outRootLength, ReadOnlySpan<byte> 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<byte> outValue, out int outLength, ReadOnlySpan<byte> path, bool hasMountName)
{
outLength = 0;
int rootLength = 0;
ReadOnlySpan<byte> 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,

View file

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LibHac.Common;
using LibHac.Fs;
using LibHac.FsSystem;
using Xunit;
@ -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<byte> 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<byte> 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<byte> 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);
}
}
}