diff --git a/src/LibHac/Common/U8Span.cs b/src/LibHac/Common/U8Span.cs
index 9e4cb0ee..bd336f58 100644
--- a/src/LibHac/Common/U8Span.cs
+++ b/src/LibHac/Common/U8Span.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Text;
namespace LibHac.Common
@@ -76,6 +77,18 @@ namespace LibHac.Common
return new U8String(_buffer.ToArray());
}
- public bool IsEmpty() => _buffer.IsEmpty;
+ ///
+ /// Checks if the has no buffer.
+ ///
+ /// if the span has no buffer.
+ /// Otherwise, .
+ public bool IsNull() => _buffer.IsEmpty;
+
+ ///
+ /// Checks if the has no buffer or begins with a null terminator.
+ ///
+ /// if the span has no buffer or begins with a null terminator.
+ /// Otherwise, .
+ public bool IsEmpty() => _buffer.IsEmpty || MemoryMarshal.GetReference(_buffer) == 0;
}
}
diff --git a/src/LibHac/Fs/MountHelpers.cs b/src/LibHac/Fs/MountHelpers.cs
index e2ad4312..33ecb09e 100644
--- a/src/LibHac/Fs/MountHelpers.cs
+++ b/src/LibHac/Fs/MountHelpers.cs
@@ -6,7 +6,7 @@ namespace LibHac.Fs
{
public static Result CheckMountName(U8Span name)
{
- if (name.IsEmpty()) return ResultFs.NullArgument.Log();
+ if (name.IsNull()) return ResultFs.NullArgument.Log();
if (name.Length > 0 && name[0] == '@') return ResultFs.InvalidMountName.Log();
if (!CheckMountNameImpl(name)) return ResultFs.InvalidMountName.Log();
@@ -16,7 +16,7 @@ namespace LibHac.Fs
public static Result CheckMountNameAcceptingReservedMountName(U8Span name)
{
- if (name.IsEmpty()) return ResultFs.NullArgument.Log();
+ if (name.IsNull()) return ResultFs.NullArgument.Log();
if (!CheckMountNameImpl(name)) return ResultFs.InvalidMountName.Log();
diff --git a/src/LibHac/Fs/PathTool.cs b/src/LibHac/Fs/PathTool.cs
index 91d9a201..b1610de4 100644
--- a/src/LibHac/Fs/PathTool.cs
+++ b/src/LibHac/Fs/PathTool.cs
@@ -360,7 +360,7 @@ namespace LibHac.Fs
}
}
- if (path2.IsEmpty() || IsNullTerminator(path2[0]))
+ if (path2.IsEmpty())
return ResultFs.InvalidPathFormat.Log();
if (ContainsParentDirectoryAlt(path2))
diff --git a/src/LibHac/FsService/PathNormalizer.cs b/src/LibHac/FsService/PathNormalizer.cs
index 08f29259..cf2822a6 100644
--- a/src/LibHac/FsService/PathNormalizer.cs
+++ b/src/LibHac/FsService/PathNormalizer.cs
@@ -1,26 +1,30 @@
using System;
using LibHac.Common;
+using LibHac.Fs;
+using LibHac.FsSystem;
namespace LibHac.FsService
{
public ref struct PathNormalizer
{
- private U8Span _path;
- private Result _result;
+ private readonly U8Span _path;
+ public U8Span Path => _path;
+
+ public Result Result { get; }
public PathNormalizer(U8Span path, Option option)
{
if (option.HasFlag(Option.AcceptEmpty) && path.IsEmpty())
{
_path = path;
- _result = Result.Success;
+ Result = Result.Success;
}
else
{
bool preserveUnc = option.HasFlag(Option.PreserveUnc);
bool preserveTailSeparator = option.HasFlag(Option.PreserveTailSeparator);
bool hasMountName = option.HasFlag(Option.HasMountName);
- _result = Normalize(out _path, path, preserveUnc, preserveTailSeparator, hasMountName);
+ Result = Normalize(out _path, path, preserveUnc, preserveTailSeparator, hasMountName);
}
}
@@ -28,8 +32,33 @@ namespace LibHac.FsService
bool preserveTailSeparator, bool hasMountName)
{
normalizedPath = default;
- throw new NotImplementedException();
+ Result rc = PathTool.IsNormalized(out bool isNormalized, path, preserveUnc, hasMountName);
+ if (rc.IsFailure()) return rc;
+
+ if (isNormalized)
+ {
+ normalizedPath = path;
+ }
+ else
+ {
+ var buffer = new byte[PathTools.MaxPathLength + 1];
+
+ rc = PathTool.Normalize(buffer, out long normalizedLength, path, preserveUnc, hasMountName);
+ if (rc.IsFailure()) return rc;
+
+ // GetLength is capped at MaxPathLength bytes to leave room for the null terminator
+ if (preserveTailSeparator &&
+ PathTool.IsSeparator(path[StringUtils.GetLength(path, PathTools.MaxPathLength) - 1]))
+ {
+ buffer[(int)normalizedLength] = StringTraits.DirectorySeparator;
+ buffer[(int)normalizedLength + 1] = StringTraits.NullTerminator;
+ }
+
+ normalizedPath = new U8Span(buffer);
+ }
+
+ return Result.Success;
}
[Flags]
diff --git a/tests/LibHac.Tests/Fs/PathToolTestGenerator.cpp b/tests/LibHac.Tests/Fs/PathToolTestGenerator.cpp
index ffa8ffa9..cb81a948 100644
--- a/tests/LibHac.Tests/Fs/PathToolTestGenerator.cpp
+++ b/tests/LibHac.Tests/Fs/PathToolTestGenerator.cpp
@@ -148,6 +148,7 @@ void CreateNormalizationTestData(void (*func)(char const*, bool, bool)) {
func("mount:/a/b/../c", preserveUnc, true);
func("a:/a/b/c", preserveUnc, true);
func("mount:/a/b/../c", preserveUnc, true);
+ func("mount:/a/b/../c", preserveUnc, false);
func("mount:\\a/b/../c", preserveUnc, true);
func("mount:\\a/b\\../c", preserveUnc, true);
func("mount:\\a/b/c", preserveUnc, true);
diff --git a/tests/LibHac.Tests/Fs/PathToolTests.cs b/tests/LibHac.Tests/Fs/PathToolTests.cs
index 470d8602..24818b11 100644
--- a/tests/LibHac.Tests/Fs/PathToolTests.cs
+++ b/tests/LibHac.Tests/Fs/PathToolTests.cs
@@ -75,6 +75,7 @@ namespace LibHac.Tests.Fs
new object[] {@"./a/b/c/.", false, false, @"", 0, ResultFs.InvalidPathFormat.Value},
new object[] {@"abc", false, false, @"", 0, ResultFs.InvalidPathFormat.Value},
new object[] {@"mount:/a/b/../c", false, true, @"mount:/a/c", 10, Result.Success},
+ new object[] {@"mount:/a/b/../c", false, false, @"", 0, ResultFs.InvalidPathFormat.Value},
new object[] {@"a:/a/b/c", false, true, @"a:/a/b/c", 8, Result.Success},
new object[] {@"mount:/a/b/../c", false, true, @"mount:/a/c", 10, Result.Success},
new object[] {@"mount:\a/b/../c", false, true, @"mount:\a/c", 10, Result.Success},
@@ -464,6 +465,7 @@ namespace LibHac.Tests.Fs
new object[] {@"./a/b/c/.", false, false, false, ResultFs.InvalidPathFormat.Value},
new object[] {@"abc", false, false, false, ResultFs.InvalidPathFormat.Value},
new object[] {@"mount:/a/b/../c", false, true, false, Result.Success},
+ new object[] {@"mount:/a/b/../c", false, false, false, ResultFs.InvalidPathFormat.Value},
new object[] {@"a:/a/b/c", false, true, true, Result.Success},
new object[] {@"mount:/a/b/../c", false, true, false, Result.Success},
new object[] {@"mount:\a/b/../c", false, true, false, ResultFs.InvalidPathFormat.Value},
diff --git a/tests/LibHac.Tests/FsService/PathNormalizerTests.cs b/tests/LibHac.Tests/FsService/PathNormalizerTests.cs
new file mode 100644
index 00000000..6d222bd7
--- /dev/null
+++ b/tests/LibHac.Tests/FsService/PathNormalizerTests.cs
@@ -0,0 +1,84 @@
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.FsService;
+using Xunit;
+
+namespace LibHac.Tests.FsService
+{
+ public class PathNormalizerTests
+ {
+ [Fact]
+ public static void Ctor_EmptyPathWithAcceptEmptyOption_ReturnsEmptyPathWithSuccess()
+ {
+ var normalizer = new PathNormalizer("".ToU8Span(), PathNormalizer.Option.AcceptEmpty);
+
+ Assert.Equal(Result.Success, normalizer.Result);
+ Assert.True(normalizer.Path.IsEmpty());
+ }
+
+ [Fact]
+ public static void Normalize_PreserveTailSeparatorOption_KeepsExistingTailSeparator()
+ {
+ var normalizer = new PathNormalizer("/a/./b/".ToU8Span(), PathNormalizer.Option.PreserveTailSeparator);
+
+ Assert.Equal(Result.Success, normalizer.Result);
+ Assert.Equal("/a/b/", normalizer.Path.ToString());
+ }
+
+ [Fact]
+ public static void Normalize_PreserveTailSeparatorOption_IgnoresMissingTailSeparator()
+ {
+ var normalizer = new PathNormalizer("/a/./b".ToU8Span(), PathNormalizer.Option.PreserveTailSeparator);
+
+ Assert.Equal(Result.Success, normalizer.Result);
+ Assert.Equal("/a/b", normalizer.Path.ToString());
+ }
+
+ [Fact]
+ public static void Normalize_PathAlreadyNormalized_ReturnsSameBuffer()
+ {
+ var originalPath = "/a/b".ToU8Span();
+ var normalizer = new PathNormalizer(originalPath, PathNormalizer.Option.PreserveTailSeparator);
+
+ Assert.Equal(Result.Success, normalizer.Result);
+
+ // Compares addresses and lengths of the buffers
+ Assert.True(originalPath.Value == normalizer.Path.Value);
+ }
+
+ [Fact]
+ public static void Normalize_PreserveUncOptionOn_PreservesUncPath()
+ {
+ var normalizer = new PathNormalizer("//aa/bb/..".ToU8Span(), PathNormalizer.Option.PreserveUnc);
+
+ Assert.Equal(Result.Success, normalizer.Result);
+ Assert.Equal(@"\\aa/bb", normalizer.Path.ToString());
+ }
+
+ [Fact]
+ public static void Normalize_PreserveUncOptionOff_DoesNotPreserveUncPath()
+ {
+ var normalizer = new PathNormalizer("//aa/bb/..".ToU8Span(), PathNormalizer.Option.None);
+
+ Assert.Equal(Result.Success, normalizer.Result);
+ Assert.Equal(@"/aa", normalizer.Path.ToString());
+ }
+
+ [Fact]
+ public static void Normalize_MountNameOptionOn_ParsesMountName()
+ {
+ var normalizer = new PathNormalizer("mount:/a/./b".ToU8Span(), PathNormalizer.Option.HasMountName);
+
+ Assert.Equal(Result.Success, normalizer.Result);
+ Assert.Equal("mount:/a/b", normalizer.Path.ToString());
+ }
+
+ [Fact]
+ public static void Normalize_MountNameOptionOff_DoesNotParseMountName()
+ {
+ var normalizer = new PathNormalizer("mount:/a/./b".ToU8Span(), PathNormalizer.Option.None);
+
+ Assert.Equal(ResultFs.InvalidPathFormat.Value, normalizer.Result);
+ }
+ }
+}