mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Add version 12 implementations of path utility classes
This commit is contained in:
parent
f444a999ba
commit
79a4c62b2e
8 changed files with 2479 additions and 0 deletions
630
src/LibHac/Fs/Common/PathFormatter.cs
Normal file
630
src/LibHac/Fs/Common/PathFormatter.cs
Normal file
|
@ -0,0 +1,630 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Util;
|
||||
using static LibHac.Fs.StringTraits;
|
||||
|
||||
namespace LibHac.Fs.Common
|
||||
{
|
||||
public static class PathFormatter
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static Result CheckHostName(ReadOnlySpan<byte> name)
|
||||
{
|
||||
if (name.Length == 2 && name[0] == Dot && name[1] == Dot)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
for (int i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (name[i] == ':' || name[i] == '$')
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static Result CheckSharedName(ReadOnlySpan<byte> name)
|
||||
{
|
||||
if (name.Length == 1 && name[0] == Dot)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
if (name.Length == 2 && name[0] == Dot && name[1] == Dot)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
for (int i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (name[i] == ':')
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static Result ParseMountName(out ReadOnlySpan<byte> newPath, out int mountNameLength,
|
||||
Span<byte> outMountNameBuffer, ReadOnlySpan<byte> path)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
UnsafeHelpers.SkipParamInit(out mountNameLength);
|
||||
newPath = default;
|
||||
|
||||
int maxMountLength = outMountNameBuffer.Length == 0
|
||||
? PathTools.MountNameLengthMax + 1
|
||||
: Math.Min(outMountNameBuffer.Length, PathTools.MountNameLengthMax + 1);
|
||||
|
||||
int mountLength;
|
||||
for (mountLength = 0; mountLength < maxMountLength && path.At(mountLength) != 0; mountLength++)
|
||||
{
|
||||
byte c = path[mountLength];
|
||||
|
||||
if (c == DriveSeparator)
|
||||
{
|
||||
mountLength++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (c == DirectorySeparator || c == AltDirectorySeparator)
|
||||
{
|
||||
newPath = path;
|
||||
mountNameLength = 0;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
if (mountLength <= 2 || path[mountLength - 1] != DriveSeparator)
|
||||
{
|
||||
newPath = path;
|
||||
mountNameLength = 0;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
for (int i = 0; i < mountLength; i++)
|
||||
{
|
||||
if (path.At(i) is (byte)'*' or (byte)'?' or (byte)'<' or (byte)'>' or (byte)'|')
|
||||
return ResultFs.InvalidCharacter.Log();
|
||||
}
|
||||
|
||||
if (!outMountNameBuffer.IsEmpty)
|
||||
{
|
||||
if (mountLength >= outMountNameBuffer.Length)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
path.Slice(0, mountLength).CopyTo(outMountNameBuffer);
|
||||
outMountNameBuffer[mountLength] = NullTerminator;
|
||||
}
|
||||
|
||||
newPath = path.Slice(mountLength);
|
||||
mountNameLength = mountLength;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static Result SkipMountName(out ReadOnlySpan<byte> newPath, out int mountNameLength,
|
||||
ReadOnlySpan<byte> path)
|
||||
{
|
||||
return ParseMountName(out newPath, out mountNameLength, Span<byte>.Empty, path);
|
||||
}
|
||||
|
||||
private static Result ParseWindowsPathImpl(out ReadOnlySpan<byte> newPath, out int windowsPathLength,
|
||||
Span<byte> normalizeBuffer, ReadOnlySpan<byte> path, bool hasMountName)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
UnsafeHelpers.SkipParamInit(out windowsPathLength);
|
||||
newPath = default;
|
||||
|
||||
if (normalizeBuffer.Length != 0)
|
||||
normalizeBuffer[0] = NullTerminator;
|
||||
|
||||
ReadOnlySpan<byte> currentPath = path;
|
||||
|
||||
if (hasMountName && path.At(0) == DirectorySeparator)
|
||||
{
|
||||
if (path.At(1) == AltDirectorySeparator && path.At(2) == AltDirectorySeparator)
|
||||
{
|
||||
if (normalizeBuffer.Length == 0)
|
||||
return ResultFs.NotNormalized.Log();
|
||||
|
||||
currentPath = path.Slice(1);
|
||||
}
|
||||
else if (path.Length != 0 && WindowsPath12.IsWindowsDrive(path.Slice(1)))
|
||||
{
|
||||
if (normalizeBuffer.Length == 0)
|
||||
return ResultFs.NotNormalized.Log();
|
||||
|
||||
currentPath = path.Slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (WindowsPath12.IsWindowsDrive(currentPath))
|
||||
{
|
||||
int winPathLength;
|
||||
for (winPathLength = 2; currentPath.At(winPathLength) != NullTerminator; winPathLength++)
|
||||
{
|
||||
if (currentPath[winPathLength] == DirectorySeparator ||
|
||||
currentPath[winPathLength] == AltDirectorySeparator)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizeBuffer.IsEmpty)
|
||||
{
|
||||
for (int i = 0; i < winPathLength; i++)
|
||||
{
|
||||
if (currentPath[i] == '\\')
|
||||
return ResultFs.NotNormalized.Log();
|
||||
}
|
||||
}
|
||||
|
||||
if (!normalizeBuffer.IsEmpty)
|
||||
{
|
||||
if (winPathLength >= normalizeBuffer.Length)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
currentPath.Slice(0, winPathLength).CopyTo(normalizeBuffer);
|
||||
normalizeBuffer[winPathLength] = NullTerminator;
|
||||
PathUtility12.Replace(normalizeBuffer.Slice(0, winPathLength), AltDirectorySeparator,
|
||||
DirectorySeparator);
|
||||
}
|
||||
|
||||
newPath = currentPath.Slice(winPathLength);
|
||||
windowsPathLength = winPathLength;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
if (WindowsPath12.IsDosDevicePath(currentPath))
|
||||
{
|
||||
int dosPathLength = WindowsPath12.GetDosDevicePathPrefixLength();
|
||||
|
||||
if (WindowsPath12.IsWindowsDrive(currentPath.Slice(dosPathLength)))
|
||||
{
|
||||
dosPathLength += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
dosPathLength--;
|
||||
}
|
||||
|
||||
if (!normalizeBuffer.IsEmpty)
|
||||
{
|
||||
if (dosPathLength >= normalizeBuffer.Length)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
currentPath.Slice(0, dosPathLength).CopyTo(normalizeBuffer);
|
||||
normalizeBuffer[dosPathLength] = NullTerminator;
|
||||
PathUtility12.Replace(normalizeBuffer.Slice(0, dosPathLength), DirectorySeparator,
|
||||
AltDirectorySeparator);
|
||||
}
|
||||
|
||||
newPath = currentPath.Slice(dosPathLength);
|
||||
windowsPathLength = dosPathLength;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
if (WindowsPath12.IsUncPath(currentPath, false, true))
|
||||
{
|
||||
Result rc;
|
||||
|
||||
ReadOnlySpan<byte> finalPath = currentPath;
|
||||
|
||||
if (currentPath.At(2) == DirectorySeparator || currentPath.At(2) == AltDirectorySeparator)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
int currentComponentOffset = 0;
|
||||
int pos;
|
||||
for (pos = 2; currentPath.At(pos) != NullTerminator; pos++)
|
||||
{
|
||||
if (currentPath.At(pos) == DirectorySeparator || currentPath.At(pos) == AltDirectorySeparator)
|
||||
{
|
||||
if (currentComponentOffset != 0)
|
||||
{
|
||||
rc = CheckSharedName(
|
||||
currentPath.Slice(currentComponentOffset, pos - currentComponentOffset));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
finalPath = currentPath.Slice(pos);
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentPath.At(pos + 1) == DirectorySeparator || currentPath.At(pos + 1) == AltDirectorySeparator)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
rc = CheckHostName(currentPath.Slice(2, pos - 2));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
currentComponentOffset = pos + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentComponentOffset == pos)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
if (currentComponentOffset != 0 && finalPath == currentPath)
|
||||
{
|
||||
rc = CheckSharedName(currentPath.Slice(currentComponentOffset, pos - currentComponentOffset));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
finalPath = currentPath.Slice(pos);
|
||||
}
|
||||
|
||||
ref byte currentPathStart = ref MemoryMarshal.GetReference(currentPath);
|
||||
ref byte finalPathStart = ref MemoryMarshal.GetReference(finalPath);
|
||||
int uncPrefixLength = (int)Unsafe.ByteOffset(ref currentPathStart, ref finalPathStart);
|
||||
|
||||
if (normalizeBuffer.IsEmpty)
|
||||
{
|
||||
for (int i = 0; i < uncPrefixLength; i++)
|
||||
{
|
||||
if (currentPath[i] == DirectorySeparator)
|
||||
return ResultFs.NotNormalized.Log();
|
||||
}
|
||||
}
|
||||
|
||||
if (!normalizeBuffer.IsEmpty)
|
||||
{
|
||||
if (uncPrefixLength >= normalizeBuffer.Length)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
currentPath.Slice(0, uncPrefixLength).CopyTo(normalizeBuffer);
|
||||
normalizeBuffer[uncPrefixLength] = NullTerminator;
|
||||
PathUtility12.Replace(normalizeBuffer.Slice(0, uncPrefixLength), DirectorySeparator, AltDirectorySeparator);
|
||||
}
|
||||
|
||||
newPath = finalPath;
|
||||
windowsPathLength = uncPrefixLength;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
newPath = path;
|
||||
windowsPathLength = 0;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static Result ParseWindowsPath(out ReadOnlySpan<byte> newPath, out int windowsPathLength,
|
||||
Span<byte> normalizeBuffer, ReadOnlySpan<byte> path, bool hasMountName)
|
||||
{
|
||||
return ParseWindowsPathImpl(out newPath, out windowsPathLength, normalizeBuffer, path, hasMountName);
|
||||
}
|
||||
|
||||
public static Result SkipWindowsPath(out ReadOnlySpan<byte> newPath, out int windowsPathLength,
|
||||
out bool isNormalized, ReadOnlySpan<byte> path, bool hasMountName)
|
||||
{
|
||||
isNormalized = true;
|
||||
|
||||
Result rc = ParseWindowsPathImpl(out newPath, out windowsPathLength, Span<byte>.Empty, path, hasMountName);
|
||||
if (!rc.IsSuccess())
|
||||
{
|
||||
if (ResultFs.NotNormalized.Includes(rc))
|
||||
{
|
||||
isNormalized = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static Result ParseRelativeDotPathImpl(out ReadOnlySpan<byte> newPath, out int length,
|
||||
Span<byte> relativePathBuffer, ReadOnlySpan<byte> path)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
UnsafeHelpers.SkipParamInit(out length);
|
||||
newPath = default;
|
||||
|
||||
if (relativePathBuffer.Length != 0)
|
||||
relativePathBuffer[0] = NullTerminator;
|
||||
|
||||
if (path.At(0) == Dot && (path.At(1) == NullTerminator || path.At(1) == DirectorySeparator ||
|
||||
path.At(1) == AltDirectorySeparator))
|
||||
{
|
||||
if (relativePathBuffer.Length >= 2)
|
||||
{
|
||||
relativePathBuffer[0] = Dot;
|
||||
relativePathBuffer[1] = NullTerminator;
|
||||
}
|
||||
|
||||
newPath = path.Slice(1);
|
||||
length = 1;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
if (path.At(0) == Dot && path.At(1) == Dot)
|
||||
return ResultFs.DirectoryUnobtainable.Log();
|
||||
|
||||
newPath = path;
|
||||
length = 0;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static Result ParseRelativeDotPath(out ReadOnlySpan<byte> newPath, out int length,
|
||||
Span<byte> relativePathBuffer, ReadOnlySpan<byte> path)
|
||||
{
|
||||
return ParseRelativeDotPathImpl(out newPath, out length, relativePathBuffer, path);
|
||||
}
|
||||
|
||||
public static Result SkipRelativeDotPath(out ReadOnlySpan<byte> newPath, out int length,
|
||||
ReadOnlySpan<byte> path)
|
||||
{
|
||||
return ParseRelativeDotPathImpl(out newPath, out length, Span<byte>.Empty, path);
|
||||
}
|
||||
|
||||
public static Result IsNormalized(out bool isNormalized, out int normalizedLength, ReadOnlySpan<byte> path,
|
||||
PathFlags flags)
|
||||
{
|
||||
UnsafeHelpers.SkipParamInit(out isNormalized, out normalizedLength);
|
||||
|
||||
Result rc = PathUtility12.CheckUtf8(path);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
ReadOnlySpan<byte> buffer = path;
|
||||
int totalLength = 0;
|
||||
|
||||
if (path.At(0) == NullTerminator)
|
||||
{
|
||||
if (!flags.IsEmptyPathAllowed())
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
isNormalized = true;
|
||||
normalizedLength = 0;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
if (path.At(0) != DirectorySeparator &&
|
||||
!flags.IsWindowsPathAllowed() &&
|
||||
!flags.IsRelativePathAllowed() &&
|
||||
!flags.IsMountNameAllowed())
|
||||
{
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
}
|
||||
|
||||
if (WindowsPath12.IsWindowsPath(path, false) && !flags.IsWindowsPathAllowed())
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
bool hasMountName = false;
|
||||
|
||||
rc = SkipMountName(out buffer, out int mountNameLength, buffer);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (mountNameLength != 0)
|
||||
{
|
||||
if (!flags.IsMountNameAllowed())
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
totalLength += mountNameLength;
|
||||
hasMountName = true;
|
||||
}
|
||||
|
||||
if (buffer.At(0) != DirectorySeparator && !PathUtility12.IsPathStartWithCurrentDirectory(buffer) &&
|
||||
!WindowsPath12.IsWindowsPath(buffer, false))
|
||||
{
|
||||
if (!flags.IsRelativePathAllowed() || !PathUtility12.CheckInvalidCharacter(buffer.At(0)).IsSuccess())
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
isNormalized = false;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
bool isRelativePath = false;
|
||||
|
||||
rc = SkipRelativeDotPath(out buffer, out int relativePathLength, buffer);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (relativePathLength != 0)
|
||||
{
|
||||
if (!flags.IsRelativePathAllowed())
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
totalLength += relativePathLength;
|
||||
|
||||
if (buffer.At(0) == NullTerminator)
|
||||
{
|
||||
isNormalized = true;
|
||||
normalizedLength = totalLength;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
isRelativePath = true;
|
||||
}
|
||||
|
||||
rc = SkipWindowsPath(out buffer, out int windowsPathLength, out bool isNormalizedWin, buffer, hasMountName);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (!isNormalizedWin)
|
||||
{
|
||||
if (!flags.IsWindowsPathAllowed())
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
isNormalized = false;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
if (windowsPathLength != 0)
|
||||
{
|
||||
if (!flags.IsWindowsPathAllowed())
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
totalLength += windowsPathLength;
|
||||
|
||||
if (isRelativePath)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
if (buffer.At(0) == NullTerminator)
|
||||
{
|
||||
isNormalized = false;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
if (buffer[i] == AltDirectorySeparator)
|
||||
{
|
||||
isNormalized = false;
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PathNormalizer12.IsParentDirectoryPathReplacementNeeded(buffer))
|
||||
return ResultFs.DirectoryUnobtainable.Log();
|
||||
|
||||
rc = PathUtility12.CheckInvalidBackslash(out bool isBackslashContained, buffer,
|
||||
flags.IsWindowsPathAllowed() || flags.IsBackslashAllowed());
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (isBackslashContained && !flags.IsBackslashAllowed())
|
||||
{
|
||||
isNormalized = false;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
rc = PathNormalizer12.IsNormalized(out isNormalized, out int length, buffer);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
totalLength += length;
|
||||
normalizedLength = totalLength;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static Result Normalize(Span<byte> outputBuffer, ReadOnlySpan<byte> path, PathFlags flags)
|
||||
{
|
||||
Result rc;
|
||||
|
||||
ReadOnlySpan<byte> src = path;
|
||||
int currentPos = 0;
|
||||
bool isWindowsPath = false;
|
||||
|
||||
if (path.At(0) == NullTerminator)
|
||||
{
|
||||
if (!flags.IsEmptyPathAllowed())
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
if (outputBuffer.Length != 0)
|
||||
outputBuffer[0] = NullTerminator;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
bool hasMountName = false;
|
||||
|
||||
if (flags.IsMountNameAllowed())
|
||||
{
|
||||
rc = ParseMountName(out src, out int mountNameLength, outputBuffer.Slice(currentPos), src);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
currentPos += mountNameLength;
|
||||
hasMountName = mountNameLength != 0;
|
||||
}
|
||||
|
||||
bool isDriveRelative = false;
|
||||
|
||||
if (src.At(0) != DirectorySeparator && !PathUtility12.IsPathStartWithCurrentDirectory(src) &&
|
||||
!WindowsPath12.IsWindowsPath(src, false))
|
||||
{
|
||||
if (!flags.IsRelativePathAllowed() || !PathUtility12.CheckInvalidCharacter(src.At(0)).IsSuccess())
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
outputBuffer[currentPos++] = Dot;
|
||||
isDriveRelative = true;
|
||||
}
|
||||
|
||||
if (flags.IsRelativePathAllowed())
|
||||
{
|
||||
if (currentPos >= outputBuffer.Length)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
rc = ParseRelativeDotPath(out src, out int relativePathLength, outputBuffer.Slice(currentPos), src);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
currentPos += relativePathLength;
|
||||
|
||||
if (src.At(0) == NullTerminator)
|
||||
{
|
||||
if (currentPos >= outputBuffer.Length)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
outputBuffer[currentPos] = NullTerminator;
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.IsWindowsPathAllowed())
|
||||
{
|
||||
ReadOnlySpan<byte> originalPath = src;
|
||||
|
||||
if (currentPos >= outputBuffer.Length)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
rc = ParseWindowsPath(out src, out int windowsPathLength, outputBuffer.Slice(currentPos), src,
|
||||
hasMountName);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
currentPos += windowsPathLength;
|
||||
|
||||
if (src.At(0) == NullTerminator)
|
||||
{
|
||||
// Note: Bug is in the original code. Should be "currentPos + 2"
|
||||
if (currentPos + 1 >= outputBuffer.Length)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
outputBuffer[currentPos] = DirectorySeparator;
|
||||
outputBuffer[currentPos + 1] = NullTerminator;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
int skippedLength = (int)Unsafe.ByteOffset(ref MemoryMarshal.GetReference(originalPath),
|
||||
ref MemoryMarshal.GetReference(src));
|
||||
|
||||
if (skippedLength > 0)
|
||||
isWindowsPath = true;
|
||||
}
|
||||
|
||||
rc = PathUtility12.CheckInvalidBackslash(out bool isBackslashContained, src,
|
||||
flags.IsWindowsPathAllowed() || flags.IsBackslashAllowed());
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
byte[] srcBufferSlashReplaced = null;
|
||||
try
|
||||
{
|
||||
if (isBackslashContained && flags.IsWindowsPathAllowed())
|
||||
{
|
||||
srcBufferSlashReplaced = ArrayPool<byte>.Shared.Rent(path.Length);
|
||||
|
||||
StringUtils.Copy(srcBufferSlashReplaced, path);
|
||||
PathUtility12.Replace(srcBufferSlashReplaced, AltDirectorySeparator, DirectorySeparator);
|
||||
|
||||
int srcOffset = (int)Unsafe.ByteOffset(ref MemoryMarshal.GetReference(path),
|
||||
ref MemoryMarshal.GetReference(src));
|
||||
|
||||
src = srcBufferSlashReplaced.AsSpan(srcOffset);
|
||||
}
|
||||
|
||||
rc = PathNormalizer12.Normalize(outputBuffer.Slice(currentPos), out _, src, isWindowsPath, isDriveRelative);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (srcBufferSlashReplaced is not null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(srcBufferSlashReplaced);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Result CheckPathFormat(ReadOnlySpan<byte> path, PathFlags flags)
|
||||
{
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
367
src/LibHac/Fs/Common/PathNormalizer12.cs
Normal file
367
src/LibHac/Fs/Common/PathNormalizer12.cs
Normal file
|
@ -0,0 +1,367 @@
|
|||
using System;
|
||||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.FsSystem;
|
||||
using static LibHac.Fs.Common.PathUtility12;
|
||||
using static LibHac.Fs.StringTraits;
|
||||
|
||||
namespace LibHac.Fs.Common
|
||||
{
|
||||
public static class PathNormalizer12
|
||||
{
|
||||
private enum PathState
|
||||
{
|
||||
Initial,
|
||||
Normal,
|
||||
FirstSeparator,
|
||||
Separator,
|
||||
CurrentDir,
|
||||
ParentDir
|
||||
}
|
||||
|
||||
public static Result Normalize(Span<byte> outputBuffer, out int length, ReadOnlySpan<byte> path, bool isWindowsPath,
|
||||
bool isDriveRelativePath)
|
||||
{
|
||||
UnsafeHelpers.SkipParamInit(out length);
|
||||
|
||||
ReadOnlySpan<byte> currentPath = path;
|
||||
int totalLength = 0;
|
||||
int i = 0;
|
||||
|
||||
if (!IsSeparator(path.At(0)))
|
||||
{
|
||||
if (!isDriveRelativePath)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
outputBuffer[totalLength++] = DirectorySeparator;
|
||||
}
|
||||
|
||||
var convertedPath = new RentedArray<byte>();
|
||||
try
|
||||
{
|
||||
// Check if parent directory path replacement is needed.
|
||||
if (IsParentDirectoryPathReplacementNeeded(currentPath))
|
||||
{
|
||||
// Allocate a buffer to hold the replacement path.
|
||||
convertedPath = new RentedArray<byte>(PathTools.MaxPathLength + 1);
|
||||
|
||||
// Replace the path.
|
||||
ReplaceParentDirectoryPath(convertedPath.Span, currentPath);
|
||||
|
||||
// Set current path to be the replacement path.
|
||||
currentPath = new U8Span(convertedPath.Span);
|
||||
}
|
||||
|
||||
bool skipNextSeparator = false;
|
||||
|
||||
while (!IsNul(currentPath.At(i)))
|
||||
{
|
||||
if (IsSeparator(currentPath[i]))
|
||||
{
|
||||
do
|
||||
{
|
||||
i++;
|
||||
} while (IsSeparator(currentPath.At(i)));
|
||||
|
||||
if (IsNul(currentPath.At(i)))
|
||||
break;
|
||||
|
||||
if (!skipNextSeparator)
|
||||
{
|
||||
if (totalLength + 1 == outputBuffer.Length)
|
||||
{
|
||||
outputBuffer[totalLength] = NullTerminator;
|
||||
length = totalLength;
|
||||
|
||||
return ResultFs.TooLongPath.Log();
|
||||
}
|
||||
|
||||
outputBuffer[totalLength++] = DirectorySeparator;
|
||||
}
|
||||
|
||||
skipNextSeparator = false;
|
||||
}
|
||||
|
||||
int dirLen = 0;
|
||||
while (!IsSeparator(currentPath.At(i + dirLen)) && !IsNul(currentPath.At(i + dirLen)))
|
||||
{
|
||||
dirLen++;
|
||||
}
|
||||
|
||||
if (IsCurrentDirectory(currentPath.Slice(i)))
|
||||
{
|
||||
skipNextSeparator = true;
|
||||
}
|
||||
else if (IsParentDirectory(currentPath.Slice(i)))
|
||||
{
|
||||
Assert.SdkAssert(outputBuffer[totalLength - 1] == DirectorySeparator);
|
||||
|
||||
if (!isWindowsPath)
|
||||
Assert.SdkAssert(outputBuffer[0] == DirectorySeparator);
|
||||
|
||||
if (totalLength == 1)
|
||||
{
|
||||
if (!isWindowsPath)
|
||||
return ResultFs.DirectoryUnobtainable.Log();
|
||||
|
||||
totalLength--;
|
||||
}
|
||||
else
|
||||
{
|
||||
totalLength -= 2;
|
||||
|
||||
do
|
||||
{
|
||||
if (outputBuffer[totalLength] == DirectorySeparator)
|
||||
break;
|
||||
|
||||
totalLength--;
|
||||
} while (totalLength != 0);
|
||||
}
|
||||
|
||||
if (!isWindowsPath)
|
||||
Assert.SdkAssert(outputBuffer[totalLength] == DirectorySeparator);
|
||||
|
||||
Assert.SdkAssert(totalLength < outputBuffer.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (totalLength + dirLen + 1 > outputBuffer.Length)
|
||||
{
|
||||
int copyLen = outputBuffer.Length - 1 - totalLength;
|
||||
|
||||
for (int j = 0; j < copyLen; j++)
|
||||
{
|
||||
outputBuffer[totalLength++] = currentPath[i + j];
|
||||
}
|
||||
|
||||
outputBuffer[totalLength] = NullTerminator;
|
||||
length = totalLength;
|
||||
return ResultFs.TooLongPath.Log();
|
||||
}
|
||||
|
||||
for (int j = 0; j < dirLen; j++)
|
||||
{
|
||||
outputBuffer[totalLength++] = currentPath[i + j];
|
||||
}
|
||||
}
|
||||
|
||||
i += dirLen;
|
||||
}
|
||||
|
||||
if (skipNextSeparator)
|
||||
totalLength--;
|
||||
|
||||
if (totalLength == 0 && outputBuffer.Length != 0)
|
||||
{
|
||||
totalLength = 1;
|
||||
outputBuffer[0] = DirectorySeparator;
|
||||
}
|
||||
|
||||
// Note: This bug is in the original code. They probably meant to put "totalLength + 1"
|
||||
if (totalLength - 1 > outputBuffer.Length)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
outputBuffer[totalLength] = NullTerminator;
|
||||
|
||||
Result rc = IsNormalized(out bool isNormalized, out _, outputBuffer);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
Assert.SdkAssert(isNormalized);
|
||||
|
||||
length = totalLength;
|
||||
return Result.Success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
convertedPath.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given path is normalized. Path must be a basic path, starting with a directory separator
|
||||
/// and not containing any sort of prefix such as a mount name.
|
||||
/// </summary>
|
||||
/// <param name="isNormalized">When this function returns <see cref="Result.Success"/>,
|
||||
/// contains <see langword="true"/> if the path is normalized or <see langword="false"/> if it is not.
|
||||
/// Contents are undefined if the function does not return <see cref="Result.Success"/>.
|
||||
/// </param>
|
||||
/// <param name="length">When this function returns <see cref="Result.Success"/> and
|
||||
/// <paramref name="isNormalized"/> is <see langword="true"/>, contains the length of the normalized path.
|
||||
/// Contents are undefined if the function does not return <see cref="Result.Success"/>
|
||||
/// or <paramref name="isNormalized"/> is <see langword="false"/>.
|
||||
/// </param>
|
||||
/// <param name="path">The path to check.</param>
|
||||
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
|
||||
/// <see cref="ResultFs.InvalidCharacter"/>: The path contains an invalid character.<br/>
|
||||
/// <see cref="ResultFs.InvalidPathFormat"/>: The path is not in a valid format.</returns>
|
||||
public static Result IsNormalized(out bool isNormalized, out int length, ReadOnlySpan<byte> path)
|
||||
{
|
||||
UnsafeHelpers.SkipParamInit(out isNormalized, out length);
|
||||
|
||||
var state = PathState.Initial;
|
||||
int pathLength = 0;
|
||||
|
||||
for (int i = 0; i < path.Length; i++)
|
||||
{
|
||||
byte c = path[i];
|
||||
if (c == NullTerminator) break;
|
||||
|
||||
pathLength++;
|
||||
|
||||
if (state != PathState.Initial)
|
||||
{
|
||||
Result rc = CheckInvalidCharacter(c);
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case PathState.Initial:
|
||||
if (c != DirectorySeparator)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
state = PathState.FirstSeparator;
|
||||
|
||||
break;
|
||||
case PathState.Normal:
|
||||
|
||||
if (c == DirectorySeparator)
|
||||
state = PathState.Separator;
|
||||
|
||||
break;
|
||||
case PathState.FirstSeparator:
|
||||
case PathState.Separator:
|
||||
if (c == DirectorySeparator)
|
||||
{
|
||||
isNormalized = false;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
state = c == Dot ? PathState.CurrentDir : PathState.Normal;
|
||||
break;
|
||||
case PathState.CurrentDir:
|
||||
if (c == DirectorySeparator)
|
||||
{
|
||||
isNormalized = false;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
state = c == Dot ? PathState.ParentDir : PathState.Normal;
|
||||
break;
|
||||
case PathState.ParentDir:
|
||||
if (c == DirectorySeparator)
|
||||
{
|
||||
isNormalized = false;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
state = PathState.Normal;
|
||||
break;
|
||||
// ReSharper disable once UnreachableSwitchCaseDueToIntegerAnalysis
|
||||
default:
|
||||
Abort.UnexpectedDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case PathState.Initial:
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
case PathState.Normal:
|
||||
case PathState.FirstSeparator:
|
||||
isNormalized = true;
|
||||
break;
|
||||
case PathState.Separator:
|
||||
case PathState.CurrentDir:
|
||||
case PathState.ParentDir:
|
||||
isNormalized = false;
|
||||
break;
|
||||
// ReSharper disable once UnreachableSwitchCaseDueToIntegerAnalysis
|
||||
default:
|
||||
Abort.UnexpectedDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
length = pathLength;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a path begins with / or \ and contains any of these patterns:
|
||||
/// "/..\", "\..\", "\../", "\..0" where '0' is the null terminator.
|
||||
/// </summary>
|
||||
public static bool IsParentDirectoryPathReplacementNeeded(ReadOnlySpan<byte> path)
|
||||
{
|
||||
if (path.Length == 0 || (path[0] != DirectorySeparator && path[0] != AltDirectorySeparator))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < path.Length - 2 && path[i] != NullTerminator; i++)
|
||||
{
|
||||
byte c3 = path.At(i + 3);
|
||||
|
||||
if (path[i] == AltDirectorySeparator &&
|
||||
path[i + 1] == Dot &&
|
||||
path[i + 2] == Dot &&
|
||||
(c3 == DirectorySeparator || c3 == AltDirectorySeparator || c3 == NullTerminator))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((path[i] == DirectorySeparator || path[i] == AltDirectorySeparator) &&
|
||||
path[i + 1] == Dot &&
|
||||
path[i + 2] == Dot &&
|
||||
c3 == AltDirectorySeparator)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void ReplaceParentDirectoryPath(Span<byte> dest, ReadOnlySpan<byte> source)
|
||||
{
|
||||
dest[0] = DirectorySeparator;
|
||||
|
||||
int i = 1;
|
||||
while (source.Length > i && source[i] != NullTerminator)
|
||||
{
|
||||
if (source.Length > i + 2 &&
|
||||
(source[i - 1] == DirectorySeparator || source[i - 1] == AltDirectorySeparator) &&
|
||||
source[i + 0] == Dot &&
|
||||
source[i + 1] == Dot &&
|
||||
(source[i + 2] == DirectorySeparator || source[i + 2] == AltDirectorySeparator))
|
||||
{
|
||||
dest[i - 1] = DirectorySeparator;
|
||||
dest[i + 0] = Dot;
|
||||
dest[i + 1] = Dot;
|
||||
dest[i + 2] = DirectorySeparator;
|
||||
i += 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (source.Length > i + 1 &&
|
||||
source[i - 1] == AltDirectorySeparator &&
|
||||
source[i + 0] == Dot &&
|
||||
source[i + 1] == Dot &&
|
||||
(source.Length == i + 2 || source[i + 2] == NullTerminator))
|
||||
{
|
||||
dest[i - 1] = DirectorySeparator;
|
||||
dest[i + 0] = Dot;
|
||||
dest[i + 1] = Dot;
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
|
||||
dest[i] = source[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
dest[i] = NullTerminator;
|
||||
}
|
||||
}
|
||||
}
|
245
src/LibHac/Fs/Common/PathUtility12.cs
Normal file
245
src/LibHac/Fs/Common/PathUtility12.cs
Normal file
|
@ -0,0 +1,245 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.FsSrv.Sf;
|
||||
using LibHac.Util;
|
||||
using static LibHac.Fs.StringTraits;
|
||||
|
||||
namespace LibHac.Fs.Common
|
||||
{
|
||||
public static class PathUtility12
|
||||
{
|
||||
public static void Replace(Span<byte> buffer, byte currentChar, byte newChar)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(buffer);
|
||||
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
if (buffer[i] == currentChar)
|
||||
{
|
||||
buffer[i] = newChar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsCurrentDirectory(ReadOnlySpan<byte> path)
|
||||
{
|
||||
if (path.Length < 1)
|
||||
return false;
|
||||
|
||||
return path[0] == Dot &&
|
||||
(path.Length < 2 || path[1] == NullTerminator || path[1] == DirectorySeparator);
|
||||
}
|
||||
|
||||
public static bool IsParentDirectory(ReadOnlySpan<byte> path)
|
||||
{
|
||||
if (path.Length < 2)
|
||||
return false;
|
||||
|
||||
return path[0] == Dot &&
|
||||
path[1] == Dot &&
|
||||
(path.Length < 3 || path[2] == NullTerminator || path[2] == DirectorySeparator);
|
||||
}
|
||||
|
||||
public static bool IsSeparator(byte c)
|
||||
{
|
||||
return c == DirectorySeparator;
|
||||
}
|
||||
|
||||
public static bool IsNul(byte c)
|
||||
{
|
||||
return c == NullTerminator;
|
||||
}
|
||||
|
||||
public static Result ConvertToFspPath(out FspPath fspPath, ReadOnlySpan<byte> path)
|
||||
{
|
||||
UnsafeHelpers.SkipParamInit(out fspPath);
|
||||
|
||||
int length = StringUtils.Copy(SpanHelpers.AsByteSpan(ref fspPath), path, PathTool.EntryNameLengthMax + 1);
|
||||
|
||||
if (length >= PathTool.EntryNameLengthMax + 1)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
Result rc = PathFormatter.SkipMountName(out ReadOnlySpan<byte> pathWithoutMountName, out _,
|
||||
new U8Span(path));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (!WindowsPath12.IsWindowsPath(pathWithoutMountName, true))
|
||||
{
|
||||
Replace(SpanHelpers.AsByteSpan(ref fspPath).Slice(0, 0x300), AltDirectorySeparator, DirectorySeparator);
|
||||
}
|
||||
else if (fspPath.Str[0] == DirectorySeparator && fspPath.Str[1] == DirectorySeparator)
|
||||
{
|
||||
SpanHelpers.AsByteSpan(ref fspPath)[0] = AltDirectorySeparator;
|
||||
SpanHelpers.AsByteSpan(ref fspPath)[1] = AltDirectorySeparator;
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static bool IsDirectoryPath(ReadOnlySpan<byte> path)
|
||||
{
|
||||
if (path.Length < 1 || path[0] == NullTerminator)
|
||||
return false;
|
||||
|
||||
int length = StringUtils.GetLength(path);
|
||||
return path[length - 1] == DirectorySeparator || path[length - 1] == AltDirectorySeparator;
|
||||
}
|
||||
|
||||
public static bool IsDirectoryPath(in FspPath path)
|
||||
{
|
||||
return IsDirectoryPath(SpanHelpers.AsReadOnlyByteSpan(in path));
|
||||
}
|
||||
|
||||
public static Result CheckUtf8(ReadOnlySpan<byte> path)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
uint utf8Buffer = 0;
|
||||
Span<byte> utf8BufferSpan = SpanHelpers.AsByteSpan(ref utf8Buffer);
|
||||
|
||||
ReadOnlySpan<byte> currentChar = path;
|
||||
|
||||
while (currentChar.Length > 0 && currentChar[0] != NullTerminator)
|
||||
{
|
||||
utf8BufferSpan.Clear();
|
||||
|
||||
CharacterEncodingResult result =
|
||||
CharacterEncoding.PickOutCharacterFromUtf8String(utf8BufferSpan, ref currentChar);
|
||||
|
||||
if (result != CharacterEncodingResult.Success)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
result = CharacterEncoding.ConvertCharacterUtf8ToUtf32(out _, utf8BufferSpan);
|
||||
|
||||
if (result != CharacterEncodingResult.Success)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static Result CheckInvalidCharacter(byte c)
|
||||
{
|
||||
/*
|
||||
The optimized code is equivalent to this:
|
||||
|
||||
ReadOnlySpan<byte> invalidChars = new[]
|
||||
{(byte) ':', (byte) '*', (byte) '?', (byte) '<', (byte) '>', (byte) '|'};
|
||||
|
||||
for (int i = 0; i < invalidChars.Length; i++)
|
||||
{
|
||||
if (c == invalidChars[i])
|
||||
return ResultFs.InvalidCharacter.Log();
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
*/
|
||||
|
||||
const ulong mask = (1ul << (byte)':') |
|
||||
(1ul << (byte)'*') |
|
||||
(1ul << (byte)'?') |
|
||||
(1ul << (byte)'<') |
|
||||
(1ul << (byte)'>');
|
||||
|
||||
if (c <= 0x3Fu && ((1ul << c) & mask) != 0 || c == (byte)'|')
|
||||
return ResultFs.InvalidCharacter.Log();
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static Result CheckInvalidBackslash(out bool containsBackslash, ReadOnlySpan<byte> path, bool allowBackslash)
|
||||
{
|
||||
containsBackslash = false;
|
||||
|
||||
for (int i = 0; i < path.Length && path[i] != NullTerminator; i++)
|
||||
{
|
||||
if (path[i] == '\\')
|
||||
{
|
||||
containsBackslash = true;
|
||||
|
||||
if (!allowBackslash)
|
||||
return ResultFs.InvalidCharacter.Log();
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static Result CheckEntryNameBytes(ReadOnlySpan<byte> path, int maxEntryLength)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
int currentEntryLength = 0;
|
||||
|
||||
for (int i = 0; i < path.Length && path[i] != NullTerminator; i++)
|
||||
{
|
||||
currentEntryLength++;
|
||||
|
||||
if (path[i] == DirectorySeparator || path[i] == AltDirectorySeparator)
|
||||
currentEntryLength = 0;
|
||||
|
||||
// Note: The original does use >= instead of >
|
||||
if (currentEntryLength >= maxEntryLength)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public static bool IsSubPath(ReadOnlySpan<byte> lhs, ReadOnlySpan<byte> rhs)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(lhs);
|
||||
Assert.SdkRequiresNotNull(rhs);
|
||||
|
||||
if (WindowsPath12.IsUncPath(lhs) && !WindowsPath12.IsUncPath(rhs))
|
||||
return false;
|
||||
|
||||
if (!WindowsPath12.IsUncPath(lhs) && WindowsPath12.IsUncPath(rhs))
|
||||
return false;
|
||||
|
||||
if (lhs.At(0) == DirectorySeparator && lhs.At(1) == NullTerminator &&
|
||||
rhs.At(0) == DirectorySeparator && rhs.At(1) != NullTerminator)
|
||||
return true;
|
||||
|
||||
if (rhs.At(0) == DirectorySeparator && rhs.At(1) == NullTerminator &&
|
||||
lhs.At(0) == DirectorySeparator && lhs.At(1) != NullTerminator)
|
||||
return true;
|
||||
|
||||
for (int i = 0; ; i++)
|
||||
{
|
||||
if (lhs.At(i) == NullTerminator)
|
||||
{
|
||||
return rhs.At(i) == DirectorySeparator;
|
||||
}
|
||||
else if (rhs.At(i) == NullTerminator)
|
||||
{
|
||||
return lhs.At(i) == DirectorySeparator;
|
||||
}
|
||||
else if (lhs.At(i) != rhs.At(i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsPathAbsolute(ReadOnlySpan<byte> path)
|
||||
{
|
||||
if (WindowsPath12.IsWindowsPath(path, false))
|
||||
return true;
|
||||
|
||||
return path.At(0) == DirectorySeparator;
|
||||
}
|
||||
|
||||
public static bool IsPathRelative(ReadOnlySpan<byte> path)
|
||||
{
|
||||
return path.At(0) != NullTerminator && !IsPathAbsolute(path);
|
||||
}
|
||||
|
||||
public static bool IsPathStartWithCurrentDirectory(ReadOnlySpan<byte> path)
|
||||
{
|
||||
return IsCurrentDirectory(path) || IsParentDirectory(path);
|
||||
}
|
||||
}
|
||||
}
|
249
src/LibHac/Fs/Common/WindowsPath12.cs
Normal file
249
src/LibHac/Fs/Common/WindowsPath12.cs
Normal file
|
@ -0,0 +1,249 @@
|
|||
using System;
|
||||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Util;
|
||||
using static LibHac.Util.CharacterEncoding;
|
||||
|
||||
namespace LibHac.Fs.Common
|
||||
{
|
||||
public static class WindowsPath12
|
||||
{
|
||||
public static int GetCodePointByteLength(byte firstCodeUnit)
|
||||
{
|
||||
if ((firstCodeUnit & 0x80) == 0x00) return 1;
|
||||
if ((firstCodeUnit & 0xE0) == 0xC0) return 2;
|
||||
if ((firstCodeUnit & 0xF0) == 0xE0) return 3;
|
||||
if ((firstCodeUnit & 0xF8) == 0xF0) return 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool IsUncPathImpl(ReadOnlySpan<byte> path, bool checkForwardSlash, bool checkBackSlash)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
if ((uint)path.Length < 2)
|
||||
return false;
|
||||
|
||||
if (checkForwardSlash && path[0] == '/' && path[1] == '/')
|
||||
return true;
|
||||
|
||||
return checkBackSlash && path[0] == '\\' && path[1] == '\\';
|
||||
}
|
||||
|
||||
private static bool IsUncPathImpl(ReadOnlySpan<char> path, bool checkForwardSlash, bool checkBackSlash)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
if ((uint)path.Length < 2)
|
||||
return false;
|
||||
|
||||
if (checkForwardSlash && path[0] == '/' && path[1] == '/')
|
||||
return true;
|
||||
|
||||
return checkBackSlash && path[0] == '\\' && path[1] == '\\';
|
||||
}
|
||||
|
||||
private static int GetUncPathPrefixLengthImpl(ReadOnlySpan<byte> path, bool checkForwardSlash)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
int length;
|
||||
int separatorCount = 0;
|
||||
|
||||
for (length = 0; length < path.Length && path[length] != 0; length++)
|
||||
{
|
||||
if (checkForwardSlash && path[length] == '/')
|
||||
++separatorCount;
|
||||
|
||||
if (path[length] == '\\')
|
||||
++separatorCount;
|
||||
|
||||
if (separatorCount == 4)
|
||||
return length;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
private static int GetUncPathPrefixLengthImpl(ReadOnlySpan<char> path, bool checkForwardSlash)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
int length;
|
||||
int separatorCount = 0;
|
||||
|
||||
for (length = 0; length < path.Length && path[length] != 0; length++)
|
||||
{
|
||||
if (checkForwardSlash && path[length] == '/')
|
||||
++separatorCount;
|
||||
|
||||
if (path[length] == '\\')
|
||||
++separatorCount;
|
||||
|
||||
if (separatorCount == 4)
|
||||
return length;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
private static bool IsDosDevicePathImpl(ReadOnlySpan<byte> path)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
if ((uint)path.Length < 4)
|
||||
return false;
|
||||
|
||||
return path[0] == '\\' &&
|
||||
path[1] == '\\' &&
|
||||
(path[2] == '.' || path[2] == '?') &&
|
||||
(path[3] == '/' || path[3] == '\\');
|
||||
}
|
||||
|
||||
private static bool IsDosDevicePathImpl(ReadOnlySpan<char> path)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
if ((uint)path.Length < 4)
|
||||
return false;
|
||||
|
||||
return path[0] == '\\' &&
|
||||
path[1] == '\\' &&
|
||||
(path[2] == '.' || path[2] == '?') &&
|
||||
(path[3] == '/' || path[3] == '\\');
|
||||
}
|
||||
|
||||
public static bool IsWindowsDrive(ReadOnlySpan<byte> path)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
if ((uint)path.Length < 2)
|
||||
return false;
|
||||
|
||||
// Mask lowercase letters to uppercase and check if it's in range
|
||||
return ((0b1101_1111 & path[0]) - 'A' <= 'Z' - 'A') && path[1] == ':';
|
||||
// return ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') && path[1] == ':';
|
||||
}
|
||||
|
||||
public static bool IsUncPath(ReadOnlySpan<byte> path)
|
||||
{
|
||||
return IsUncPathImpl(path, true, true);
|
||||
}
|
||||
|
||||
public static bool IsUncPath(ReadOnlySpan<byte> path, bool checkForwardSlash, bool checkBackSlash)
|
||||
{
|
||||
return IsUncPathImpl(path, checkForwardSlash, checkBackSlash);
|
||||
}
|
||||
|
||||
public static int GetUncPathPrefixLength(ReadOnlySpan<byte> path)
|
||||
{
|
||||
return GetUncPathPrefixLengthImpl(path, true);
|
||||
}
|
||||
|
||||
public static bool IsDosDevicePath(ReadOnlySpan<byte> path)
|
||||
{
|
||||
return IsDosDevicePathImpl(path);
|
||||
}
|
||||
|
||||
public static int GetDosDevicePathPrefixLength()
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
public static bool IsWindowsPath(ReadOnlySpan<byte> path, bool checkForwardSlash)
|
||||
{
|
||||
return IsWindowsDrive(path) || IsDosDevicePath(path) || IsUncPath(path, checkForwardSlash, true);
|
||||
}
|
||||
|
||||
public static int GetWindowsSkipLength(ReadOnlySpan<byte> path)
|
||||
{
|
||||
if (IsDosDevicePath(path))
|
||||
return GetDosDevicePathPrefixLength();
|
||||
|
||||
if (IsWindowsDrive(path))
|
||||
return 2;
|
||||
|
||||
if (IsUncPath(path))
|
||||
return GetUncPathPrefixLength(path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static bool IsDosDelimiterW(char c)
|
||||
{
|
||||
return c == '/' || c == '\\';
|
||||
}
|
||||
|
||||
public static bool IsWindowsDriveW(ReadOnlySpan<char> path)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
if ((uint)path.Length < 2)
|
||||
return false;
|
||||
|
||||
// Mask lowercase letters to uppercase and check if it's in range
|
||||
return ((0b1101_1111 & path[0]) - 'A' <= 'Z' - 'A') && path[1] == ':';
|
||||
// return ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') && path[1] == ':';
|
||||
}
|
||||
|
||||
public static bool IsUncPathW(ReadOnlySpan<char> path)
|
||||
{
|
||||
return IsUncPathImpl(path, true, true);
|
||||
}
|
||||
|
||||
public static int GetUncPathPrefixLengthW(ReadOnlySpan<char> path)
|
||||
{
|
||||
return GetUncPathPrefixLengthImpl(path, true);
|
||||
}
|
||||
|
||||
public static bool IsDosDevicePathW(ReadOnlySpan<char> path)
|
||||
{
|
||||
return IsDosDevicePathImpl(path);
|
||||
}
|
||||
|
||||
public static bool IsWindowsPathW(ReadOnlySpan<char> path)
|
||||
{
|
||||
return IsWindowsDriveW(path) || IsUncPathW(path) || IsDosDevicePathW(path);
|
||||
}
|
||||
|
||||
public static Result CheckCharacterCountForWindows(ReadOnlySpan<byte> path, int maxNameLength, int maxPathLength)
|
||||
{
|
||||
Assert.SdkRequiresNotNull(path);
|
||||
|
||||
ReadOnlySpan<byte> currentChar = path;
|
||||
int currentNameLength = 0;
|
||||
int currentPathLength = 0;
|
||||
|
||||
while (currentChar.Length > 1 && currentChar[0] != 0)
|
||||
{
|
||||
int utf16CodeUnitCount = GetCodePointByteLength(currentChar[0]) < 4 ? 1 : 2;
|
||||
|
||||
int utf8Buffer = 0;
|
||||
CharacterEncodingResult result =
|
||||
PickOutCharacterFromUtf8String(SpanHelpers.AsByteSpan(ref utf8Buffer), ref currentChar);
|
||||
|
||||
if (result != CharacterEncodingResult.Success)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
result = ConvertCharacterUtf8ToUtf32(out uint pathChar, SpanHelpers.AsReadOnlyByteSpan(in utf8Buffer));
|
||||
|
||||
if (result != CharacterEncodingResult.Success)
|
||||
return ResultFs.InvalidPathFormat.Log();
|
||||
|
||||
currentNameLength += utf16CodeUnitCount;
|
||||
currentPathLength += utf16CodeUnitCount;
|
||||
|
||||
if (pathChar == '/' || pathChar == '\\')
|
||||
currentNameLength = 0;
|
||||
|
||||
if (maxNameLength > 0 && currentNameLength > maxNameLength)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
|
||||
if (maxPathLength > 0 && currentPathLength > maxPathLength)
|
||||
return ResultFs.TooLongPath.Log();
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
396
tests/LibHac.Tests/Fs/PathFormatterTests.cs
Normal file
396
tests/LibHac.Tests/Fs/PathFormatterTests.cs
Normal file
|
@ -0,0 +1,396 @@
|
|||
// ReSharper disable InconsistentNaming
|
||||
|
||||
using System;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Common;
|
||||
using LibHac.Util;
|
||||
using Xunit;
|
||||
|
||||
namespace LibHac.Tests.Fs
|
||||
{
|
||||
public class PathFormatterTests
|
||||
{
|
||||
public static TheoryData<string, string, string, Result> TestData_Normalize_EmptyPath => new()
|
||||
{
|
||||
{ @"", "", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"", "E", @"", Result.Success },
|
||||
{ @"/aa/bb/../cc", "E", @"/aa/cc", Result.Success }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_Normalize_EmptyPath))]
|
||||
public static void Normalize_EmptyPath(string path, string pathFlags, string expectedNormalized, Result expectedResult)
|
||||
{
|
||||
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, string, Result> TestData_Normalize_MountName => new()
|
||||
{
|
||||
{ @"mount:/aa/bb", "", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:/aa/bb", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:/aa/bb", "M", @"mount:/aa/bb", Result.Success },
|
||||
{ @"mount:/aa/./bb", "M", @"mount:/aa/bb", Result.Success },
|
||||
{ @"mount:\aa\bb", "M", @"mount:", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"m:/aa/bb", "M", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mo>unt:/aa/bb", "M", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"moun?t:/aa/bb", "M", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"mo&unt:/aa/bb", "M", @"mo&unt:/aa/bb", Result.Success },
|
||||
{ @"/aa/./bb", "M", @"/aa/bb", Result.Success },
|
||||
{ @"mount/aa/./bb", "M", @"", ResultFs.InvalidPathFormat.Value }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_Normalize_MountName))]
|
||||
public static void Normalize_MountName(string path, string pathFlags, string expectedNormalized, Result expectedResult)
|
||||
{
|
||||
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, string, Result> TestData_Normalize_WindowsPath => new()
|
||||
{
|
||||
{ @"c:/aa/bb", "", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"c:\aa\bb", "", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"\\host\share", "", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"\\.\c:\", "", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"\\.\c:/aa/bb/.", "", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"\\?\c:\", "", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"mount:\\host\share\aa\bb", "M", @"mount:", ResultFs.InvalidCharacter.Value },
|
||||
{ @"mount:\\host/share\aa\bb", "M", @"mount:", ResultFs.InvalidCharacter.Value },
|
||||
{ @"mount:/\\aa\..\bb", "MW", @"mount:", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:/c:\aa\..\bb", "MW", @"mount:c:/bb", Result.Success },
|
||||
{ @"mount:/aa/bb", "MW", @"mount:/aa/bb", Result.Success },
|
||||
{ @"/mount:/aa/bb", "MW", @"/mount:/aa/bb", ResultFs.InvalidCharacter.Value },
|
||||
{ @"/mount:/aa/bb", "W", @"/mount:/aa/bb", ResultFs.InvalidCharacter.Value },
|
||||
{ @"a:aa/../bb", "MW", @"a:aa/bb", Result.Success },
|
||||
{ @"a:aa\..\bb", "MW", @"a:aa/bb", Result.Success },
|
||||
{ @"/a:aa\..\bb", "W", @"/bb", Result.Success },
|
||||
{ @"\\?\c:\.\aa", "W", @"\\?\c:/aa", Result.Success },
|
||||
{ @"\\.\c:\.\aa", "W", @"\\.\c:/aa", Result.Success },
|
||||
{ @"\\.\mount:\.\aa", "W", @"\\./mount:/aa", ResultFs.InvalidCharacter.Value },
|
||||
{ @"\\./.\aa", "W", @"\\./aa", Result.Success },
|
||||
{ @"\\/aa", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\\aa", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\", "W", @"/", Result.Success },
|
||||
{ @"\\host\share", "W", @"\\host\share/", Result.Success },
|
||||
{ @"\\host\share\path", "W", @"\\host\share/path", Result.Success },
|
||||
{ @"\\host\share\path\aa\bb\..\cc\.", "W", @"\\host\share/path/aa/cc", Result.Success },
|
||||
{ @"\\host\", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\ho$st\share\path", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host:\share\path", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\..\share\path", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host\s:hare\path", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host\.\path", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host\..\path", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host\sha:re", "W", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @".\\host\share", "RW", @"..\\host\share/", Result.Success }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_Normalize_WindowsPath))]
|
||||
public static void Normalize_WindowsPath(string path, string pathFlags, string expectedNormalized, Result expectedResult)
|
||||
{
|
||||
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, string, Result> TestData_Normalize_RelativePath => new()
|
||||
{
|
||||
{ @"./aa/bb", "", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"./aa/bb/../cc", "R", @"./aa/cc", Result.Success },
|
||||
{ @".\aa/bb/../cc", "R", @"..", ResultFs.InvalidCharacter.Value },
|
||||
{ @".", "R", @".", Result.Success },
|
||||
{ @"../aa/bb", "R", @"", ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"/aa/./bb", "R", @"/aa/bb", Result.Success },
|
||||
{ @"mount:./aa/bb", "MR", @"mount:./aa/bb", Result.Success },
|
||||
{ @"mount:./aa/./bb", "MR", @"mount:./aa/bb", Result.Success },
|
||||
{ @"mount:./aa/bb", "M", @"mount:", ResultFs.InvalidPathFormat.Value }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_Normalize_RelativePath))]
|
||||
public static void Normalize_RelativePath(string path, string pathFlags, string expectedNormalized, Result expectedResult)
|
||||
{
|
||||
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, string, Result> TestData_Normalize_Backslash => new()
|
||||
{
|
||||
{ @"\aa\bb\..\cc", "", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\aa\bb\..\cc", "B", @"", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"/aa\bb\..\cc", "", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"/aa\bb\..\cc", "B", @"/cc", Result.Success },
|
||||
{ @"/aa\bb\cc", "", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"/aa\bb\cc", "B", @"/aa\bb\cc", Result.Success },
|
||||
{ @"\\host\share\path\aa\bb\cc", "W", @"\\host\share/path/aa/bb/cc", Result.Success },
|
||||
{ @"\\host\share\path\aa\bb\cc", "WB", @"\\host\share/path/aa/bb/cc", Result.Success },
|
||||
{ @"/aa/bb\../cc/..\dd\..\ee/..", "", @"", ResultFs.InvalidCharacter.Value },
|
||||
{ @"/aa/bb\../cc/..\dd\..\ee/..", "B", @"/aa", Result.Success }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_Normalize_Backslash))]
|
||||
public static void Normalize_Backslash(string path, string pathFlags, string expectedNormalized, Result expectedResult)
|
||||
{
|
||||
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, string, Result> TestData_Normalize_All => new()
|
||||
{
|
||||
{ @"mount:./aa/bb", "WRM", @"mount:./aa/bb", Result.Success },
|
||||
{ @"mount:./aa/bb\cc/dd", "WRM", @"mount:./aa/bb/cc/dd", Result.Success },
|
||||
{ @"mount:./aa/bb\cc/dd", "WRMB", @"mount:./aa/bb/cc/dd", Result.Success },
|
||||
{ @"mount:./.c:/aa/bb", "RM", @"mount:./.c:/aa/bb", ResultFs.InvalidCharacter.Value },
|
||||
{ @"mount:.c:/aa/bb", "WRM", @"mount:./.c:/aa/bb", ResultFs.InvalidCharacter.Value },
|
||||
{ @"mount:./cc:/aa/bb", "WRM", @"mount:./cc:/aa/bb", ResultFs.InvalidCharacter.Value },
|
||||
{ @"mount:./\\host\share/aa/bb", "MW", @"mount:", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:./\\host\share/aa/bb", "WRM", @"mount:.\\host\share/aa/bb", Result.Success },
|
||||
{ @"mount:.\\host\share/aa/bb", "WRM", @"mount:..\\host\share/aa/bb", Result.Success },
|
||||
{ @"mount:..\\host\share/aa/bb", "WRM", @"mount:.", ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @".\\host\share/aa/bb", "WRM", @"..\\host\share/aa/bb", Result.Success },
|
||||
{ @"..\\host\share/aa/bb", "WRM", @".", ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"mount:\\host\share/aa/bb", "MW", @"mount:\\host\share/aa/bb", Result.Success },
|
||||
{ @"mount:\aa\bb", "BM", @"mount:", ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:/aa\bb", "BM", @"mount:/aa\bb", Result.Success },
|
||||
{ @".//aa/bb", "RW", @"./aa/bb", Result.Success },
|
||||
{ @"./aa/bb", "R", @"./aa/bb", Result.Success },
|
||||
{ @"./c:/aa/bb", "RW", @"./c:/aa/bb", ResultFs.InvalidCharacter.Value }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_Normalize_All))]
|
||||
public static void Normalize_All(string path, string pathFlags, string expectedNormalized, Result expectedResult)
|
||||
{
|
||||
NormalizeImpl(path, pathFlags, 0x301, expectedNormalized, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, int, string, Result> TestData_Normalize_SmallBuffer => new()
|
||||
{
|
||||
{ @"/aa/bb", "M", 1, @"", ResultFs.TooLongPath.Value },
|
||||
{ @"mount:/aa/bb", "MR", 6, @"", ResultFs.TooLongPath.Value },
|
||||
{ @"mount:/aa/bb", "MR", 7, @"mount:", ResultFs.TooLongPath.Value },
|
||||
{ @"aa/bb", "MR", 3, @"./", ResultFs.TooLongPath.Value },
|
||||
{ @"\\host\share", "W", 13, @"\\host\share", ResultFs.TooLongPath.Value }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_Normalize_SmallBuffer))]
|
||||
public static void Normalize_SmallBuffer(string path, string pathFlags, int bufferSize, string expectedNormalized, Result expectedResult)
|
||||
{
|
||||
NormalizeImpl(path, pathFlags, bufferSize, expectedNormalized, expectedResult);
|
||||
}
|
||||
|
||||
private static void NormalizeImpl(string path, string pathFlags, int bufferSize, string expectedNormalized, Result expectedResult)
|
||||
{
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
|
||||
Result result = PathFormatter.Normalize(buffer, path.ToU8Span(), GetPathFlags(pathFlags));
|
||||
|
||||
Assert.Equal(expectedResult, result);
|
||||
Assert.Equal(expectedNormalized, StringUtils.Utf8ZToString(buffer));
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_EmptyPath => new()
|
||||
{
|
||||
{ @"", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"", "E", true, 0, Result.Success },
|
||||
{ @"/aa/bb/../cc", "E", false, 0, Result.Success }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_IsNormalized_EmptyPath))]
|
||||
public static void IsNormalized_EmptyPath(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
|
||||
Result expectedResult)
|
||||
{
|
||||
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_MountName => new()
|
||||
{
|
||||
{ @"mount:/aa/bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:/aa/bb", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:/aa/bb", "M", true, 12, Result.Success },
|
||||
{ @"mount:/aa/./bb", "M", false, 6, Result.Success },
|
||||
{ @"mount:\aa\bb", "M", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"m:/aa/bb", "M", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mo>unt:/aa/bb", "M", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"moun?t:/aa/bb", "M", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"mo&unt:/aa/bb", "M", true, 13, Result.Success },
|
||||
{ @"/aa/./bb", "M", false, 0, Result.Success },
|
||||
{ @"mount/aa/./bb", "M", false, 0, ResultFs.InvalidPathFormat.Value }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_IsNormalized_MountName))]
|
||||
public static void IsNormalized_MountName(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
|
||||
Result expectedResult)
|
||||
{
|
||||
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_WindowsPath => new()
|
||||
{
|
||||
{ @"c:/aa/bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"c:/aa/bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"c:\aa\bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host\share", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\.\c:\", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\.\c:/aa/bb/.", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\?\c:\", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:\\host\share\aa\bb", "M", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:\\host/share\aa\bb", "M", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:/\\aa\..\bb", "MW", false, 0, Result.Success },
|
||||
{ @"mount:/c:\aa\..\bb", "MW", false, 0, Result.Success },
|
||||
{ @"mount:/aa/bb", "MW", true, 12, Result.Success },
|
||||
{ @"/mount:/aa/bb", "MW", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"/mount:/aa/bb", "W", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"a:aa/../bb", "MW", false, 8, Result.Success },
|
||||
{ @"a:aa\..\bb", "MW", false, 0, Result.Success },
|
||||
{ @"/a:aa\..\bb", "W", false, 0, ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"\\?\c:\.\aa", "W", false, 0, Result.Success },
|
||||
{ @"\\.\c:\.\aa", "W", false, 0, Result.Success },
|
||||
{ @"\\.\mount:\.\aa", "W", false, 0, Result.Success },
|
||||
{ @"\\./.\aa", "W", false, 0, Result.Success },
|
||||
{ @"\\/aa", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\\aa", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\", "W", false, 0, Result.Success },
|
||||
{ @"\\host\share", "W", false, 0, Result.Success },
|
||||
{ @"\\host\share\path", "W", false, 0, Result.Success },
|
||||
{ @"\\host\share\path\aa\bb\..\cc\.", "W", false, 0, Result.Success },
|
||||
{ @"\\host\", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\ho$st\share\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host:\share\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\..\share\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host\s:hare\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host\.\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host\..\path", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\host\sha:re", "W", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @".\\host\share", "RW", false, 0, Result.Success }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_IsNormalized_WindowsPath))]
|
||||
public static void IsNormalized_WindowsPath(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
|
||||
Result expectedResult)
|
||||
{
|
||||
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_RelativePath => new()
|
||||
{
|
||||
{ @"./aa/bb", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"./aa/bb/../cc", "R", false, 1, Result.Success },
|
||||
{ @".\aa/bb/../cc", "R", false, 0, Result.Success },
|
||||
{ @".", "R", true, 1, Result.Success },
|
||||
{ @"../aa/bb", "R", false, 0, ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"/aa/./bb", "R", false, 0, Result.Success },
|
||||
{ @"mount:./aa/bb", "MR", true, 13, Result.Success },
|
||||
{ @"mount:./aa/./bb", "MR", false, 7, Result.Success },
|
||||
{ @"mount:./aa/bb", "M", false, 0, ResultFs.InvalidPathFormat.Value }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_IsNormalized_RelativePath))]
|
||||
public static void IsNormalized_RelativePath(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
|
||||
Result expectedResult)
|
||||
{
|
||||
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_Backslash => new()
|
||||
{
|
||||
{ @"\aa\bb\..\cc", "", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\aa\bb\..\cc", "B", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"/aa\bb\..\cc", "", false, 0, ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"/aa\bb\..\cc", "B", false, 0, ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"/aa\bb\cc", "", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"/aa\bb\cc", "B", true, 9, Result.Success },
|
||||
{ @"\\host\share\path\aa\bb\cc", "W", false, 0, Result.Success },
|
||||
{ @"\\host\share\path\aa\bb\cc", "WB", false, 0, Result.Success },
|
||||
{ @"/aa/bb\../cc/..\dd\..\ee/..", "", false, 0, ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"/aa/bb\../cc/..\dd\..\ee/..", "B", false, 0, ResultFs.DirectoryUnobtainable.Value }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_IsNormalized_Backslash))]
|
||||
public static void IsNormalized_Backslash(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
|
||||
Result expectedResult)
|
||||
{
|
||||
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, bool, int, Result> TestData_IsNormalized_All => new()
|
||||
{
|
||||
{ @"mount:./aa/bb", "WRM", true, 13, Result.Success },
|
||||
{ @"mount:./aa/bb\cc/dd", "WRM", false, 0, Result.Success },
|
||||
{ @"mount:./aa/bb\cc/dd", "WRMB", true, 19, Result.Success },
|
||||
{ @"mount:./.c:/aa/bb", "RM", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"mount:.c:/aa/bb", "WRM", false, 0, Result.Success },
|
||||
{ @"mount:./cc:/aa/bb", "WRM", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"mount:./\\host\share/aa/bb", "MW", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:./\\host\share/aa/bb", "WRM", false, 0, Result.Success },
|
||||
{ @"mount:.\\host\share/aa/bb", "WRM", false, 0, Result.Success },
|
||||
{ @"mount:..\\host\share/aa/bb", "WRM", false, 0, Result.Success },
|
||||
{ @".\\host\share/aa/bb", "WRM", false, 0, Result.Success },
|
||||
{ @"..\\host\share/aa/bb", "WRM", false, 0, Result.Success },
|
||||
{ @"mount:\\host\share/aa/bb", "MW", true, 24, Result.Success },
|
||||
{ @"mount:\aa\bb", "BM", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:/aa\bb", "BM", true, 12, Result.Success },
|
||||
{ @".//aa/bb", "RW", false, 1, Result.Success },
|
||||
{ @"./aa/bb", "R", true, 7, Result.Success },
|
||||
{ @"./c:/aa/bb", "RW", false, 0, ResultFs.InvalidCharacter.Value }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_IsNormalized_All))]
|
||||
public static void IsNormalized_All(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
|
||||
Result expectedResult)
|
||||
{
|
||||
IsNormalizedImpl(path, pathFlags, expectedIsNormalized, expectedLength, expectedResult);
|
||||
}
|
||||
|
||||
private static void IsNormalizedImpl(string path, string pathFlags, bool expectedIsNormalized, long expectedLength,
|
||||
Result expectedResult)
|
||||
{
|
||||
Result result = PathFormatter.IsNormalized(out bool isNormalized, out int length, path.ToU8Span(),
|
||||
GetPathFlags(pathFlags));
|
||||
|
||||
Assert.Equal(expectedResult, result);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
Assert.Equal(expectedIsNormalized, isNormalized);
|
||||
|
||||
if (isNormalized)
|
||||
{
|
||||
Assert.Equal(expectedLength, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void IsNormalized_InvalidUtf8()
|
||||
{
|
||||
ReadOnlySpan<byte> invalidUtf8 = new byte[] { 0x44, 0xE3, 0xAA, 0x55, 0x50 };
|
||||
|
||||
Result result = PathFormatter.IsNormalized(out _, out _, invalidUtf8, new PathFlags());
|
||||
|
||||
Assert.Result(ResultFs.InvalidPathFormat, result);
|
||||
}
|
||||
|
||||
private static PathFlags GetPathFlags(string pathFlags)
|
||||
{
|
||||
var flags = new PathFlags();
|
||||
|
||||
foreach (char c in pathFlags)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case 'B':
|
||||
flags.AllowBackslash();
|
||||
break;
|
||||
case 'E':
|
||||
flags.AllowEmptyPath();
|
||||
break;
|
||||
case 'M':
|
||||
flags.AllowMountName();
|
||||
break;
|
||||
case 'R':
|
||||
flags.AllowRelativePath();
|
||||
break;
|
||||
case 'W':
|
||||
flags.AllowWindowsPath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
}
|
406
tests/LibHac.Tests/Fs/PathNormalizationTestGenerator.cpp
Normal file
406
tests/LibHac.Tests/Fs/PathNormalizationTestGenerator.cpp
Normal file
|
@ -0,0 +1,406 @@
|
|||
// Uses GLoat to run code in nnsdk https://github.com/h1k421/GLoat
|
||||
#include <gloat.hpp>
|
||||
|
||||
#include<array>
|
||||
#include<string>
|
||||
#include<tuple>
|
||||
|
||||
static char Buf[0x80000];
|
||||
static int BufPos = 0;
|
||||
|
||||
static char ResultNameBuf[0x100];
|
||||
|
||||
namespace nn::fs::detail {
|
||||
bool IsEnabledAccessLog();
|
||||
}
|
||||
|
||||
// SDK 12
|
||||
namespace nn::fs {
|
||||
bool IsSubPath(const char* path1, const char* path2);
|
||||
|
||||
class PathFlags {
|
||||
private:
|
||||
int32_t value;
|
||||
public:
|
||||
PathFlags() { value = 0; }
|
||||
|
||||
void AllowWindowsPath() { value |= (1 << 0); }
|
||||
void AllowRelativePath() { value |= (1 << 1); }
|
||||
void AllowEmptyPath() { value |= (1 << 2); }
|
||||
void AllowMountName() { value |= (1 << 3); }
|
||||
void AllowBackslash() { value |= (1 << 4); }
|
||||
|
||||
const bool IsWindowsPathAllowed() { return (value & (1 << 0)) != 0; }
|
||||
const bool IsRelativePathAllowed() { return (value & (1 << 1)) != 0; }
|
||||
const bool IsEmptyPathAllowed() { return (value & (1 << 2)) != 0; }
|
||||
const bool IsMountNameAllowed() { return (value & (1 << 3)) != 0; }
|
||||
const bool IsBackslashAllowed() { return (value & (1 << 4)) != 0; }
|
||||
};
|
||||
|
||||
class PathFormatter {
|
||||
public:
|
||||
static nn::Result Normalize(char* buffer, uint64_t normalizeBufferLength, const char* path, uint64_t pathLength, const nn::fs::PathFlags&);
|
||||
static nn::Result IsNormalized(bool* outIsNormalized, uint64_t* outNormalizedPathLength, const char* path, const nn::fs::PathFlags&);
|
||||
static nn::Result SkipWindowsPath(const char** outPath, uint64_t* outLength, bool* outIsNormalized, const char* path, bool hasMountName);
|
||||
static nn::Result SkipMountName(const char** outPath, uint64_t* outLength, const char* path);
|
||||
};
|
||||
|
||||
class PathNormalizer {
|
||||
public:
|
||||
static nn::Result Normalize(char* outBuffer, uint64_t* outLength, const char* path, uint64_t outBufferLength, bool isWindowsPath, bool isDriveRelative);
|
||||
static nn::Result IsNormalized(bool* outIsNormalized, uint64_t* outNormalizedPathLength, const char* path);
|
||||
};
|
||||
}
|
||||
|
||||
template<typename T, typename... Ts>
|
||||
constexpr auto make_array(T&& head, Ts&&... tail)->std::array<T, 1 + sizeof...(Ts)>
|
||||
{
|
||||
return { head, tail ... };
|
||||
}
|
||||
|
||||
template<size_t N, typename... Ts>
|
||||
void CreateTest(const char* name, void (*func)(Ts...), const std::array<std::tuple<Ts...>, N>& testData) {
|
||||
Buf[0] = '\n';
|
||||
BufPos = 1;
|
||||
|
||||
BufPos += sprintf(&Buf[BufPos], "%s\n", name);
|
||||
|
||||
for (auto item : testData) {
|
||||
std::apply(func, item);
|
||||
}
|
||||
|
||||
svcOutputDebugString(Buf, BufPos);
|
||||
}
|
||||
|
||||
const char* GetResultName(nn::Result result) {
|
||||
switch (result.GetValue()) {
|
||||
case 0: return "Result.Success";
|
||||
case 0x2EE402: return "ResultFs.InvalidPath.Value";
|
||||
case 0x2EE602: return "ResultFs.TooLongPath.Value";
|
||||
case 0x2EE802: return "ResultFs.InvalidCharacter.Value";
|
||||
case 0x2EEA02: return "ResultFs.InvalidPathFormat.Value";
|
||||
case 0x2EEC02: return "ResultFs.DirectoryUnobtainable.Value";
|
||||
default:
|
||||
sprintf(ResultNameBuf, "0x%x", result.GetValue());
|
||||
return ResultNameBuf;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr const char* const BoolStr(bool value)
|
||||
{
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
|
||||
nn::fs::PathFlags GetPathFlags(char const* pathFlags) {
|
||||
nn::fs::PathFlags flags = nn::fs::PathFlags();
|
||||
|
||||
for (char const* c = pathFlags; *c; c++) {
|
||||
switch (*c) {
|
||||
case 'B':
|
||||
flags.AllowBackslash();
|
||||
break;
|
||||
case 'E':
|
||||
flags.AllowEmptyPath();
|
||||
break;
|
||||
case 'M':
|
||||
flags.AllowMountName();
|
||||
break;
|
||||
case 'R':
|
||||
flags.AllowRelativePath();
|
||||
break;
|
||||
case 'W':
|
||||
flags.AllowWindowsPath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
static constexpr const auto TestData_PathFormatterNormalize_EmptyPath = make_array(
|
||||
// Check AllowEmptyPath option
|
||||
std::make_tuple("", ""),
|
||||
std::make_tuple("", "E"),
|
||||
std::make_tuple("/aa/bb/../cc", "E")
|
||||
);
|
||||
|
||||
static constexpr const auto TestData_PathFormatterNormalize_MountName = make_array(
|
||||
// Mount names should only be allowed with the AllowMountNames option
|
||||
std::make_tuple("mount:/aa/bb", ""), // Mount name isn't allowed without the AllowMountNames option
|
||||
std::make_tuple("mount:/aa/bb", "W"),
|
||||
std::make_tuple("mount:/aa/bb", "M"), // Basic mount names
|
||||
std::make_tuple("mount:/aa/./bb", "M"),
|
||||
std::make_tuple("mount:\\aa\\bb", "M"),
|
||||
std::make_tuple("m:/aa/bb", "M"), // Windows mount name without AllowWindowsPath option
|
||||
std::make_tuple("mo>unt:/aa/bb", "M"), // Mount names with invalid characters
|
||||
std::make_tuple("moun?t:/aa/bb", "M"),
|
||||
std::make_tuple("mo&unt:/aa/bb", "M"), // Mount name with valid special character
|
||||
std::make_tuple("/aa/./bb", "M"), // AllowMountName set when path has no mount name
|
||||
std::make_tuple("mount/aa/./bb", "M") // Relative path or mount name is missing separator
|
||||
);
|
||||
|
||||
static constexpr const auto TestData_PathFormatterNormalize_WindowsPath = make_array(
|
||||
// Windows paths should only be allowed with the AllowWindowsPath option
|
||||
std::make_tuple(R"(c:/aa/bb)", ""),
|
||||
std::make_tuple(R"(c:\aa\bb)", ""),
|
||||
std::make_tuple(R"(\\host\share)", ""),
|
||||
std::make_tuple(R"(\\.\c:\)", ""),
|
||||
std::make_tuple(R"(\\.\c:/aa/bb/.)", ""),
|
||||
std::make_tuple(R"(\\?\c:\)", ""),
|
||||
std::make_tuple(R"(mount:\\host\share\aa\bb)", "M"), // Catch instances where the Windows path comes after other parts in the path
|
||||
std::make_tuple(R"(mount:\\host/share\aa\bb)", "M"), // And do it again with the UNC path not normalized
|
||||
|
||||
std::make_tuple(R"(mount:/\\aa\..\bb)", "MW"),
|
||||
std::make_tuple(R"(mount:/c:\aa\..\bb)", "MW"),
|
||||
std::make_tuple(R"(mount:/aa/bb)", "MW"),
|
||||
std::make_tuple(R"(/mount:/aa/bb)", "MW"),
|
||||
std::make_tuple(R"(/mount:/aa/bb)", "W"),
|
||||
std::make_tuple(R"(a:aa/../bb)", "MW"),
|
||||
std::make_tuple(R"(a:aa\..\bb)", "MW"),
|
||||
std::make_tuple(R"(/a:aa\..\bb)", "W"),
|
||||
std::make_tuple(R"(\\?\c:\.\aa)", "W"), // Path with win32 file namespace prefix
|
||||
std::make_tuple(R"(\\.\c:\.\aa)", "W"), // Path with win32 device namespace prefix
|
||||
std::make_tuple(R"(\\.\mount:\.\aa)", "W"),
|
||||
std::make_tuple(R"(\\./.\aa)", "W"),
|
||||
std::make_tuple(R"(\\/aa)", "W"),
|
||||
std::make_tuple(R"(\\\aa)", "W"),
|
||||
std::make_tuple(R"(\\)", "W"),
|
||||
std::make_tuple(R"(\\host\share)", "W"), // Basic UNC paths
|
||||
std::make_tuple(R"(\\host\share\path)", "W"),
|
||||
std::make_tuple(R"(\\host\share\path\aa\bb\..\cc\.)", "W"), // UNC path using only backslashes that is not normalized
|
||||
std::make_tuple(R"(\\host\)", "W"), // Share name cannot be empty
|
||||
std::make_tuple(R"(\\ho$st\share\path)", "W"), // Invalid character '$' in host name
|
||||
std::make_tuple(R"(\\host:\share\path)", "W"), // Invalid character ':' in host name
|
||||
std::make_tuple(R"(\\..\share\path)", "W"), // Host name can't be ".."
|
||||
std::make_tuple(R"(\\host\s:hare\path)", "W"), // Invalid character ':' in host name
|
||||
std::make_tuple(R"(\\host\.\path)", "W"), // Share name can't be "."
|
||||
std::make_tuple(R"(\\host\..\path)", "W"), // Share name can't be ".."
|
||||
std::make_tuple(R"(\\host\sha:re)", "W"), // Invalid share name when nothing follows it
|
||||
std::make_tuple(R"(.\\host\share)", "RW") // Can't have a relative Windows path
|
||||
);
|
||||
|
||||
static constexpr const auto TestData_PathFormatterNormalize_RelativePath = make_array(
|
||||
std::make_tuple("./aa/bb", ""), // Relative path isn't allowed without the AllowRelativePaths option
|
||||
std::make_tuple("./aa/bb/../cc", "R"), // Basic relative paths using different separators
|
||||
std::make_tuple(".\\aa/bb/../cc", "R"),
|
||||
std::make_tuple(".", "R"), // Standalone current directory
|
||||
std::make_tuple("../aa/bb", "R"), // Path starting with parent directory is not allowed
|
||||
std::make_tuple("/aa/./bb", "R"), // Absolute paths should work normally
|
||||
std::make_tuple("mount:./aa/bb", "MR"), // Mount name with relative path
|
||||
std::make_tuple("mount:./aa/./bb", "MR"),
|
||||
std::make_tuple("mount:./aa/bb", "M")
|
||||
);
|
||||
|
||||
static constexpr const auto TestData_PathFormatterNormalize_Backslash = make_array(
|
||||
std::make_tuple(R"(\aa\bb\..\cc)", ""), // Paths can't start with a backslash no matter the path flags set
|
||||
std::make_tuple(R"(\aa\bb\..\cc)", "B"),
|
||||
std::make_tuple(R"(/aa\bb\..\cc)", ""), // Paths can contain backslashes if they start with a frontslash and have AllowBackslash set
|
||||
std::make_tuple(R"(/aa\bb\..\cc)", "B"), // When backslashes are allowed they do not count as a directory separator
|
||||
std::make_tuple(R"(/aa\bb\cc)", ""), // Normalized path without a prefix except it uses backslashes
|
||||
std::make_tuple(R"(/aa\bb\cc)", "B"),
|
||||
std::make_tuple(R"(\\host\share\path\aa\bb\cc)", "W"), // Otherwise normalized Windows path except with backslashes
|
||||
std::make_tuple(R"(\\host\share\path\aa\bb\cc)", "WB"),
|
||||
std::make_tuple(R"(/aa/bb\../cc/..\dd\..\ee/..)", ""), // Path with "parent directory path replacement needed"
|
||||
std::make_tuple(R"(/aa/bb\../cc/..\dd\..\ee/..)", "B")
|
||||
);
|
||||
|
||||
static constexpr const auto TestData_PathFormatterNormalize_All = make_array(
|
||||
std::make_tuple(R"(mount:./aa/bb)", "WRM"), // Normalized path with both mount name and relative path
|
||||
std::make_tuple(R"(mount:./aa/bb\cc/dd)", "WRM"), // Path with backslashes
|
||||
std::make_tuple(R"(mount:./aa/bb\cc/dd)", "WRMB"), // This path is considered normalized but the backslashes still normalize to forward slashes
|
||||
std::make_tuple(R"(mount:./.c:/aa/bb)", "RM"), // These next 2 form a chain where if you normalize one it'll turn into the next
|
||||
std::make_tuple(R"(mount:.c:/aa/bb)", "WRM"),
|
||||
std::make_tuple(R"(mount:./cc:/aa/bb)", "WRM"),
|
||||
std::make_tuple(R"(mount:./\\host\share/aa/bb)", "MW"),
|
||||
std::make_tuple(R"(mount:./\\host\share/aa/bb)", "WRM"), // These next 3 form a chain where if you normalize one it'll turn into the next
|
||||
std::make_tuple(R"(mount:.\\host\share/aa/bb)", "WRM"),
|
||||
std::make_tuple(R"(mount:..\\host\share/aa/bb)", "WRM"),
|
||||
std::make_tuple(R"(.\\host\share/aa/bb)", "WRM"), // These next 2 form a chain where if you normalize one it'll turn into the next
|
||||
std::make_tuple(R"(..\\host\share/aa/bb)", "WRM"),
|
||||
std::make_tuple(R"(mount:\\host\share/aa/bb)", "MW"), // Use a mount name and windows path together
|
||||
std::make_tuple(R"(mount:\aa\bb)", "BM"), // Backslashes are never allowed directly after a mount name even with AllowBackslashes
|
||||
std::make_tuple(R"(mount:/aa\bb)", "BM"),
|
||||
std::make_tuple(R"(.//aa/bb)", "RW"), // Relative path followed by a Windows path won't work
|
||||
std::make_tuple(R"(./aa/bb)", "R"),
|
||||
std::make_tuple(R"(./c:/aa/bb)", "RW")
|
||||
);
|
||||
|
||||
void CreateTest_PathFormatterNormalize(char const* path, char const* pathFlags) {
|
||||
char normalized[0x200] = { 0 };
|
||||
nn::fs::PathFlags flags = GetPathFlags(pathFlags);
|
||||
|
||||
nn::Result result = nn::fs::PathFormatter::Normalize(normalized, 0x200, path, 0x200, flags);
|
||||
|
||||
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", @\"%s\", %s},\n",
|
||||
path, pathFlags, normalized, GetResultName(result));
|
||||
}
|
||||
|
||||
void CreateTest_PathFormatterIsNormalized(char const* path, char const* pathFlags) {
|
||||
bool isNormalized = 0;
|
||||
uint64_t normalizedLength = 0;
|
||||
nn::fs::PathFlags flags = GetPathFlags(pathFlags);
|
||||
|
||||
nn::Result result = nn::fs::PathFormatter::IsNormalized(&isNormalized, &normalizedLength, path, flags);
|
||||
|
||||
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", %s, %ld, %s},\n",
|
||||
path, pathFlags, BoolStr(isNormalized), normalizedLength, GetResultName(result));
|
||||
}
|
||||
|
||||
static constexpr const auto TestData_PathFormatterNormalize_SmallBuffer = make_array(
|
||||
//std::make_tuple(R"(aa/bb)", "MR", 2), // Crashes nnsdk and throws an out-of-range exception in LibHac. I guess that counts as a pass?
|
||||
std::make_tuple(R"(/aa/bb)", "M", 1),
|
||||
std::make_tuple(R"(mount:/aa/bb)", "MR", 6),
|
||||
std::make_tuple(R"(mount:/aa/bb)", "MR", 7),
|
||||
std::make_tuple(R"(aa/bb)", "MR", 3),
|
||||
std::make_tuple(R"(\\host\share)", "W", 13)
|
||||
);
|
||||
|
||||
void CreateTest_PathFormatterNormalize_SmallBuffer(char const* path, char const* pathFlags, int bufferSize) {
|
||||
char normalized[0x200] = { 0 };
|
||||
nn::fs::PathFlags flags = GetPathFlags(pathFlags);
|
||||
|
||||
svcOutputDebugString(path, strnlen(path, 0x200));
|
||||
|
||||
nn::Result result = nn::fs::PathFormatter::Normalize(normalized, bufferSize, path, 0x200, flags);
|
||||
|
||||
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", \"%s\", %d, @\"%s\", %s},\n",
|
||||
path, pathFlags, bufferSize, normalized, GetResultName(result));
|
||||
}
|
||||
|
||||
static constexpr const auto TestData_PathNormalizerNormalize = make_array(
|
||||
std::make_tuple("/aa/bb/c/", false, true),
|
||||
std::make_tuple("aa/bb/c/", false, false),
|
||||
std::make_tuple("aa/bb/c/", false, true),
|
||||
std::make_tuple("mount:a/b", false, true),
|
||||
std::make_tuple("/aa/bb/../..", true, false),
|
||||
std::make_tuple("/aa/bb/../../..", true, false),
|
||||
std::make_tuple("/aa/bb/../../..", false, false),
|
||||
std::make_tuple("aa/bb/../../..", true, true),
|
||||
std::make_tuple("aa/bb/../../..", false, true),
|
||||
std::make_tuple("", false, false),
|
||||
std::make_tuple("/", false, false),
|
||||
std::make_tuple("/.", false, false),
|
||||
std::make_tuple("/./", false, false),
|
||||
std::make_tuple("/..", false, false),
|
||||
std::make_tuple("//.", false, false),
|
||||
std::make_tuple("/ ..", false, false),
|
||||
std::make_tuple("/.. /", false, false),
|
||||
std::make_tuple("/. /.", false, false),
|
||||
std::make_tuple("/aa/bb/cc/dd/./.././../..", false, false),
|
||||
std::make_tuple("/aa/bb/cc/dd/./.././../../..", false, false),
|
||||
std::make_tuple("/./aa/./bb/./cc/./dd/.", false, false),
|
||||
std::make_tuple("/aa\\bb/cc", false, false),
|
||||
std::make_tuple("/aa\\bb/cc", false, false),
|
||||
std::make_tuple("/a|/bb/cc", false, false),
|
||||
std::make_tuple("/>a/bb/cc", false, false),
|
||||
std::make_tuple("/aa/.</cc", false, false),
|
||||
std::make_tuple("/aa/..</cc", false, false),
|
||||
std::make_tuple("\\\\aa/bb/cc", false, false),
|
||||
std::make_tuple("\\\\aa\\bb\\cc", false, false),
|
||||
std::make_tuple("/aa/bb/..\\cc", false, false),
|
||||
std::make_tuple("/aa/bb\\..\\cc", false, false),
|
||||
std::make_tuple("/aa/bb\\..", false, false),
|
||||
std::make_tuple("/aa\\bb/../cc", false, false)
|
||||
);
|
||||
|
||||
void CreateTest_PathNormalizerNormalize(char const* path, bool isWindowsPath, bool isRelativePath) {
|
||||
char normalized[0x200] = { 0 };
|
||||
uint64_t normalizedLength = 0;
|
||||
|
||||
nn::Result result = nn::fs::PathNormalizer::Normalize(normalized, &normalizedLength, path, 0x200, isWindowsPath, isRelativePath);
|
||||
|
||||
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %s, @\"%s\", %ld, %s},\n",
|
||||
path, BoolStr(isWindowsPath), BoolStr(isRelativePath), normalized, normalizedLength, GetResultName(result));
|
||||
}
|
||||
|
||||
void CreateTest_PathNormalizerIsNormalized(char const* path, bool isWindowsPath, bool isRelativePath) {
|
||||
bool isNormalized = false;
|
||||
uint64_t normalizedLength = 0;
|
||||
|
||||
nn::Result result = nn::fs::PathNormalizer::IsNormalized(&isNormalized, &normalizedLength, path);
|
||||
|
||||
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %s, %ld, %s},\n",
|
||||
path, BoolStr(isNormalized), normalizedLength, GetResultName(result));
|
||||
}
|
||||
|
||||
static constexpr const auto TestData_PathNormalizerNormalize_SmallBuffer = make_array(
|
||||
std::make_tuple("/aa/bb/cc/", 7),
|
||||
std::make_tuple("/aa/bb/cc/", 8),
|
||||
std::make_tuple("/aa/bb/cc/", 9),
|
||||
std::make_tuple("/aa/bb/cc/", 10),
|
||||
std::make_tuple("/aa/bb/cc", 9),
|
||||
std::make_tuple("/aa/bb/cc", 10),
|
||||
std::make_tuple("/./aa/./bb/./cc", 9),
|
||||
std::make_tuple("/./aa/./bb/./cc", 10),
|
||||
std::make_tuple("/aa/bb/cc/../../..", 9),
|
||||
std::make_tuple("/aa/bb/cc/../../..", 10),
|
||||
std::make_tuple("/aa/bb/.", 7),
|
||||
std::make_tuple("/aa/bb/./", 7),
|
||||
std::make_tuple("/aa/bb/..", 8),
|
||||
std::make_tuple("/aa/bb", 1),
|
||||
std::make_tuple("/aa/bb", 2),
|
||||
std::make_tuple("/aa/bb", 3),
|
||||
std::make_tuple("aa/bb", 1)
|
||||
);
|
||||
|
||||
void CreateTest_PathNormalizerNormalize_SmallBuffer(char const* path, int bufferSize) {
|
||||
char normalized[0x200] = { 0 };
|
||||
uint64_t normalizedLength = 0;
|
||||
|
||||
nn::Result result = nn::fs::PathNormalizer::Normalize(normalized, &normalizedLength, path, bufferSize, false, false);
|
||||
|
||||
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", %d, @\"%s\", %ld, %s},\n",
|
||||
path, bufferSize, normalized, normalizedLength, GetResultName(result));
|
||||
}
|
||||
|
||||
static constexpr const auto TestData_PathUtility_IsSubPath = make_array(
|
||||
std::make_tuple("//a/b", "/a"),
|
||||
std::make_tuple("/a", "//a/b"),
|
||||
std::make_tuple("//a/b", "\\\\a"),
|
||||
std::make_tuple("//a/b", "//a"),
|
||||
std::make_tuple("/", "/a"),
|
||||
std::make_tuple("/a", "/"),
|
||||
std::make_tuple("/", "/"),
|
||||
std::make_tuple("", ""),
|
||||
std::make_tuple("/", ""),
|
||||
std::make_tuple("/", "mount:/a"),
|
||||
std::make_tuple("mount:/", "mount:/"),
|
||||
std::make_tuple("mount:/a/b", "mount:/a/b"),
|
||||
std::make_tuple("mount:/a/b", "mount:/a/b/c"),
|
||||
std::make_tuple("/a/b", "/a/b/c"),
|
||||
std::make_tuple("/a/b/c", "/a/b"),
|
||||
std::make_tuple("/a/b", "/a/b"),
|
||||
std::make_tuple("/a/b", "/a/b\\c")
|
||||
);
|
||||
|
||||
void CreateTest_PathUtility_IsSubPath(const char* path1, const char* path2) {
|
||||
bool result = nn::fs::IsSubPath(path1, path2);
|
||||
|
||||
BufPos += sprintf(&Buf[BufPos], "{@\"%s\", @\"%s\", %s},\n",
|
||||
path1, path2, BoolStr(result));
|
||||
}
|
||||
|
||||
extern "C" void nnMain(void) {
|
||||
// nn::fs::detail::IsEnabledAccessLog(); // Adds the sdk version to the output
|
||||
|
||||
CreateTest("TestData_PathFormatter_Normalize_EmptyPath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_EmptyPath);
|
||||
CreateTest("TestData_PathFormatter_Normalize_MountName", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_MountName);
|
||||
CreateTest("TestData_PathFormatter_Normalize_WindowsPath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_WindowsPath);
|
||||
CreateTest("TestData_PathFormatter_Normalize_RelativePath", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_RelativePath);
|
||||
CreateTest("TestData_PathFormatter_Normalize_Backslash", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_Backslash);
|
||||
CreateTest("TestData_PathFormatter_Normalize_All", CreateTest_PathFormatterNormalize, TestData_PathFormatterNormalize_All);
|
||||
CreateTest("TestData_PathFormatter_Normalize_SmallBuffer", CreateTest_PathFormatterNormalize_SmallBuffer, TestData_PathFormatterNormalize_SmallBuffer);
|
||||
|
||||
CreateTest("TestData_PathFormatter_IsNormalized_EmptyPath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_EmptyPath);
|
||||
CreateTest("TestData_PathFormatter_IsNormalized_MountName", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_MountName);
|
||||
CreateTest("TestData_PathFormatter_IsNormalized_WindowsPath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_WindowsPath);
|
||||
CreateTest("TestData_PathFormatter_IsNormalized_RelativePath", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_RelativePath);
|
||||
CreateTest("TestData_PathFormatter_IsNormalized_Backslash", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_Backslash);
|
||||
CreateTest("TestData_PathFormatter_IsNormalized_All", CreateTest_PathFormatterIsNormalized, TestData_PathFormatterNormalize_All);
|
||||
|
||||
CreateTest("TestData_PathNormalizer_Normalize", CreateTest_PathNormalizerNormalize, TestData_PathNormalizerNormalize);
|
||||
CreateTest("TestData_PathNormalizer_Normalize_SmallBuffer", CreateTest_PathNormalizerNormalize_SmallBuffer, TestData_PathNormalizerNormalize_SmallBuffer);
|
||||
CreateTest("TestData_PathNormalizer_IsNormalized", CreateTest_PathNormalizerIsNormalized, TestData_PathNormalizerNormalize);
|
||||
|
||||
CreateTest("TestData_PathUtility_IsSubPath", CreateTest_PathUtility_IsSubPath, TestData_PathUtility_IsSubPath);
|
||||
}
|
148
tests/LibHac.Tests/Fs/PathNormalizerTests.cs
Normal file
148
tests/LibHac.Tests/Fs/PathNormalizerTests.cs
Normal file
|
@ -0,0 +1,148 @@
|
|||
// ReSharper disable InconsistentNaming
|
||||
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Common;
|
||||
using LibHac.Util;
|
||||
using Xunit;
|
||||
|
||||
namespace LibHac.Tests.Fs
|
||||
{
|
||||
public class PathNormalizerTests
|
||||
{
|
||||
public static TheoryData<string, bool, bool, string, long, Result> TestData_Normalize => new()
|
||||
{
|
||||
{ @"/aa/bb/c/", false, true, @"/aa/bb/c", 8, Result.Success },
|
||||
{ @"aa/bb/c/", false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"aa/bb/c/", false, true, @"/aa/bb/c", 8, Result.Success },
|
||||
{ @"mount:a/b", false, true, @"/mount:a/b", 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"/aa/bb/../..", true, false, @"/", 1, Result.Success },
|
||||
{ @"/aa/bb/../../..", true, false, @"/", 1, Result.Success },
|
||||
{ @"/aa/bb/../../..", false, false, @"/aa/bb/", 0, ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"aa/bb/../../..", true, true, @"/", 1, Result.Success },
|
||||
{ @"aa/bb/../../..", false, true, @"/aa/bb/", 0, ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"", false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"/", false, false, @"/", 1, Result.Success },
|
||||
{ @"/.", false, false, @"/", 1, Result.Success },
|
||||
{ @"/./", false, false, @"/", 1, Result.Success },
|
||||
{ @"/..", false, false, @"/", 0, ResultFs.DirectoryUnobtainable.Value },
|
||||
{ @"//.", false, false, @"/", 1, Result.Success },
|
||||
{ @"/ ..", false, false, @"/ ..", 4, Result.Success },
|
||||
{ @"/.. /", false, false, @"/.. ", 4, Result.Success },
|
||||
{ @"/. /.", false, false, @"/. ", 3, Result.Success },
|
||||
{ @"/aa/bb/cc/dd/./.././../..", false, false, @"/aa", 3, Result.Success },
|
||||
{ @"/aa/bb/cc/dd/./.././../../..", false, false, @"/", 1, Result.Success },
|
||||
{ @"/./aa/./bb/./cc/./dd/.", false, false, @"/aa/bb/cc/dd", 12, Result.Success },
|
||||
{ @"/aa\bb/cc", false, false, @"/aa\bb/cc", 9, Result.Success },
|
||||
{ @"/aa\bb/cc", false, false, @"/aa\bb/cc", 9, Result.Success },
|
||||
{ @"/a|/bb/cc", false, false, @"/a|/bb/cc", 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"/>a/bb/cc", false, false, @"/>a/bb/cc", 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"/aa/.</cc", false, false, @"/aa/.</cc", 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"/aa/..</cc", false, false, @"/aa/..</cc", 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"\\aa/bb/cc", false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\aa\bb\cc", false, false, @"", 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"/aa/bb/..\cc", false, false, @"/aa/cc", 6, Result.Success },
|
||||
{ @"/aa/bb\..\cc", false, false, @"/aa/cc", 6, Result.Success },
|
||||
{ @"/aa/bb\..", false, false, @"/aa", 3, Result.Success },
|
||||
{ @"/aa\bb/../cc", false, false, @"/cc", 3, Result.Success }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_Normalize))]
|
||||
public static void Normalize(string path, bool isWindowsPath, bool isDriveRelativePath, string expectedNormalized,
|
||||
long expectedLength, Result expectedResult)
|
||||
{
|
||||
byte[] buffer = new byte[0x301];
|
||||
|
||||
Result result = PathNormalizer12.Normalize(buffer, out int normalizedLength, path.ToU8Span(), isWindowsPath,
|
||||
isDriveRelativePath);
|
||||
|
||||
Assert.Equal(expectedResult, result);
|
||||
Assert.Equal(expectedNormalized, StringUtils.Utf8ZToString(buffer));
|
||||
Assert.Equal(expectedLength, normalizedLength);
|
||||
}
|
||||
|
||||
public static TheoryData<string, int, string, long, Result> TestData_Normalize_SmallBuffer => new()
|
||||
{
|
||||
{ @"/aa/bb/cc/", 7, @"/aa/bb", 6, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb/cc/", 8, @"/aa/bb/", 7, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb/cc/", 9, @"/aa/bb/c", 8, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb/cc/", 10, @"/aa/bb/cc", 9, Result.Success },
|
||||
{ @"/aa/bb/cc", 9, @"/aa/bb/c", 8, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb/cc", 10, @"/aa/bb/cc", 9, Result.Success },
|
||||
{ @"/./aa/./bb/./cc", 9, @"/aa/bb/c", 8, ResultFs.TooLongPath.Value },
|
||||
{ @"/./aa/./bb/./cc", 10, @"/aa/bb/cc", 9, Result.Success },
|
||||
{ @"/aa/bb/cc/../../..", 9, @"/aa/bb/c", 8, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb/cc/../../..", 10, @"/aa/bb/cc", 9, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb/.", 7, @"/aa/bb", 6, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb/./", 7, @"/aa/bb", 6, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb/..", 8, @"/aa", 3, Result.Success },
|
||||
{ @"/aa/bb", 1, @"", 0, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb", 2, @"/", 1, ResultFs.TooLongPath.Value },
|
||||
{ @"/aa/bb", 3, @"/a", 2, ResultFs.TooLongPath.Value },
|
||||
{ @"aa/bb", 1, @"", 0, ResultFs.InvalidPathFormat.Value }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_Normalize_SmallBuffer))]
|
||||
public static void Normalize_SmallBuffer(string path, int bufferLength, string expectedNormalized, long expectedLength, Result expectedResult)
|
||||
{
|
||||
byte[] buffer = new byte[bufferLength];
|
||||
|
||||
Result result = PathNormalizer12.Normalize(buffer, out int normalizedLength, path.ToU8Span(), false, false);
|
||||
|
||||
Assert.Equal(expectedResult, result);
|
||||
Assert.Equal(expectedNormalized, StringUtils.Utf8ZToString(buffer));
|
||||
Assert.Equal(expectedLength, normalizedLength);
|
||||
}
|
||||
|
||||
public static TheoryData<string, bool, long, Result> TestData_IsNormalized => new()
|
||||
{
|
||||
{ @"/aa/bb/c/", false, 9, Result.Success },
|
||||
{ @"aa/bb/c/", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"aa/bb/c/", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"mount:a/b", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"/aa/bb/../..", false, 0, Result.Success },
|
||||
{ @"/aa/bb/../../..", false, 0, Result.Success },
|
||||
{ @"/aa/bb/../../..", false, 0, Result.Success },
|
||||
{ @"aa/bb/../../..", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"aa/bb/../../..", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"/", true, 1, Result.Success },
|
||||
{ @"/.", false, 2, Result.Success },
|
||||
{ @"/./", false, 0, Result.Success },
|
||||
{ @"/..", false, 3, Result.Success },
|
||||
{ @"//.", false, 0, Result.Success },
|
||||
{ @"/ ..", true, 4, Result.Success },
|
||||
{ @"/.. /", false, 5, Result.Success },
|
||||
{ @"/. /.", false, 5, Result.Success },
|
||||
{ @"/aa/bb/cc/dd/./.././../..", false, 0, Result.Success },
|
||||
{ @"/aa/bb/cc/dd/./.././../../..", false, 0, Result.Success },
|
||||
{ @"/./aa/./bb/./cc/./dd/.", false, 0, Result.Success },
|
||||
{ @"/aa\bb/cc", true, 9, Result.Success },
|
||||
{ @"/aa\bb/cc", true, 9, Result.Success },
|
||||
{ @"/a|/bb/cc", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"/>a/bb/cc", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"/aa/.</cc", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"/aa/..</cc", false, 0, ResultFs.InvalidCharacter.Value },
|
||||
{ @"\\aa/bb/cc", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"\\aa\bb\cc", false, 0, ResultFs.InvalidPathFormat.Value },
|
||||
{ @"/aa/bb/..\cc", true, 12, Result.Success },
|
||||
{ @"/aa/bb\..\cc", true, 12, Result.Success },
|
||||
{ @"/aa/bb\..", true, 9, Result.Success },
|
||||
{ @"/aa\bb/../cc", false, 0, Result.Success }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_IsNormalized))]
|
||||
public static void IsNormalized(string path, bool expectedIsNormalized, long expectedLength, Result expectedResult)
|
||||
{
|
||||
Result result = PathNormalizer12.IsNormalized(out bool isNormalized, out int length, path.ToU8Span());
|
||||
|
||||
Assert.Equal(expectedResult, result);
|
||||
Assert.Equal(expectedLength, length);
|
||||
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
Assert.Equal(expectedIsNormalized, isNormalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
tests/LibHac.Tests/Fs/PathUtilityTests.cs
Normal file
38
tests/LibHac.Tests/Fs/PathUtilityTests.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using LibHac.Common;
|
||||
using LibHac.Fs.Common;
|
||||
using Xunit;
|
||||
|
||||
namespace LibHac.Tests.Fs
|
||||
{
|
||||
public class PathUtilityTests
|
||||
{
|
||||
public static TheoryData<string, string, bool> TestData_IsSubPath => new()
|
||||
{
|
||||
{ @"//a/b", @"/a", false },
|
||||
{ @"/a", @"//a/b", false },
|
||||
{ @"//a/b", @"\\a", false },
|
||||
{ @"//a/b", @"//a", true },
|
||||
{ @"/", @"/a", true },
|
||||
{ @"/a", @"/", true },
|
||||
{ @"/", @"/", false },
|
||||
{ @"", @"", false },
|
||||
{ @"/", @"", true },
|
||||
{ @"/", @"mount:/a", false },
|
||||
{ @"mount:/", @"mount:/", false },
|
||||
{ @"mount:/a/b", @"mount:/a/b", false },
|
||||
{ @"mount:/a/b", @"mount:/a/b/c", true },
|
||||
{ @"/a/b", @"/a/b/c", true },
|
||||
{ @"/a/b/c", @"/a/b", true },
|
||||
{ @"/a/b", @"/a/b", false },
|
||||
{ @"/a/b", @"/a/b\c", false }
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(TestData_IsSubPath))]
|
||||
public static void IsSubPath(string path1, string path2, bool expectedResult)
|
||||
{
|
||||
bool result = PathUtility12.IsSubPath(path1.ToU8Span(), path2.ToU8Span());
|
||||
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue