From 71c4e6746b321349bf6b1517ccff6b35f79b56e5 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 23 Dec 2018 14:49:28 -0700 Subject: [PATCH] Begin implementing IFileSystem --- build/Build.cs | 10 +- src/LibHac.Nand/NandPartition.cs | 2 +- src/LibHac/FileSystem.cs | 2 +- .../{IFileSystem.cs => IFileSystemOld.cs} | 2 +- src/LibHac/IO/CachedStorage.cs | 2 +- src/LibHac/IO/DirectoryEntry.cs | 22 ++ src/LibHac/IO/FileBase.cs | 23 ++ src/LibHac/IO/FileSystemExtensions.cs | 68 +++++ src/LibHac/IO/HierarchicalRomFileTable.cs | 7 + src/LibHac/IO/IDirectory.cs | 10 + src/LibHac/IO/IFile.cs | 13 + src/LibHac/IO/IFileSystem.cs | 27 ++ src/LibHac/IO/PathTools.cs | 102 +++++++ src/LibHac/IO/RomFsDirectory.cs | 88 ++++++ src/LibHac/IO/RomFsFile.cs | 48 ++++ src/LibHac/IO/RomFsFileSystem.cs | 153 ++++++++++ src/LibHac/IO/Storage.cs | 4 +- src/LibHac/IO/ValueStringBuilder.cs | 271 ++++++++++++++++++ src/LibHac/Nacp.cs | 2 +- src/LibHac/Nca.cs | 9 + src/LibHac/RomfsEntry.cs | 3 + src/LibHac/SwitchFs.cs | 6 +- src/hactoolnet/ProcessNsp.cs | 2 +- tests/LibHac.Tests/PathToolsTests.cs | 45 +++ 24 files changed, 905 insertions(+), 16 deletions(-) rename src/LibHac/{IFileSystem.cs => IFileSystemOld.cs} (92%) create mode 100644 src/LibHac/IO/DirectoryEntry.cs create mode 100644 src/LibHac/IO/FileBase.cs create mode 100644 src/LibHac/IO/FileSystemExtensions.cs create mode 100644 src/LibHac/IO/HierarchicalRomFileTable.cs create mode 100644 src/LibHac/IO/IDirectory.cs create mode 100644 src/LibHac/IO/IFile.cs create mode 100644 src/LibHac/IO/IFileSystem.cs create mode 100644 src/LibHac/IO/PathTools.cs create mode 100644 src/LibHac/IO/RomFsDirectory.cs create mode 100644 src/LibHac/IO/RomFsFile.cs create mode 100644 src/LibHac/IO/RomFsFileSystem.cs create mode 100644 src/LibHac/IO/ValueStringBuilder.cs create mode 100644 tests/LibHac.Tests/PathToolsTests.cs diff --git a/build/Build.cs b/build/Build.cs index 44a57f41..1cacbe25 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -53,7 +53,7 @@ class Build : NukeBuild .DependsOn(Clean) .Executes(() => { - var settings = new DotNetRestoreSettings() + DotNetRestoreSettings settings = new DotNetRestoreSettings() .SetProjectFile(Solution); if (EnvironmentInfo.IsUnix) settings = settings.RemoveRuntimes("net46"); @@ -65,7 +65,7 @@ class Build : NukeBuild .DependsOn(Restore) .Executes(() => { - var buildSettings = new DotNetBuildSettings() + DotNetBuildSettings buildSettings = new DotNetBuildSettings() .SetProjectFile(Solution) .EnableNoRestore() .SetConfiguration(Configuration); @@ -74,7 +74,7 @@ class Build : NukeBuild DotNetBuild(s => buildSettings); - var publishSettings = new DotNetPublishSettings() + DotNetPublishSettings publishSettings = new DotNetPublishSettings() .EnableNoRestore() .SetConfiguration(Configuration); @@ -105,7 +105,7 @@ class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - var settings = new DotNetPackSettings() + DotNetPackSettings settings = new DotNetPackSettings() .SetProject(LibHacProject) .EnableNoBuild() .SetConfiguration(Configuration) @@ -154,7 +154,7 @@ class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - var settings = new DotNetTestSettings() + DotNetTestSettings settings = new DotNetTestSettings() .SetProjectFile(LibHacTestProject) .EnableNoBuild() .SetConfiguration(Configuration); diff --git a/src/LibHac.Nand/NandPartition.cs b/src/LibHac.Nand/NandPartition.cs index 16406371..22fff573 100644 --- a/src/LibHac.Nand/NandPartition.cs +++ b/src/LibHac.Nand/NandPartition.cs @@ -4,7 +4,7 @@ using DiscUtils.Fat; namespace LibHac.Nand { - public class NandPartition : IFileSystem + public class NandPartition : IFileSystemOld { public FatFileSystem Fs { get; } diff --git a/src/LibHac/FileSystem.cs b/src/LibHac/FileSystem.cs index 6c1f9755..d2286e93 100644 --- a/src/LibHac/FileSystem.cs +++ b/src/LibHac/FileSystem.cs @@ -4,7 +4,7 @@ using System.IO; namespace LibHac { - public class FileSystem : IFileSystem + public class FileSystem : IFileSystemOld { public string Root { get; } diff --git a/src/LibHac/IFileSystem.cs b/src/LibHac/IFileSystemOld.cs similarity index 92% rename from src/LibHac/IFileSystem.cs rename to src/LibHac/IFileSystemOld.cs index 27d7aeab..84b650d4 100644 --- a/src/LibHac/IFileSystem.cs +++ b/src/LibHac/IFileSystemOld.cs @@ -2,7 +2,7 @@ namespace LibHac { - public interface IFileSystem + public interface IFileSystemOld { bool FileExists(string path); bool DirectoryExists(string path); 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/DirectoryEntry.cs b/src/LibHac/IO/DirectoryEntry.cs new file mode 100644 index 00000000..591e9118 --- /dev/null +++ b/src/LibHac/IO/DirectoryEntry.cs @@ -0,0 +1,22 @@ +namespace LibHac.IO +{ + public class DirectoryEntry + { + public string Name { get; } + public DirectoryEntryType Type { get; } + public long Size { get; } + + public DirectoryEntry(string name, DirectoryEntryType type, long size) + { + Name = name; + Type = type; + Size = size; + } + } + + public enum DirectoryEntryType + { + Directory, + File + } +} diff --git a/src/LibHac/IO/FileBase.cs b/src/LibHac/IO/FileBase.cs new file mode 100644 index 00000000..a2872aa3 --- /dev/null +++ b/src/LibHac/IO/FileBase.cs @@ -0,0 +1,23 @@ +using System; + +namespace LibHac.IO +{ + public abstract class FileBase : IFile + { + public abstract int Read(Span destination, long offset); + public abstract void Write(ReadOnlySpan source, long offset); + public abstract void Flush(); + public abstract long GetSize(); + public abstract long SetSize(); + + protected int GetAvailableSizeAndValidate(ReadOnlySpan span, long offset) + { + long fileLength = GetSize(); + + if (span == null) throw new ArgumentNullException(nameof(span)); + if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset), "Argument must be non-negative."); + + return (int)Math.Min(fileLength - offset, span.Length); + } + } +} diff --git a/src/LibHac/IO/FileSystemExtensions.cs b/src/LibHac/IO/FileSystemExtensions.cs new file mode 100644 index 00000000..143dfba9 --- /dev/null +++ b/src/LibHac/IO/FileSystemExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace LibHac.IO +{ + public static class FileSystemExtensions + { + public static void Extract(this IFileSystem fs, string outDir) + { + IDirectory root = fs.OpenDirectory("/", OpenDirectoryMode.All); + + foreach (string filename in root.EnumerateFiles()) + { + //Console.WriteLine(filename); + IFile file = fs.OpenFile(filename); + string outPath = Path.Combine(outDir, filename.TrimStart('/')); + + string directoryName = Path.GetDirectoryName(outPath); + if(!string.IsNullOrWhiteSpace(directoryName)) Directory.CreateDirectory(directoryName); + + using (var outFile = new FileStream(outPath, FileMode.Create, FileAccess.ReadWrite)) + { + file.CopyTo(outFile); + } + } + } + + public static IEnumerable EnumerateFiles(this IDirectory directory) + { + DirectoryEntry[] entries = directory.Read(); + + foreach (DirectoryEntry entry in entries) + { + if (entry.Type == DirectoryEntryType.Directory) + { + foreach (string a in EnumerateFiles(directory.ParentFileSystem.OpenDirectory(entry.Name, OpenDirectoryMode.All))) + { + yield return a; + } + } + + if (entry.Type == DirectoryEntryType.File) + { + yield return entry.Name; + } + } + } + + public static void CopyTo(this IFile file, Stream output) + { + const int bufferSize = 0x8000; + long remaining = file.GetSize(); + long inOffset = 0; + var buffer = new byte[bufferSize]; + + while (remaining > 0) + { + int toWrite = (int)Math.Min(buffer.Length, remaining); + file.Read(buffer.AsSpan(0, toWrite), inOffset); + + output.Write(buffer, 0, toWrite); + remaining -= toWrite; + inOffset += toWrite; + } + } + } +} diff --git a/src/LibHac/IO/HierarchicalRomFileTable.cs b/src/LibHac/IO/HierarchicalRomFileTable.cs new file mode 100644 index 00000000..ca6b77f2 --- /dev/null +++ b/src/LibHac/IO/HierarchicalRomFileTable.cs @@ -0,0 +1,7 @@ +namespace LibHac.IO +{ + public class HierarchicalRomFileTable + { + + } +} diff --git a/src/LibHac/IO/IDirectory.cs b/src/LibHac/IO/IDirectory.cs new file mode 100644 index 00000000..a26793d6 --- /dev/null +++ b/src/LibHac/IO/IDirectory.cs @@ -0,0 +1,10 @@ +namespace LibHac.IO +{ + public interface IDirectory + { + IFileSystem ParentFileSystem { get; } + + DirectoryEntry[] Read(); + int GetEntryCount(); + } +} \ No newline at end of file diff --git a/src/LibHac/IO/IFile.cs b/src/LibHac/IO/IFile.cs new file mode 100644 index 00000000..00f26f66 --- /dev/null +++ b/src/LibHac/IO/IFile.cs @@ -0,0 +1,13 @@ +using System; + +namespace LibHac.IO +{ + public interface IFile + { + int Read(Span destination, long offset); + void Write(ReadOnlySpan source, long offset); + void Flush(); + long GetSize(); + long SetSize(); + } +} \ No newline at end of file diff --git a/src/LibHac/IO/IFileSystem.cs b/src/LibHac/IO/IFileSystem.cs new file mode 100644 index 00000000..5b40db81 --- /dev/null +++ b/src/LibHac/IO/IFileSystem.cs @@ -0,0 +1,27 @@ +using System; + +namespace LibHac.IO +{ + public interface IFileSystem + { + void Commit(); + void CreateDirectory(string path); + void CreateFile(string path, long size); + void DeleteDirectory(string path); + void DeleteFile(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/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 new file mode 100644 index 00000000..6c25c864 --- /dev/null +++ b/src/LibHac/IO/RomFsDirectory.cs @@ -0,0 +1,88 @@ +using System.IO; + +namespace LibHac.IO +{ + public class RomFsDirectory : IDirectory + { + public IFileSystem ParentFileSystem { get; } + + private RomfsDir Directory { get; } + private OpenDirectoryMode Mode { get; } + + public RomFsDirectory(RomFsFileSystem fs, string path, OpenDirectoryMode mode) + { + if (!fs.DirectoryDict.TryGetValue(path, out RomfsDir dir)) + { + throw new DirectoryNotFoundException(path); + } + + ParentFileSystem = fs; + Directory = dir; + Mode = mode; + } + + public DirectoryEntry[] Read() + { + int count = GetEntryCount(); + + var entries = new DirectoryEntry[count]; + int index = 0; + + if (Mode.HasFlag(OpenDirectoryMode.Directories)) + { + RomfsDir dirEntry = Directory.FirstChild; + + while (dirEntry != null) + { + entries[index] = new DirectoryEntry(dirEntry.FullPath, DirectoryEntryType.Directory, 0); + dirEntry = dirEntry.NextSibling; + index++; + } + } + + if (Mode.HasFlag(OpenDirectoryMode.Files)) + { + RomfsFile fileEntry = Directory.FirstFile; + + while (fileEntry != null) + { + entries[index] = + new DirectoryEntry(fileEntry.FullPath, DirectoryEntryType.File, fileEntry.DataLength); + fileEntry = fileEntry.NextSibling; + index++; + } + } + + return entries; + } + + public int GetEntryCount() + { + int count = 0; + + if (Mode.HasFlag(OpenDirectoryMode.Directories)) + { + RomfsDir dirEntry = Directory.FirstChild; + + while (dirEntry != null) + { + count++; + dirEntry = dirEntry.NextSibling; + } + } + + if (Mode.HasFlag(OpenDirectoryMode.Files)) + { + 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 new file mode 100644 index 00000000..9b41d4f3 --- /dev/null +++ b/src/LibHac/IO/RomFsFile.cs @@ -0,0 +1,48 @@ +using System; + +namespace LibHac.IO +{ + public class RomFsFile : FileBase + { + private IStorage BaseStorage { get; } + private long Offset { get; } + private long Size { get; } + + public RomFsFile(IStorage baseStorage, long offset, long size) + { + BaseStorage = baseStorage; + Offset = offset; + Size = size; + } + + public override int Read(Span destination, long offset) + { + long storageOffset = Offset + offset; + int toRead = GetAvailableSizeAndValidate(destination, offset); + + BaseStorage.Read(destination.Slice(0, toRead), storageOffset); + + return toRead; + } + + public override void Write(ReadOnlySpan source, long offset) + { + throw new NotImplementedException(); + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override long GetSize() + { + return Size; + } + + public override long SetSize() + { + throw new NotSupportedException(); + } + } +} diff --git a/src/LibHac/IO/RomFsFileSystem.cs b/src/LibHac/IO/RomFsFileSystem.cs new file mode 100644 index 00000000..078b5f6c --- /dev/null +++ b/src/LibHac/IO/RomFsFileSystem.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LibHac.IO +{ + public class RomFsFileSystem : IFileSystem + { + public RomfsHeader Header { get; } + public List Directories { get; } = new List(); + public List Files { get; } = new List(); + public RomfsDir RootDir { get; } + + public Dictionary FileDict { get; } + public Dictionary DirectoryDict { get; } + private IStorage BaseStorage { get; } + + // todo Don't parse entire table when opening + public RomFsFileSystem(IStorage storage) + { + BaseStorage = storage; + + byte[] dirMetaTable; + byte[] fileMetaTable; + using (var reader = new BinaryReader(BaseStorage.AsStream(), Encoding.Default, true)) + { + Header = new RomfsHeader(reader); + reader.BaseStream.Position = Header.DirMetaTableOffset; + dirMetaTable = reader.ReadBytes((int)Header.DirMetaTableSize); + reader.BaseStream.Position = Header.FileMetaTableOffset; + fileMetaTable = reader.ReadBytes((int)Header.FileMetaTableSize); + } + + using (var reader = new BinaryReader(new MemoryStream(dirMetaTable))) + { + int position = 0; + while (position + 20 < Header.DirMetaTableSize) + { + var dir = new RomfsDir(reader) { Offset = position }; + Directories.Add(dir); + if (dir.ParentDirOffset == position) RootDir = dir; + position = (int)reader.BaseStream.Position; + } + } + + using (var reader = new BinaryReader(new MemoryStream(fileMetaTable))) + { + int position = 0; + while (position + 20 < Header.FileMetaTableSize) + { + var file = new RomfsFile(reader) { Offset = position }; + Files.Add(file); + position = (int)reader.BaseStream.Position; + } + } + + SetReferences(); + RomfsEntry.ResolveFilenames(Files); + RomfsEntry.ResolveFilenames(Directories); + FileDict = Files.ToDictionary(x => x.FullPath, x => x); + DirectoryDict = Directories.ToDictionary(x => x.FullPath, x => x); + } + + private void SetReferences() + { + Dictionary dirDict = Directories.ToDictionary(x => x.Offset, x => x); + Dictionary fileDict = Files.ToDictionary(x => x.Offset, x => x); + + foreach (RomfsDir dir in Directories) + { + if (dir.ParentDirOffset >= 0 && dir.ParentDirOffset != dir.Offset) dir.ParentDir = dirDict[dir.ParentDirOffset]; + if (dir.NextSiblingOffset >= 0) dir.NextSibling = dirDict[dir.NextSiblingOffset]; + if (dir.FirstChildOffset >= 0) dir.FirstChild = dirDict[dir.FirstChildOffset]; + if (dir.FirstFileOffset >= 0) dir.FirstFile = fileDict[dir.FirstFileOffset]; + if (dir.NextDirHashOffset >= 0) dir.NextDirHash = dirDict[dir.NextDirHashOffset]; + } + + foreach (RomfsFile file in Files) + { + if (file.ParentDirOffset >= 0) file.ParentDir = dirDict[file.ParentDirOffset]; + if (file.NextSiblingOffset >= 0) file.NextSibling = fileDict[file.NextSiblingOffset]; + if (file.NextFileHashOffset >= 0) file.NextFileHash = fileDict[file.NextFileHashOffset]; + } + } + + public void Commit() + { + throw new NotSupportedException(); + } + + public void CreateDirectory(string path) + { + throw new NotSupportedException(); + } + + public void CreateFile(string path, long size) + { + throw new NotSupportedException(); + } + + public void DeleteDirectory(string path) + { + throw new NotSupportedException(); + } + + public void DeleteFile(string path) + { + throw new NotSupportedException(); + } + + public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) + { + return new RomFsDirectory(this, path, mode); + } + + public IFile OpenFile(string path) + { + if (!FileDict.TryGetValue(path, out RomfsFile file)) + { + throw new FileNotFoundException(); + } + + return OpenFile(file); + } + + public IFile OpenFile(RomfsFile file) + { + return new RomFsFile(BaseStorage, Header.DataOffset + file.DataOffset, file.DataLength); + } + + public void RenameDirectory(string srcPath, string dstPath) + { + throw new NotSupportedException(); + } + + public void RenameFile(string srcPath, string dstPath) + { + throw new NotSupportedException(); + } + + public bool DirectoryExists(string path) + { + return DirectoryDict.ContainsKey(path); + } + + public bool FileExists(string path) + { + return FileDict.ContainsKey(path); + } + } +} diff --git a/src/LibHac/IO/Storage.cs b/src/LibHac/IO/Storage.cs index 6a8cf33b..0a7cfc7e 100644 --- a/src/LibHac/IO/Storage.cs +++ b/src/LibHac/IO/Storage.cs @@ -95,7 +95,7 @@ namespace LibHac.IO if (Length != -1) { - if (offset + count > Length) throw new ArgumentException(); + if (offset + count > Length) throw new ArgumentException("The given offset and count exceed the length of the Storage"); } } @@ -107,7 +107,7 @@ namespace LibHac.IO if (Length != -1) { - if (offset + destination.Length > Length) throw new ArgumentException("Storage"); + if (offset + destination.Length > Length) throw new ArgumentException("The given offset and count exceed the length of the Storage"); } } } diff --git a/src/LibHac/IO/ValueStringBuilder.cs b/src/LibHac/IO/ValueStringBuilder.cs new file mode 100644 index 00000000..68adedd6 --- /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() + { + string 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/src/LibHac/Nacp.cs b/src/LibHac/Nacp.cs index ade77bd7..6c6f1f93 100644 --- a/src/LibHac/Nacp.cs +++ b/src/LibHac/Nacp.cs @@ -64,7 +64,7 @@ namespace LibHac { long start = file.Position; - BinaryReader reader = new BinaryReader(file); + var reader = new BinaryReader(file); for (int i = 0; i < 16; i++) { diff --git a/src/LibHac/Nca.cs b/src/LibHac/Nca.cs index 1121ee51..556c73bf 100644 --- a/src/LibHac/Nca.cs +++ b/src/LibHac/Nca.cs @@ -255,6 +255,15 @@ namespace LibHac return new HierarchicalIntegrityVerificationStorage(initInfo, integrityCheckLevel, leaveOpen); } + public IFileSystem OpenSectionFileSystem(int index) + { + IStorage storage = OpenSection(index, false, IntegrityCheckLevel.ErrorOnInvalid, true); + + var fs = new RomFsFileSystem(storage); + + return fs; + } + /// /// Sets a base to use when reading patches. /// diff --git a/src/LibHac/RomfsEntry.cs b/src/LibHac/RomfsEntry.cs index 1fa90653..1fef1bb9 100644 --- a/src/LibHac/RomfsEntry.cs +++ b/src/LibHac/RomfsEntry.cs @@ -31,6 +31,9 @@ namespace LibHac dir = dir.ParentDir; } + //todo + if (list.Count == 1) list.Add("/"); + for (int i = list.Count - 1; i >= 0; i--) { sb.Append(list[i]); diff --git a/src/LibHac/SwitchFs.cs b/src/LibHac/SwitchFs.cs index a97788c0..93d29957 100644 --- a/src/LibHac/SwitchFs.cs +++ b/src/LibHac/SwitchFs.cs @@ -12,7 +12,7 @@ namespace LibHac public class SwitchFs : IDisposable { public Keyset Keyset { get; } - public IFileSystem Fs { get; } + public IFileSystemOld Fs { get; } public string ContentsDir { get; } public string SaveDir { get; } @@ -21,7 +21,7 @@ namespace LibHac public Dictionary Titles { get; } = new Dictionary(); public Dictionary Applications { get; } = new Dictionary(); - public SwitchFs(Keyset keyset, IFileSystem fs) + public SwitchFs(Keyset keyset, IFileSystemOld fs) { Fs = fs; Keyset = keyset; @@ -232,7 +232,7 @@ namespace LibHac } } - internal static IStorage OpenSplitNcaStorage(IFileSystem fs, string path) + internal static IStorage OpenSplitNcaStorage(IFileSystemOld fs, string path) { var files = new List(); var storages = new List(); diff --git a/src/hactoolnet/ProcessNsp.cs b/src/hactoolnet/ProcessNsp.cs index 212980c4..d441d4c0 100644 --- a/src/hactoolnet/ProcessNsp.cs +++ b/src/hactoolnet/ProcessNsp.cs @@ -13,7 +13,7 @@ namespace hactoolnet { using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read)) { - Pfs pfs = new Pfs(file.AsStorage()); + var pfs = new Pfs(file.AsStorage()); ctx.Logger.LogMessage(pfs.Print()); if (ctx.Options.OutDir != null) 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")); + } + } +}