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))
|
if (options.HasFlag(WriteOption.Flush))
|
||||||
{
|
{
|
||||||
return Flush();
|
return FlushImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
|
@ -83,7 +83,7 @@ namespace LibHac.Fs
|
||||||
{
|
{
|
||||||
if (IsDisposed) return ResultFs.PreconditionViolation.Log();
|
if (IsDisposed) return ResultFs.PreconditionViolation.Log();
|
||||||
|
|
||||||
return OperateRange(outBuffer, operationId, offset, size, inBuffer);
|
return OperateRangeImpl(outBuffer, operationId, offset, size, inBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
@ -12,8 +12,8 @@ namespace LibHac.FsSystem
|
||||||
{
|
{
|
||||||
public static class PathTools
|
public static class PathTools
|
||||||
{
|
{
|
||||||
public static readonly char DirectorySeparator = '/';
|
internal const char DirectorySeparator = '/';
|
||||||
public static readonly char MountSeparator = ':';
|
internal const char MountSeparator = ':';
|
||||||
internal const int MountNameLength = 0xF;
|
internal const int MountNameLength = 0xF;
|
||||||
|
|
||||||
// Todo: Remove
|
// Todo: Remove
|
||||||
|
@ -416,6 +416,12 @@ namespace LibHac.FsSystem
|
||||||
return c == DirectorySeparator;
|
return c == DirectorySeparator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
internal static bool IsDirectorySeparator(byte c)
|
||||||
|
{
|
||||||
|
return c == DirectorySeparator;
|
||||||
|
}
|
||||||
|
|
||||||
public static Result GetMountName(string path, out string mountName)
|
public static Result GetMountName(string path, out string mountName)
|
||||||
{
|
{
|
||||||
Result rc = GetMountNameLength(path, out int length);
|
Result rc = GetMountNameLength(path, out int length);
|
||||||
|
@ -444,7 +450,7 @@ namespace LibHac.FsSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
length = default;
|
length = default;
|
||||||
return ResultFs.InvalidMountName;
|
return ResultFs.InvalidMountName.Log();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool MatchesPattern(string searchPattern, string name, bool ignoreCase)
|
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 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
|
private enum NormalizeState
|
||||||
{
|
{
|
||||||
Initial,
|
Initial,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using LibHac.Common;
|
||||||
|
using LibHac.Fs;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
@ -169,5 +171,109 @@ namespace LibHac.Tests
|
||||||
|
|
||||||
return paths.Select(x => new object[] { x }).ToArray();
|
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