diff --git a/src/LibHac/IO/CachedStorage.cs b/src/LibHac/IO/CachedStorage.cs index 44d54f13..37a38908 100644 --- a/src/LibHac/IO/CachedStorage.cs +++ b/src/LibHac/IO/CachedStorage.cs @@ -136,7 +136,7 @@ namespace LibHac.IO length = (int)Math.Min(Length - offset, length); } - BaseStorage.Read(block.Buffer, offset, length, 0); + BaseStorage.Read(block.Buffer.AsSpan(0, length), offset); block.Length = length; block.Index = index; block.Dirty = false; diff --git a/src/LibHac/IO/IFileSystem.cs b/src/LibHac/IO/IFileSystem.cs index 9386f443..5b40db81 100644 --- a/src/LibHac/IO/IFileSystem.cs +++ b/src/LibHac/IO/IFileSystem.cs @@ -1,4 +1,6 @@ -namespace LibHac.IO +using System; + +namespace LibHac.IO { public interface IFileSystem { @@ -7,11 +9,19 @@ void CreateFile(string path, long size); void DeleteDirectory(string path); void DeleteFile(string path); - IDirectory OpenDirectory(string path); + IDirectory OpenDirectory(string path, OpenDirectoryMode mode); IFile OpenFile(string path); void RenameDirectory(string srcPath, string dstPath); void RenameFile(string srcPath, string dstPath); bool DirectoryExists(string path); bool FileExists(string path); } + + [Flags] + public enum OpenDirectoryMode + { + Directories = 1, + Files = 2, + All = Directories | Files + } } \ No newline at end of file diff --git a/src/LibHac/IO/IFileSystemExtensions.cs b/src/LibHac/IO/IFileSystemExtensions.cs index 7b7b213c..f0970404 100644 --- a/src/LibHac/IO/IFileSystemExtensions.cs +++ b/src/LibHac/IO/IFileSystemExtensions.cs @@ -8,7 +8,7 @@ namespace LibHac.IO { public static void Extract(this IFileSystem fs, string outDir) { - var root = fs.OpenDirectory("/"); + var root = fs.OpenDirectory("/", OpenDirectoryMode.All); foreach (var filename in root.EnumerateFiles()) { @@ -33,7 +33,7 @@ namespace LibHac.IO { if (entry.Type == DirectoryEntryType.Directory) { - foreach(string a in EnumerateFiles(directory.ParentFileSystem.OpenDirectory(entry.Name))) + foreach (string a in EnumerateFiles(directory.ParentFileSystem.OpenDirectory(entry.Name, OpenDirectoryMode.All))) { yield return a; } diff --git a/src/LibHac/IO/PathTools.cs b/src/LibHac/IO/PathTools.cs new file mode 100644 index 00000000..59934dcf --- /dev/null +++ b/src/LibHac/IO/PathTools.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace LibHac.IO +{ + public static class PathTools + { + public static readonly char DirectorySeparator = '/'; + + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + // See the LICENSE file in the project root for more information. + public static string Normalize(string inPath) + { + ReadOnlySpan path = inPath.AsSpan(); + + if (path.Length == 0) return DirectorySeparator.ToString(); + + if (path[0] != DirectorySeparator) + { + throw new InvalidDataException($"{nameof(path)} must begin with '{DirectorySeparator}'"); + } + + Span initialBuffer = stackalloc char[0x200]; + var sb = new ValueStringBuilder(initialBuffer); + + for (int i = 0; i < path.Length; i++) + { + char c = path[i]; + + if (IsDirectorySeparator(c) && i + 1 < path.Length) + { + // Skip this character if it's a directory separator and if the next character is, too, + // e.g. "parent//child" => "parent/child" + if (IsDirectorySeparator(path[i + 1])) continue; + + // Skip this character and the next if it's referring to the current directory, + // e.g. "parent/./child" => "parent/child" + if (IsCurrentDirectory(path, i)) + { + i++; + continue; + } + + // Skip this character and the next two if it's referring to the parent directory, + // e.g. "parent/child/../grandchild" => "parent/grandchild" + if (IsParentDirectory(path, i)) + { + // Unwind back to the last slash (and if there isn't one, clear out everything). + for (int s = sb.Length - 1; s >= 0; s--) + { + if (IsDirectorySeparator(sb[s])) + { + sb.Length = s; + break; + } + } + + i += 2; + continue; + } + } + sb.Append(c); + } + + // If we haven't changed the source path, return the original + if (sb.Length == inPath.Length) + { + return inPath; + } + + if (sb.Length == 0) + { + sb.Append(DirectorySeparator); + } + + return sb.ToString(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirectorySeparator(char c) + { + return c == DirectorySeparator; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsCurrentDirectory(ReadOnlySpan path, int index) + { + return (index + 2 == path.Length || IsDirectorySeparator(path[index + 2])) && + path[index + 1] == '.'; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsParentDirectory(ReadOnlySpan path, int index) + { + return index + 2 < path.Length && + (index + 3 == path.Length || IsDirectorySeparator(path[index + 3])) && + path[index + 1] == '.' && path[index + 2] == '.'; + } + } +} diff --git a/src/LibHac/IO/RomFsDirectory.cs b/src/LibHac/IO/RomFsDirectory.cs index af3241eb..6c25c864 100644 --- a/src/LibHac/IO/RomFsDirectory.cs +++ b/src/LibHac/IO/RomFsDirectory.cs @@ -7,8 +7,9 @@ namespace LibHac.IO public IFileSystem ParentFileSystem { get; } private RomfsDir Directory { get; } + private OpenDirectoryMode Mode { get; } - public RomFsDirectory(RomFsFileSystem fs, string path) + public RomFsDirectory(RomFsFileSystem fs, string path, OpenDirectoryMode mode) { if (!fs.DirectoryDict.TryGetValue(path, out RomfsDir dir)) { @@ -17,6 +18,7 @@ namespace LibHac.IO ParentFileSystem = fs; Directory = dir; + Mode = mode; } public DirectoryEntry[] Read() @@ -26,22 +28,29 @@ namespace LibHac.IO var entries = new DirectoryEntry[count]; int index = 0; - var dirEntry = Directory.FirstChild; - - while (dirEntry != null) + if (Mode.HasFlag(OpenDirectoryMode.Directories)) { - entries[index] = new DirectoryEntry(dirEntry.FullPath, DirectoryEntryType.Directory, 0); - dirEntry = dirEntry.NextSibling; - index++; + RomfsDir dirEntry = Directory.FirstChild; + + while (dirEntry != null) + { + entries[index] = new DirectoryEntry(dirEntry.FullPath, DirectoryEntryType.Directory, 0); + dirEntry = dirEntry.NextSibling; + index++; + } } - RomfsFile fileEntry = Directory.FirstFile; - - while (fileEntry != null) + if (Mode.HasFlag(OpenDirectoryMode.Files)) { - entries[index] = new DirectoryEntry(fileEntry.FullPath, DirectoryEntryType.File, fileEntry.DataLength); - fileEntry = fileEntry.NextSibling; - index++; + RomfsFile fileEntry = Directory.FirstFile; + + while (fileEntry != null) + { + entries[index] = + new DirectoryEntry(fileEntry.FullPath, DirectoryEntryType.File, fileEntry.DataLength); + fileEntry = fileEntry.NextSibling; + index++; + } } return entries; @@ -50,20 +59,27 @@ namespace LibHac.IO public int GetEntryCount() { int count = 0; - RomfsDir dirEntry = Directory.FirstChild; - while (dirEntry != null) + if (Mode.HasFlag(OpenDirectoryMode.Directories)) { - count++; - dirEntry = dirEntry.NextSibling; + RomfsDir dirEntry = Directory.FirstChild; + + while (dirEntry != null) + { + count++; + dirEntry = dirEntry.NextSibling; + } } - RomfsFile fileEntry = Directory.FirstFile; - - while (fileEntry != null) + if (Mode.HasFlag(OpenDirectoryMode.Files)) { - count++; - fileEntry = fileEntry.NextSibling; + RomfsFile fileEntry = Directory.FirstFile; + + while (fileEntry != null) + { + count++; + fileEntry = fileEntry.NextSibling; + } } return count; diff --git a/src/LibHac/IO/RomFsFile.cs b/src/LibHac/IO/RomFsFile.cs index defff2c3..f67b506b 100644 --- a/src/LibHac/IO/RomFsFile.cs +++ b/src/LibHac/IO/RomFsFile.cs @@ -39,7 +39,7 @@ namespace LibHac.IO public long SetSize() { - throw new NotImplementedException(); + throw new NotSupportedException(); } } } diff --git a/src/LibHac/IO/RomFsFileSystem.cs b/src/LibHac/IO/RomFsFileSystem.cs index f9506fda..ff5ef8b8 100644 --- a/src/LibHac/IO/RomFsFileSystem.cs +++ b/src/LibHac/IO/RomFsFileSystem.cs @@ -86,32 +86,32 @@ namespace LibHac.IO public void Commit() { - throw new NotImplementedException(); + throw new NotSupportedException(); } public void CreateDirectory(string path) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public void CreateFile(string path, long size) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public void DeleteDirectory(string path) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public void DeleteFile(string path) { - throw new NotImplementedException(); + throw new NotSupportedException(); } - public IDirectory OpenDirectory(string path) + public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) { - return new RomFsDirectory(this, path); + return new RomFsDirectory(this, path, mode); } public IFile OpenFile(string path) @@ -131,17 +131,17 @@ namespace LibHac.IO public void RenameDirectory(string srcPath, string dstPath) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public void RenameFile(string srcPath, string dstPath) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public bool DirectoryExists(string path) { - throw new NotImplementedException(); + return DirectoryDict.ContainsKey(path); } public bool FileExists(string path) diff --git a/src/LibHac/IO/ValueStringBuilder.cs b/src/LibHac/IO/ValueStringBuilder.cs new file mode 100644 index 00000000..6dbd00cb --- /dev/null +++ b/src/LibHac/IO/ValueStringBuilder.cs @@ -0,0 +1,271 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibHac.IO +{ + internal ref struct ValueStringBuilder + { + private char[] _arrayToReturnToPool; + private Span _chars; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _chars = _arrayToReturnToPool; + _pos = 0; + } + + public int Length + { + get => _pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; + } + } + + public int Capacity => _chars.Length; + + public void EnsureCapacity(int capacity) + { + if (capacity > _chars.Length) + Grow(capacity - _chars.Length); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + public ref char GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after + public ref char GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(_chars); + } + + public ref char this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _chars[index]; + } + } + + public override string ToString() + { + var s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawChars => _chars; + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + public bool TryCopyTo(Span destination, out int charsWritten) + { + if (_chars.Slice(0, _pos).TryCopyTo(destination)) + { + charsWritten = _pos; + Dispose(); + return true; + } + else + { + charsWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, char value, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + _chars.Slice(index, count).Fill(value); + _pos += count; + } + + public void Insert(int index, string s) + { + int count = s.Length; + + if (_pos > (_chars.Length - count)) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + s.AsSpan().CopyTo(_chars.Slice(index)); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + int pos = _pos; + if ((uint)pos < (uint)_chars.Length) + { + _chars[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string s) + { + int pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + _chars[pos] = s[0]; + _pos = pos + 1; + } + else + { + AppendSlow(s); + } + } + + private void AppendSlow(string s) + { + int pos = _pos; + if (pos > _chars.Length - s.Length) + { + Grow(s.Length); + } + + s.AsSpan().CopyTo(_chars.Slice(pos)); + _pos += s.Length; + } + + public void Append(char c, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + Span dst = _chars.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + public void Append(ReadOnlySpan value) + { + int pos = _pos; + if (pos > _chars.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = _pos; + if (origPos > _chars.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int requiredAdditionalCapacity) + { + Debug.Assert(requiredAdditionalCapacity > 0); + + char[] poolArray = ArrayPool.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); + + _chars.CopyTo(poolArray); + + char[] toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + char[] toReturn = _arrayToReturnToPool; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + } +} diff --git a/tests/LibHac.Tests/PathToolsTests.cs b/tests/LibHac.Tests/PathToolsTests.cs new file mode 100644 index 00000000..f8764ff2 --- /dev/null +++ b/tests/LibHac.Tests/PathToolsTests.cs @@ -0,0 +1,45 @@ +using System.IO; +using LibHac.IO; +using Xunit; + +namespace LibHac.Tests +{ + public class PathToolsTests + { + public static object[][] NormalizedPathTestItems = + { + new object[] {"", "/"}, + new object[] {"/", "/"}, + new object[] {"/.", "/"}, + new object[] {"/a/b/c", "/a/b/c"}, + new object[] {"/a/b/../c", "/a/c"}, + new object[] {"/a/b/c/..", "/a/b"}, + new object[] {"/a/b/c/.", "/a/b/c"}, + new object[] {"/a/../../..", "/"}, + new object[] {"/a/../../../a/b/c", "/a/b/c"}, + new object[] {"//a/b//.//c", "/a/b/c"}, + + + new object[] {"/a/b/c/", "/a/b/c/"}, + new object[] {"/a/./b/../c/", "/a/c/"}, + new object[] {"/a/../../../", "/"}, + new object[] {"//a/b//.//c/", "/a/b/c/"}, + new object[] {@"/tmp/../", @"/"}, + }; + + [Theory] + [MemberData(nameof(NormalizedPathTestItems))] + public static void NormalizePath(string path, string expected) + { + string actual = PathTools.Normalize(path); + + Assert.Equal(expected, actual); + } + + [Fact] + public static void NormalizeThrowsOnInvalidStartChar() + { + Assert.Throws(() => PathTools.Normalize(@"c:\a\b\c")); + } + } +}