From 1c28c08c9464c4f273f294f4bffd2d0fea984053 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 10 Feb 2020 01:43:11 -0700 Subject: [PATCH] Add PathNormalizer --- src/LibHac/Common/U8Span.cs | 15 +++- src/LibHac/Fs/MountHelpers.cs | 4 +- src/LibHac/Fs/PathTool.cs | 2 +- src/LibHac/FsService/PathNormalizer.cs | 39 +++++++-- .../LibHac.Tests/Fs/PathToolTestGenerator.cpp | 1 + tests/LibHac.Tests/Fs/PathToolTests.cs | 2 + .../FsService/PathNormalizerTests.cs | 84 +++++++++++++++++++ 7 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 tests/LibHac.Tests/FsService/PathNormalizerTests.cs 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); + } + } +}