mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Add a path normalizer that behaves like the one in FS
This commit is contained in:
parent
e05a37bb38
commit
49ec3d6427
4 changed files with 438 additions and 9 deletions
123
src/LibHac/Common/PathBuilder.cs
Normal file
123
src/LibHac/Common/PathBuilder.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.FsSystem;
|
||||
using Xunit;
|
||||
|
||||
|
@ -84,7 +86,7 @@ namespace LibHac.Tests
|
|||
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue