From a55b1d7c58dfc4ab9d3d813cb64bd384a4c8e1e8 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Tue, 3 Jan 2023 11:00:58 -0700 Subject: [PATCH] Implement PartitionFileSystem classes --- src/LibHac/Crypto/Sha256Generator.cs | 2 + src/LibHac/Fs/Fsa/IFile.cs | 11 +- .../FsCreator/PartitionFileSystemCreator.cs | 6 +- .../FsSrv/FsCreator/StorageOnNcaCreator.cs | 13 +- .../Impl/PartitionFileSystemFormats.cs | 89 ++- src/LibHac/FsSystem/PartitionDirectory.cs | 72 -- src/LibHac/FsSystem/PartitionFile.cs | 89 --- src/LibHac/FsSystem/PartitionFileSystem.cs | 647 ++++++++++++++---- .../FsSystem/PartitionFileSystemCore.cs | 389 ----------- .../FsSystem/PartitionFileSystemMeta.cs | 366 ++++++++++ .../FsSystem/PartitionFileSystemMetaCore.cs | 194 ------ src/LibHac/Mem/Buffer.cs | 54 ++ src/LibHac/Tools/Fs/Xci.cs | 10 +- src/LibHac/Tools/FsSystem/NcaUtils/Nca.cs | 4 +- .../FsSystem/PartitionFileSystemBuilder.cs | 24 +- src/hactoolnet/ProcessPfs.cs | 93 ++- src/hactoolnet/ProcessXci.cs | 122 ++-- .../LibHac.Tests/FsSystem/TypeLayoutTests.cs | 42 ++ 18 files changed, 1221 insertions(+), 1006 deletions(-) delete mode 100644 src/LibHac/FsSystem/PartitionDirectory.cs delete mode 100644 src/LibHac/FsSystem/PartitionFile.cs delete mode 100644 src/LibHac/FsSystem/PartitionFileSystemCore.cs create mode 100644 src/LibHac/FsSystem/PartitionFileSystemMeta.cs delete mode 100644 src/LibHac/FsSystem/PartitionFileSystemMetaCore.cs diff --git a/src/LibHac/Crypto/Sha256Generator.cs b/src/LibHac/Crypto/Sha256Generator.cs index 644a2fb9..2ea326ad 100644 --- a/src/LibHac/Crypto/Sha256Generator.cs +++ b/src/LibHac/Crypto/Sha256Generator.cs @@ -5,6 +5,8 @@ namespace LibHac.Crypto; public class Sha256Generator : IHash { + public const int HashSize = Sha256.DigestSize; + private Sha256Impl _baseHash; public Sha256Generator() diff --git a/src/LibHac/Fs/Fsa/IFile.cs b/src/LibHac/Fs/Fsa/IFile.cs index e3ad83d7..915455bc 100644 --- a/src/LibHac/Fs/Fsa/IFile.cs +++ b/src/LibHac/Fs/Fsa/IFile.cs @@ -157,10 +157,9 @@ public abstract class IFile : IDisposable return DoOperateRange(Span.Empty, operationId, offset, size, ReadOnlySpan.Empty); } - protected Result DryRead(out long readableBytes, long offset, long size, in ReadOption option, - OpenMode openMode) + protected Result DryRead(out long outReadSize, long offset, long size, in ReadOption option, OpenMode openMode) { - UnsafeHelpers.SkipParamInit(out readableBytes); + UnsafeHelpers.SkipParamInit(out outReadSize); // Check that we can read. if (!openMode.HasFlag(OpenMode.Read)) @@ -173,12 +172,12 @@ public abstract class IFile : IDisposable if (offset > fileSize) return ResultFs.OutOfRange.Log(); - readableBytes = Math.Min(fileSize - offset, size); + long readableSize = fileSize - offset; + outReadSize = Math.Min(readableSize, size); return Result.Success; } - protected Result DryWrite(out bool needsAppend, long offset, long size, in WriteOption option, - OpenMode openMode) + protected Result DryWrite(out bool needsAppend, long offset, long size, in WriteOption option, OpenMode openMode) { UnsafeHelpers.SkipParamInit(out needsAppend); diff --git a/src/LibHac/FsSrv/FsCreator/PartitionFileSystemCreator.cs b/src/LibHac/FsSrv/FsCreator/PartitionFileSystemCreator.cs index f27ae0da..07354dcf 100644 --- a/src/LibHac/FsSrv/FsCreator/PartitionFileSystemCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/PartitionFileSystemCreator.cs @@ -2,7 +2,6 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; -using LibHac.FsSystem.Impl; namespace LibHac.FsSrv.FsCreator; @@ -10,10 +9,9 @@ public class PartitionFileSystemCreator : IPartitionFileSystemCreator { public Result Create(ref SharedRef outFileSystem, ref SharedRef baseStorage) { - using var partitionFs = - new SharedRef>(new PartitionFileSystemCore()); + using var partitionFs = new SharedRef(new PartitionFileSystem()); - Result res = partitionFs.Get.Initialize(ref baseStorage); + Result res = partitionFs.Get.Initialize(in baseStorage); if (res.IsFailure()) return res.Miss(); outFileSystem.SetByMove(ref partitionFs.Ref); diff --git a/src/LibHac/FsSrv/FsCreator/StorageOnNcaCreator.cs b/src/LibHac/FsSrv/FsCreator/StorageOnNcaCreator.cs index dcf9a54f..90c374a3 100644 --- a/src/LibHac/FsSrv/FsCreator/StorageOnNcaCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/StorageOnNcaCreator.cs @@ -4,7 +4,6 @@ using LibHac.Common.Keys; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; -using LibHac.FsSystem.Impl; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using NcaFsHeader = LibHac.Tools.FsSystem.NcaUtils.NcaFsHeader; @@ -33,14 +32,12 @@ public class StorageOnNcaCreator : IStorageOnNcaCreator if (isCodeFs) { - using (var codeFs = new PartitionFileSystemCore()) - { - res = codeFs.Initialize(storageTemp); - if (res.IsFailure()) return res.Miss(); + using var codeFs = new PartitionFileSystem(); + res = codeFs.Initialize(storageTemp); + if (res.IsFailure()) return res.Miss(); - res = VerifyAcidSignature(codeFs, nca); - if (res.IsFailure()) return res.Miss(); - } + res = VerifyAcidSignature(codeFs, nca); + if (res.IsFailure()) return res.Miss(); } outStorage.Reset(storageTemp); diff --git a/src/LibHac/FsSystem/Impl/PartitionFileSystemFormats.cs b/src/LibHac/FsSystem/Impl/PartitionFileSystemFormats.cs index f9c308af..3ba816e2 100644 --- a/src/LibHac/FsSystem/Impl/PartitionFileSystemFormats.cs +++ b/src/LibHac/FsSystem/Impl/PartitionFileSystemFormats.cs @@ -1,38 +1,71 @@ -using System.Runtime.InteropServices; -using LibHac.Common; +using System; +using System.Runtime.InteropServices; +using LibHac.Common.FixedArrays; +using LibHac.Fs; namespace LibHac.FsSystem.Impl; -public interface IPartitionFileSystemEntry +public struct PartitionFileSystemFormat : IPartitionFileSystemFormat { - long Offset { get; } - long Size { get; } - int NameOffset { get; } + public static ReadOnlySpan VersionSignature => "PFS0"u8; + public static uint EntryNameLengthMax => PathTool.EntryNameLengthMax; + public static uint FileDataAlignmentSize => 0x20; + public static Result ResultSignatureVerificationFailed => ResultFs.PartitionSignatureVerificationFailed.Value; + + [StructLayout(LayoutKind.Sequential)] + public struct PartitionEntry : IPartitionFileSystemEntry + { + public long Offset; + public long Size; + public int NameOffset; + public uint Reserved; + + readonly long IPartitionFileSystemEntry.Offset => Offset; + readonly long IPartitionFileSystemEntry.Size => Size; + readonly int IPartitionFileSystemEntry.NameOffset => NameOffset; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PartitionFileSystemHeaderImpl : IPartitionFileSystemHeader + { + private Array4 _signature; + public int EntryCount; + public int NameTableSize; + public uint Reserved; + + public readonly ReadOnlySpan Signature + { + get + { + ReadOnlySpan span = _signature.ItemsRo; + return MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(span), span.Length); + } + } + + readonly int IPartitionFileSystemHeader.EntryCount => EntryCount; + readonly int IPartitionFileSystemHeader.NameTableSize => NameTableSize; + } } -[StructLayout(LayoutKind.Sequential, Size = 0x18)] -public struct StandardEntry : IPartitionFileSystemEntry +public struct Sha256PartitionFileSystemFormat : IPartitionFileSystemFormat { - public long Offset; - public long Size; - public int NameOffset; + public static ReadOnlySpan VersionSignature => "HFS0"u8; + public static uint EntryNameLengthMax => PathTool.EntryNameLengthMax; + public static uint FileDataAlignmentSize => 0x200; + public static Result ResultSignatureVerificationFailed => ResultFs.Sha256PartitionSignatureVerificationFailed.Value; - long IPartitionFileSystemEntry.Offset => Offset; - long IPartitionFileSystemEntry.Size => Size; - int IPartitionFileSystemEntry.NameOffset => NameOffset; -} + [StructLayout(LayoutKind.Sequential)] + public struct PartitionEntry : IPartitionFileSystemEntry + { + public long Offset; + public long Size; + public int NameOffset; + public int HashTargetSize; + public long HashTargetOffset; + public Array32 Hash; -[StructLayout(LayoutKind.Sequential, Size = 0x40)] -public struct HashedEntry : IPartitionFileSystemEntry -{ - public long Offset; - public long Size; - public int NameOffset; - public int HashSize; - public long HashOffset; - public Buffer32 Hash; - - long IPartitionFileSystemEntry.Offset => Offset; - long IPartitionFileSystemEntry.Size => Size; - int IPartitionFileSystemEntry.NameOffset => NameOffset; + readonly long IPartitionFileSystemEntry.Offset => Offset; + readonly long IPartitionFileSystemEntry.Size => Size; + readonly int IPartitionFileSystemEntry.NameOffset => NameOffset; + } } \ No newline at end of file diff --git a/src/LibHac/FsSystem/PartitionDirectory.cs b/src/LibHac/FsSystem/PartitionDirectory.cs deleted file mode 100644 index 7ac91655..00000000 --- a/src/LibHac/FsSystem/PartitionDirectory.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.IO; -using System.Text; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.Tools.FsSystem; -using LibHac.Util; - -namespace LibHac.FsSystem; - -public class PartitionDirectory : IDirectory -{ - private PartitionFileSystem ParentFileSystem { get; } - private OpenDirectoryMode Mode { get; } - private int CurrentIndex { get; set; } - - public PartitionDirectory(PartitionFileSystem fs, string path, OpenDirectoryMode mode) - { - path = PathTools.Normalize(path); - - if (path != "/") throw new DirectoryNotFoundException(); - - ParentFileSystem = fs; - Mode = mode; - - CurrentIndex = 0; - } - - protected override Result DoRead(out long entriesRead, Span entryBuffer) - { - if (!Mode.HasFlag(OpenDirectoryMode.File)) - { - entriesRead = 0; - return Result.Success; - } - - int entriesRemaining = ParentFileSystem.Files.Length - CurrentIndex; - int toRead = Math.Min(entriesRemaining, entryBuffer.Length); - - for (int i = 0; i < toRead; i++) - { - PartitionFileEntry fileEntry = ParentFileSystem.Files[CurrentIndex]; - ref DirectoryEntry entry = ref entryBuffer[i]; - - Span nameUtf8 = Encoding.UTF8.GetBytes(fileEntry.Name); - - entry.Type = DirectoryEntryType.File; - entry.Size = fileEntry.Size; - - StringUtils.Copy(entry.Name.Items, nameUtf8); - entry.Name[PathTool.EntryNameLengthMax] = 0; - - CurrentIndex++; - } - - entriesRead = toRead; - return Result.Success; - } - - protected override Result DoGetEntryCount(out long entryCount) - { - int count = 0; - - if (Mode.HasFlag(OpenDirectoryMode.File)) - { - count += ParentFileSystem.Files.Length; - } - - entryCount = count; - return Result.Success; - } -} \ No newline at end of file diff --git a/src/LibHac/FsSystem/PartitionFile.cs b/src/LibHac/FsSystem/PartitionFile.cs deleted file mode 100644 index 8344bfd0..00000000 --- a/src/LibHac/FsSystem/PartitionFile.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using LibHac.Fs; -using LibHac.Fs.Fsa; - -namespace LibHac.FsSystem; - -public class PartitionFile : IFile -{ - private IStorage BaseStorage { get; } - private long Offset { get; } - private long Size { get; } - private OpenMode Mode { get; } - - public PartitionFile(IStorage baseStorage, long offset, long size, OpenMode mode) - { - Mode = mode; - BaseStorage = baseStorage; - Offset = offset; - Size = size; - } - - protected override Result DoRead(out long bytesRead, long offset, Span destination, - in ReadOption option) - { - bytesRead = 0; - - Result res = DryRead(out long toRead, offset, destination.Length, in option, Mode); - if (res.IsFailure()) return res.Miss(); - - long storageOffset = Offset + offset; - BaseStorage.Read(storageOffset, destination.Slice(0, (int)toRead)); - - bytesRead = toRead; - return Result.Success; - } - - protected override Result DoWrite(long offset, ReadOnlySpan source, in WriteOption option) - { - Result res = DryWrite(out bool isResizeNeeded, offset, source.Length, in option, Mode); - if (res.IsFailure()) return res.Miss(); - - if (isResizeNeeded) return ResultFs.UnsupportedWriteForPartitionFile.Log(); - - if (offset > Size) return ResultFs.OutOfRange.Log(); - - res = BaseStorage.Write(offset, source); - if (res.IsFailure()) return res.Miss(); - - // N doesn't flush if the flag is set - if (option.HasFlushFlag()) - { - return BaseStorage.Flush(); - } - - return Result.Success; - } - - protected override Result DoFlush() - { - if (!Mode.HasFlag(OpenMode.Write)) - { - return BaseStorage.Flush(); - } - - return Result.Success; - } - - protected override Result DoGetSize(out long size) - { - size = Size; - return Result.Success; - } - - protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, - ReadOnlySpan inBuffer) - { - return ResultFs.NotImplemented.Log(); - } - - protected override Result DoSetSize(long size) - { - if (!Mode.HasFlag(OpenMode.Write)) - { - return ResultFs.WriteUnpermitted.Log(); - } - - return ResultFs.UnsupportedWriteForPartitionFile.Log(); - } -} \ No newline at end of file diff --git a/src/LibHac/FsSystem/PartitionFileSystem.cs b/src/LibHac/FsSystem/PartitionFileSystem.cs index 8bc647fc..f42de22a 100644 --- a/src/LibHac/FsSystem/PartitionFileSystem.cs +++ b/src/LibHac/FsSystem/PartitionFileSystem.cs @@ -1,76 +1,507 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; +using System.Buffers; +using System.Runtime.CompilerServices; using LibHac.Common; using LibHac.Crypto; +using LibHac.Diag; using LibHac.Fs; using LibHac.Fs.Fsa; -using LibHac.Tools.FsSystem; -using Path = LibHac.Fs.Path; +using LibHac.Util; +using Buffer = LibHac.Mem.Buffer; namespace LibHac.FsSystem; -public class PartitionFileSystem : IFileSystem +/// +/// The allocator used by a when none is provided. +/// +/// The original allocator in FS simply calls nn::fs::detail::Allocate and +/// nn::fs::detail::Deallocate. In our implementation we use the shared .NET . +/// Based on nnSdk 15.3.0 (FS 15.0.0) +file sealed class DefaultAllocatorForPartitionFileSystem : MemoryResource { - // todo Re-add way of checking a file hash - public PartitionFileSystemHeader Header { get; } - public int HeaderSize { get; } - public PartitionFileEntry[] Files { get; } + public static readonly DefaultAllocatorForPartitionFileSystem Instance = new(); - private Dictionary FileDict { get; } - private IStorage BaseStorage { get; } - - public PartitionFileSystem(IStorage storage) + protected override Buffer DoAllocate(long size, int alignment) { - using (var reader = new BinaryReader(storage.AsStream(), Encoding.Default, true)) - { - Header = new PartitionFileSystemHeader(reader); - } + byte[] array = ArrayPool.Shared.Rent((int)size); - HeaderSize = Header.HeaderSize; - Files = Header.Files; - FileDict = Header.Files.ToDictionary(x => x.Name, x => x); - BaseStorage = storage; + return new Buffer(array.AsMemory(0, (int)size), array); } - protected override Result DoOpenDirectory(ref UniqueRef outDirectory, in Path path, - OpenDirectoryMode mode) + protected override void DoDeallocate(Buffer buffer, int alignment) { - outDirectory.Reset(new PartitionDirectory(this, path.ToString(), mode)); + if (buffer.Extra is byte[] array) + { + ArrayPool.Shared.Return(array); + } + else + { + throw new LibHacException("Buffer was not allocated by this MemoryResource."); + } + } + + protected override bool DoIsEqual(MemoryResource other) + { + return ReferenceEquals(this, other); + } +} + +/// +/// Reads a standard partition file system. These files start with "PFS0" and are typically found inside NCAs +/// or as .nsp files. +/// +/// Based on nnSdk 15.3.0 (FS 15.0.0) +public class PartitionFileSystem : PartitionFileSystemCore { } + +/// +/// Reads a hashed partition file system. These files start with "HFS0" and are typically found inside XCIs. +/// +/// Based on nnSdk 15.3.0 (FS 15.0.0) +public class Sha256PartitionFileSystem : PartitionFileSystemCore { } + +/// +/// Provides the base for an that can read from different partition file system files. +/// A partition file system is a simple, flat file archive that can't contain any directories. The archive has +/// two main sections: the metadata located at the start of the file, and the actual file data located directly after. +/// +/// The type of the class used to read this file system's metadata. +/// A traits class that provides values used to read and build the metadata. +/// The type of the header at the beginning of the metadata. +/// The type of the entries in the file table in the metadata. +/// Based on nnSdk 15.3.0 (FS 15.0.0) +public class PartitionFileSystemCore : IFileSystem + where TMetaData : PartitionFileSystemMetaCore, new() + where TFormat : IPartitionFileSystemFormat + where THeader : unmanaged, IPartitionFileSystemHeader + where TEntry : unmanaged, IPartitionFileSystemEntry +{ + private static ReadOnlySpan RootPath => "/"u8; + + private IStorage _baseStorage; + private TMetaData _metaData; + private bool _isInitialized; + private long _metaDataSize; + private UniqueRef _uniqueMetaData; + private SharedRef _sharedStorage; + + /// + /// Provides access to a file from a . + /// + /// Based on nnSdk 15.3.0 (FS 15.0.0) + private class PartitionFile : IFile + { + private TEntry _partitionEntry; + private readonly PartitionFileSystemCore _parent; + private readonly OpenMode _mode; + + public PartitionFile(PartitionFileSystemCore parent, in TEntry partitionEntry, OpenMode mode) + { + _partitionEntry = partitionEntry; + _parent = parent; + _mode = mode; + } + + protected override Result DoWrite(long offset, ReadOnlySpan source, in WriteOption option) + { + Result res = DryWrite(out bool needsAppend, offset, source.Length, in option, _mode); + if (res.IsFailure()) return res.Miss(); + + if (needsAppend) + return ResultFs.UnsupportedWriteForPartitionFile.Log(); + + Assert.SdkRequires(!_mode.HasFlag(OpenMode.AllowAppend)); + + if (offset > _partitionEntry.Size) + return ResultFs.OutOfRange.Log(); + + if (offset + source.Length > _partitionEntry.Size) + return ResultFs.InvalidSize.Log(); + + return _parent._baseStorage.Write(_parent._metaDataSize + _partitionEntry.Offset + offset, source).Ret(); + } + + protected override Result DoFlush() + { + if (!_mode.HasFlag(OpenMode.Write)) + return Result.Success; + + return _parent._baseStorage.Flush().Ret(); + } + + protected override Result DoSetSize(long size) + { + Result res = DrySetSize(size, _mode); + if (res.IsFailure()) return res.Miss(); + + return ResultFs.UnsupportedWriteForPartitionFile.Log(); + } + + protected override Result DoGetSize(out long size) + { + size = _partitionEntry.Size; + return Result.Success; + } + + protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer) + { + long operateOffset; + long operateSize; + + switch (operationId) + { + case OperationId.InvalidateCache: + if (!_mode.HasFlag(OpenMode.Read)) + return ResultFs.ReadUnpermitted.Log(); + + if (_mode.HasFlag(OpenMode.Write)) + return ResultFs.UnsupportedOperateRangeForPartitionFile.Log(); + + operateOffset = 0; + operateSize = long.MaxValue; + break; + + case OperationId.QueryRange: + if (offset < 0 || offset > _partitionEntry.Size) + return ResultFs.OutOfRange.Log(); + + if (offset + size > _partitionEntry.Size || offset + size < offset) + return ResultFs.InvalidSize.Log(); + + operateOffset = _parent._metaDataSize + _partitionEntry.Offset + offset; + operateSize = size; + break; + + default: + return ResultFs.UnsupportedOperateRangeForPartitionFile.Log(); + } + + return _parent._baseStorage.OperateRange(outBuffer, operationId, operateOffset, operateSize, inBuffer).Ret(); + } + + protected override Result DoRead(out long bytesRead, long offset, Span destination, in ReadOption option) + { + if (this is PartitionFileSystem.PartitionFile file) + { + return DoRead(file, out bytesRead, offset, destination, in option).Ret(); + } + + if (this is Sha256PartitionFileSystem.PartitionFile fileSha) + { + return DoRead(fileSha, out bytesRead, offset, destination, in option).Ret(); + } + + UnsafeHelpers.SkipParamInit(out bytesRead); + Abort.DoAbort("PartitionFileSystemCore.PartitionFile type is not supported."); + return ResultFs.NotImplemented.Log(); + } + + private static Result DoRead(Sha256PartitionFileSystem.PartitionFile fs, out long bytesRead, long offset, + Span destination, in ReadOption option) + { + UnsafeHelpers.SkipParamInit(out bytesRead); + + Result res = fs.DryRead(out long readSize, offset, destination.Length, in option, fs._mode); + if (res.IsFailure()) return res.Miss(); + + long entryStart = fs._parent._metaDataSize + fs._partitionEntry.Offset; + long readEnd = offset + readSize; + long hashTargetStart = fs._partitionEntry.HashTargetOffset; + long hashTargetEnd = hashTargetStart + fs._partitionEntry.HashTargetSize; + + if (readEnd > hashTargetStart && hashTargetEnd > offset) + { + // The portion we're reading contains at least some of the hashed region. + + // Only hash target offset == 0 is supported. + if (hashTargetStart != 0) + return ResultFs.InvalidSha256PartitionHashTarget.Log(); + + // Ensure that the hashed region doesn't extend past the end of the file. + if (hashTargetEnd > fs._partitionEntry.Size) + return ResultFs.InvalidSha256PartitionHashTarget.Log(); + + // Validate our read offset. + long readOffset = entryStart + offset; + if (readOffset < offset) + return ResultFs.OutOfRange.Log(); + + // Prepare a buffer for our calculated hash. + Span hash = stackalloc byte[Sha256Generator.HashSize]; + var sha = new Sha256Generator(); + + if (offset <= hashTargetStart && hashTargetEnd <= readEnd) + { + // Easy case: the portion we're reading contains the entire hashed region. + sha.Initialize(); + + res = fs._parent._baseStorage.Read(readOffset, destination.Slice(0, (int)readSize)); + if (res.IsFailure()) return res.Miss(); + + sha.Update(destination.Slice((int)(hashTargetStart - offset), fs._partitionEntry.HashTargetSize)); + sha.GetHash(hash); + } + else if (hashTargetStart <= offset && readEnd <= hashTargetEnd) + { + // The portion we're reading is located entirely within the hashed region. + int remainingHashTargetSize = fs._partitionEntry.HashTargetSize; + // ReSharper disable once UselessBinaryOperation + // We still want to allow the code to handle any hash target start offset even though it's currently restricted to being only 0. + long currentHashTargetOffset = entryStart + hashTargetStart; + long remainingSize = readSize; + int destBufferOffset = 0; + + sha.Initialize(); + + const int bufferForHashTargetSize = 0x200; + Span bufferForHashTarget = stackalloc byte[bufferForHashTargetSize]; + + // Loop over the entire hashed region to calculate the hash. + while (remainingHashTargetSize > 0) + { + // Read the next chunk of the hash target and update the hash. + int currentReadSize = Math.Min(bufferForHashTargetSize, remainingHashTargetSize); + Span currentHashTargetBuffer = bufferForHashTarget.Slice(0, currentReadSize); + + res = fs._parent._baseStorage.Read(currentHashTargetOffset, currentHashTargetBuffer); + if (res.IsFailure()) return res.Miss(); + + sha.Update(currentHashTargetBuffer); + + // Check if the chunk we just read contains any of the requested range. + if (readOffset <= currentHashTargetOffset + currentReadSize && remainingSize > 0) + { + // Copy the relevant portion of the chunk into the destination buffer. + int hashTargetBufferOffset = (int)Math.Max(readOffset - currentHashTargetOffset, 0); + int copySize = (int)Math.Min(currentReadSize - hashTargetBufferOffset, remainingSize); + + bufferForHashTarget.Slice(hashTargetBufferOffset, copySize).CopyTo(destination.Slice(destBufferOffset)); + + remainingSize -= copySize; + destBufferOffset += copySize; + } + + remainingHashTargetSize -= currentReadSize; + currentHashTargetOffset += currentReadSize; + } + + sha.GetHash(hash); + } + else + { + return ResultFs.InvalidSha256PartitionHashTarget.Log(); + } + + if (!CryptoUtil.IsSameBytes(fs._partitionEntry.Hash, hash, hash.Length)) + { + destination.Slice(0, (int)readSize).Clear(); + return ResultFs.Sha256PartitionHashVerificationFailed.Log(); + } + } + else + { + // We aren't reading hashed data, so we can just read from the base storage. + res = fs._parent._baseStorage.Read(entryStart + offset, destination.Slice(0, (int)readSize)); + if (res.IsFailure()) return res.Miss(); + } + + bytesRead = readSize; + return Result.Success; + } + + private static Result DoRead(PartitionFileSystem.PartitionFile fs, out long bytesRead, long offset, + Span destination, in ReadOption option) + { + UnsafeHelpers.SkipParamInit(out bytesRead); + + Result res = fs.DryRead(out long readSize, offset, destination.Length, in option, fs._mode); + if (res.IsFailure()) return res.Miss(); + + res = fs._parent._baseStorage.Read(fs._parent._metaDataSize + fs._partitionEntry.Offset + offset, + destination.Slice(0, (int)readSize)); + if (res.IsFailure()) return res.Miss(); + + bytesRead = readSize; + return Result.Success; + } + } + + /// + /// Provides access to the root directory from a . + /// + /// A cannot contain any + /// subdirectories, so a will only access the root directory. + /// Based on nnSdk 15.3.0 (FS 15.0.0) + private class PartitionDirectory : IDirectory + { + private int _currentIndex; + private readonly PartitionFileSystemCore _parent; + private readonly OpenDirectoryMode _mode; + + public PartitionDirectory(PartitionFileSystemCore parent, OpenDirectoryMode mode) + { + _currentIndex = 0; + _parent = parent; + _mode = mode; + } + + protected override Result DoRead(out long entriesRead, Span entryBuffer) + { + if (!_mode.HasFlag(OpenDirectoryMode.File)) + { + // A partition file system can't contain any subdirectories. + entriesRead = 0; + return Result.Success; + } + + int entryCount = Math.Min(entryBuffer.Length, _parent._metaData.GetEntryCount() - _currentIndex); + + for (int i = 0; i < entryCount; i++) + { + ref readonly TEntry entry = ref _parent._metaData.GetEntry(_currentIndex); + ref DirectoryEntry dirEntry = ref entryBuffer[i]; + + dirEntry.Type = DirectoryEntryType.File; + dirEntry.Size = entry.Size; + U8Span entryName = _parent._metaData.GetEntryName(_currentIndex); + StringUtils.Strlcpy(dirEntry.Name.Items, entryName, dirEntry.Name.ItemsRo.Length - 1); + + _currentIndex++; + } + + entriesRead = entryCount; + return Result.Success; + } + + protected override Result DoGetEntryCount(out long entryCount) + { + if (_mode.HasFlag(OpenDirectoryMode.File)) + { + entryCount = _parent._metaData.GetEntryCount(); + } + else + { + entryCount = 0; + } + + return Result.Success; + } + } + + public PartitionFileSystemCore() + { + _isInitialized = false; + } + + public override void Dispose() + { + _sharedStorage.Destroy(); + _uniqueMetaData.Destroy(); + base.Dispose(); + } + + public Result Initialize(in SharedRef baseStorage) + { + _sharedStorage.SetByCopy(in baseStorage); + + return Initialize(_sharedStorage.Get).Ret(); + } + + public Result Initialize(in SharedRef baseStorage, MemoryResource allocator) + { + _sharedStorage.SetByCopy(in baseStorage); + + return Initialize(_sharedStorage.Get, allocator).Ret(); + } + + public Result Initialize(IStorage baseStorage) + { + return Initialize(baseStorage, DefaultAllocatorForPartitionFileSystem.Instance).Ret(); + } + + private Result Initialize(IStorage baseStorage, MemoryResource allocator) + { + if (_isInitialized) + return ResultFs.PreconditionViolation.Log(); + + _uniqueMetaData.Reset(new TMetaData()); + if (!_uniqueMetaData.HasValue) + return ResultFs.AllocationMemoryFailedInPartitionFileSystemA.Log(); + + Result res = _uniqueMetaData.Get.Initialize(baseStorage, allocator); + if (res.IsFailure()) return res.Miss(); + + _metaData = _uniqueMetaData.Get; + _baseStorage = baseStorage; + _metaDataSize = _metaData.GetMetaDataSize(); + _isInitialized = true; + return Result.Success; } - protected override Result DoOpenFile(ref UniqueRef outFile, in Path path, OpenMode mode) + public Result Initialize(ref UniqueRef metaData, in SharedRef baseStorage) { - string pathNormalized = PathTools.Normalize(path.ToString()).TrimStart('/'); + _uniqueMetaData.Set(ref metaData); - if (!FileDict.TryGetValue(pathNormalized, out PartitionFileEntry entry)) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound.Value); - } + return Initialize(_uniqueMetaData.Get, in baseStorage).Ret(); + } + + public Result Initialize(TMetaData metaData, in SharedRef baseStorage) + { + if (_isInitialized) + return ResultFs.PreconditionViolation.Log(); + + _sharedStorage.SetByCopy(in baseStorage); + _baseStorage = _sharedStorage.Get; + _metaData = metaData; + _metaDataSize = _metaData.GetMetaDataSize(); + _isInitialized = true; - outFile.Reset(OpenFile(entry, mode)); return Result.Success; } - public IFile OpenFile(PartitionFileEntry entry, OpenMode mode) + public Result GetFileBaseOffset(out long outOffset, U8Span path) { - return new PartitionFile(BaseStorage, HeaderSize + entry.Offset, entry.Size, mode); + UnsafeHelpers.SkipParamInit(out outOffset); + + if (!_isInitialized) + return ResultFs.PreconditionViolation.Log(); + + if (path.Length == 0) + return ResultFs.PathNotFound.Log(); + + int entryIndex = _metaData.GetEntryIndex(path.Slice(1)); + if (entryIndex < 0) + return ResultFs.PathNotFound.Log(); + + outOffset = _metaDataSize + _metaData.GetEntry(entryIndex).Offset; + return Result.Success; } protected override Result DoGetEntryType(out DirectoryEntryType entryType, in Path path) { - UnsafeHelpers.SkipParamInit(out entryType); + Unsafe.SkipInit(out entryType); - if (path.ToString() == "/") + if (!_isInitialized) + return ResultFs.PreconditionViolation.Log(); + + ReadOnlySpan pathString = path.GetString(); + if (pathString.At(0) != RootPath[0]) + return ResultFs.InvalidPathFormat.Log(); + + if (StringUtils.Compare(RootPath, pathString, RootPath.Length + 1) == 0) { entryType = DirectoryEntryType.Directory; return Result.Success; } - if (FileDict.ContainsKey(path.ToString().TrimStart('/'))) + if (_metaData.GetEntryIndex(pathString.Slice(1)) >= 0) { entryType = DirectoryEntryType.File; return Result.Success; @@ -79,114 +510,56 @@ public class PartitionFileSystem : IFileSystem return ResultFs.PathNotFound.Log(); } - protected override Result DoCreateDirectory(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); + protected override Result DoOpenFile(ref UniqueRef outFile, in Path path, OpenMode mode) + { + if (!_isInitialized) + return ResultFs.PreconditionViolation.Log(); + + // LibHac addition to catch empty strings + if (path.GetString().Length == 0) + return ResultFs.PathNotFound.Log(); + + int entryIndex = _metaData.GetEntryIndex(path.GetString().Slice(1)); + if (entryIndex < 0) + return ResultFs.PathNotFound.Log(); + + using var file = new UniqueRef(new PartitionFile(this, in _metaData.GetEntry(entryIndex), mode)); + if (!file.HasValue) + return ResultFs.AllocationMemoryFailedInPartitionFileSystemB.Log(); + + outFile.Set(ref file.Ref); + return Result.Success; + } + + protected override Result DoOpenDirectory(ref UniqueRef outDirectory, in Path path, OpenDirectoryMode mode) + { + if (!_isInitialized) + return ResultFs.PreconditionViolation.Log(); + + if (!(path == RootPath)) + return ResultFs.PathNotFound.Log(); + + using var directory = new UniqueRef(new PartitionDirectory(this, mode)); + if (!directory.HasValue) + return ResultFs.AllocationMemoryFailedInPartitionFileSystemC.Log(); + + outDirectory.Set(ref directory.Ref); + return Result.Success; + } + protected override Result DoCreateFile(in Path path, long size, CreateFileOptions option) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); + protected override Result DoDeleteFile(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); + protected override Result DoCreateDirectory(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); protected override Result DoDeleteDirectory(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); protected override Result DoDeleteDirectoryRecursively(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); protected override Result DoCleanDirectoryRecursively(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoDeleteFile(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoRenameDirectory(in Path currentPath, in Path newPath) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); protected override Result DoRenameFile(in Path currentPath, in Path newPath) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); + protected override Result DoRenameDirectory(in Path currentPath, in Path newPath) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); protected override Result DoCommit() { return Result.Success; } -} -public enum PartitionFileSystemType -{ - Standard, - Hashed -} - -public class PartitionFileSystemHeader -{ - public string Magic; - public int NumFiles; - public int StringTableSize; - public long Reserved; - public PartitionFileSystemType Type; - public int HeaderSize; - public PartitionFileEntry[] Files; - - public PartitionFileSystemHeader(BinaryReader reader) - { - Magic = reader.ReadAscii(4); - NumFiles = reader.ReadInt32(); - StringTableSize = reader.ReadInt32(); - Reserved = reader.ReadInt32(); - - switch (Magic) - { - case "PFS0": - Type = PartitionFileSystemType.Standard; - break; - case "HFS0": - Type = PartitionFileSystemType.Hashed; - break; - default: - ThrowHelper.ThrowResult(ResultFs.PartitionSignatureVerificationFailed.Value, $"Invalid Partition FS type \"{Magic}\""); - break; - } - - int entrySize = PartitionFileEntry.GetEntrySize(Type); - int stringTableOffset = 16 + entrySize * NumFiles; - HeaderSize = stringTableOffset + StringTableSize; - - Files = new PartitionFileEntry[NumFiles]; - for (int i = 0; i < NumFiles; i++) - { - Files[i] = new PartitionFileEntry(reader, Type) { Index = i }; - } - - for (int i = 0; i < NumFiles; i++) - { - reader.BaseStream.Position = stringTableOffset + Files[i].StringTableOffset; - Files[i].Name = reader.ReadAsciiZ(); - } - } -} - -public class PartitionFileEntry -{ - public int Index; - public long Offset; - public long Size; - public uint StringTableOffset; - public long HashedRegionOffset; - public int HashedRegionSize; - public byte[] Hash; - public string Name; - public Validity HashValidity = Validity.Unchecked; - - public PartitionFileEntry(BinaryReader reader, PartitionFileSystemType type) - { - Offset = reader.ReadInt64(); - Size = reader.ReadInt64(); - StringTableOffset = reader.ReadUInt32(); - if (type == PartitionFileSystemType.Hashed) - { - HashedRegionSize = reader.ReadInt32(); - HashedRegionOffset = reader.ReadInt64(); - Hash = reader.ReadBytes(Sha256.DigestSize); - } - else - { - reader.BaseStream.Position += 4; - } - } - - public static int GetEntrySize(PartitionFileSystemType type) - { - switch (type) - { - case PartitionFileSystemType.Standard: - return 0x18; - case PartitionFileSystemType.Hashed: - return 0x40; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - } + protected override Result DoCommitProvisionally(long counter) => ResultFs.UnsupportedCommitProvisionallyForPartitionFileSystem.Log(); } \ No newline at end of file diff --git a/src/LibHac/FsSystem/PartitionFileSystemCore.cs b/src/LibHac/FsSystem/PartitionFileSystemCore.cs deleted file mode 100644 index 45359ebc..00000000 --- a/src/LibHac/FsSystem/PartitionFileSystemCore.cs +++ /dev/null @@ -1,389 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using LibHac.Common; -using LibHac.Crypto; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.FsSystem.Impl; -using LibHac.Util; - -namespace LibHac.FsSystem; - -public class PartitionFileSystemCore : IFileSystem where T : unmanaged, IPartitionFileSystemEntry -{ - private IStorage _baseStorage; - private PartitionFileSystemMetaCore _metaData; - private bool _isInitialized; - private int _dataOffset; - private SharedRef _baseStorageShared; - - public Result Initialize(ref SharedRef baseStorage) - { - Result res = Initialize(baseStorage.Get); - if (res.IsFailure()) return res.Miss(); - - _baseStorageShared.SetByMove(ref baseStorage); - return Result.Success; - } - - public Result Initialize(IStorage baseStorage) - { - if (_isInitialized) - return ResultFs.PreconditionViolation.Log(); - - _metaData = new PartitionFileSystemMetaCore(); - - Result res = _metaData.Initialize(baseStorage); - if (res.IsFailure()) return res.Miss(); - - _baseStorage = baseStorage; - _dataOffset = _metaData.Size; - _isInitialized = true; - - return Result.Success; - } - - public override void Dispose() - { - _baseStorageShared.Destroy(); - base.Dispose(); - } - - protected override Result DoOpenDirectory(ref UniqueRef outDirectory, in Path path, - OpenDirectoryMode mode) - { - if (!_isInitialized) - return ResultFs.PreconditionViolation.Log(); - - if (path != "/"u8) - return ResultFs.PathNotFound.Log(); - - outDirectory.Reset(new PartitionDirectory(this, mode)); - - return Result.Success; - } - - protected override Result DoOpenFile(ref UniqueRef outFile, in Path path, OpenMode mode) - { - if (!_isInitialized) - return ResultFs.PreconditionViolation.Log(); - - if (!mode.HasFlag(OpenMode.Read) && !mode.HasFlag(OpenMode.Write)) - return ResultFs.InvalidArgument.Log(); - - int entryIndex = _metaData.FindEntry(new U8Span(path.GetString().Slice(1))); - if (entryIndex < 0) return ResultFs.PathNotFound.Log(); - - ref T entry = ref _metaData.GetEntry(entryIndex); - - outFile.Reset(new PartitionFile(this, ref entry, mode)); - - return Result.Success; - } - - protected override Result DoGetEntryType(out DirectoryEntryType entryType, in Path path) - { - UnsafeHelpers.SkipParamInit(out entryType); - - if (!_isInitialized) - return ResultFs.PreconditionViolation.Log(); - - ReadOnlySpan pathStr = path.GetString(); - - if (path.IsEmpty() || pathStr[0] != '/') - return ResultFs.InvalidPathFormat.Log(); - - ReadOnlySpan rootPath = "/"u8; - - if (StringUtils.Compare(rootPath, pathStr, 2) == 0) - { - entryType = DirectoryEntryType.Directory; - return Result.Success; - } - - if (_metaData.FindEntry(new U8Span(pathStr.Slice(1))) >= 0) - { - entryType = DirectoryEntryType.File; - return Result.Success; - } - - return ResultFs.PathNotFound.Log(); - } - - protected override Result DoCommit() - { - return Result.Success; - } - - protected override Result DoCreateDirectory(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoCreateFile(in Path path, long size, CreateFileOptions option) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoDeleteDirectory(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoDeleteDirectoryRecursively(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoCleanDirectoryRecursively(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoDeleteFile(in Path path) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoRenameDirectory(in Path currentPath, in Path newPath) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoRenameFile(in Path currentPath, in Path newPath) => ResultFs.UnsupportedWriteForPartitionFileSystem.Log(); - protected override Result DoCommitProvisionally(long counter) => ResultFs.UnsupportedCommitProvisionallyForPartitionFileSystem.Log(); - - private class PartitionFile : IFile - { - private PartitionFileSystemCore ParentFs { get; } - private OpenMode Mode { get; } - private T _entry; - - public PartitionFile(PartitionFileSystemCore parentFs, ref T entry, OpenMode mode) - { - ParentFs = parentFs; - _entry = entry; - Mode = mode; - } - - protected override Result DoRead(out long bytesRead, long offset, Span destination, - in ReadOption option) - { - UnsafeHelpers.SkipParamInit(out bytesRead); - - Result res = DryRead(out long bytesToRead, offset, destination.Length, in option, Mode); - if (res.IsFailure()) return res.Miss(); - - bool hashNeeded = false; - long fileStorageOffset = ParentFs._dataOffset + _entry.Offset; - - if (typeof(T) == typeof(HashedEntry)) - { - ref HashedEntry entry = ref Unsafe.As(ref _entry); - - long readEnd = offset + destination.Length; - long hashEnd = entry.HashOffset + entry.HashSize; - - // The hash must be checked if any part of the hashed region is read - hashNeeded = entry.HashOffset < readEnd && hashEnd >= offset; - } - - if (!hashNeeded) - { - res = ParentFs._baseStorage.Read(fileStorageOffset + offset, destination.Slice(0, (int)bytesToRead)); - } - else - { - ref HashedEntry entry = ref Unsafe.As(ref _entry); - - long readEnd = offset + destination.Length; - long hashEnd = entry.HashOffset + entry.HashSize; - - // Make sure the hashed region doesn't extend past the end of the file - // N's code requires that the hashed region starts at the beginning of the file - if (entry.HashOffset != 0 || hashEnd > entry.Size) - return ResultFs.InvalidSha256PartitionHashTarget.Log(); - - long storageOffset = fileStorageOffset + offset; - - // Nintendo checks for overflow here but not in other places for some reason - if (storageOffset < 0) - return ResultFs.OutOfRange.Log(); - - IHash sha256 = Sha256.CreateSha256Generator(); - sha256.Initialize(); - - var actualHash = new Buffer32(); - - // If the area to read contains the entire hashed area - if (entry.HashOffset >= offset && hashEnd <= readEnd) - { - res = ParentFs._baseStorage.Read(storageOffset, destination.Slice(0, (int)bytesToRead)); - if (res.IsFailure()) return res.Miss(); - - Span hashedArea = destination.Slice((int)(entry.HashOffset - offset), entry.HashSize); - sha256.Update(hashedArea); - } - else - { - // Can't start a read in the middle of the hashed region - if (readEnd > hashEnd || entry.HashOffset > offset) - { - return ResultFs.InvalidSha256PartitionHashTarget.Log(); - } - - int hashRemaining = entry.HashSize; - int readRemaining = (int)bytesToRead; - long readPos = fileStorageOffset + entry.HashOffset; - int outBufPos = 0; - - const int hashBufferSize = 0x200; - Span hashBuffer = stackalloc byte[hashBufferSize]; - - while (hashRemaining > 0) - { - int toRead = Math.Min(hashRemaining, hashBufferSize); - Span hashBufferSliced = hashBuffer.Slice(0, toRead); - - res = ParentFs._baseStorage.Read(readPos, hashBufferSliced); - if (res.IsFailure()) return res.Miss(); - - sha256.Update(hashBufferSliced); - - if (readRemaining > 0 && storageOffset <= readPos + toRead) - { - int hashBufferOffset = (int)Math.Max(storageOffset - readPos, 0); - int toCopy = Math.Min(readRemaining, toRead - hashBufferOffset); - - hashBuffer.Slice(hashBufferOffset, toCopy).CopyTo(destination.Slice(outBufPos)); - - outBufPos += toCopy; - readRemaining -= toCopy; - } - - hashRemaining -= toRead; - readPos += toRead; - } - } - - sha256.GetHash(actualHash); - - if (!CryptoUtil.IsSameBytes(entry.Hash, actualHash, Sha256.DigestSize)) - { - destination.Slice(0, (int)bytesToRead).Clear(); - - return ResultFs.Sha256PartitionHashVerificationFailed.Log(); - } - - res = Result.Success; - } - - if (res.IsSuccess()) - bytesRead = bytesToRead; - - return res; - } - - protected override Result DoWrite(long offset, ReadOnlySpan source, in WriteOption option) - { - Result res = DryWrite(out bool isResizeNeeded, offset, source.Length, in option, Mode); - if (res.IsFailure()) return res.Miss(); - - if (isResizeNeeded) - return ResultFs.UnsupportedWriteForPartitionFile.Log(); - - if (_entry.Size < offset) - return ResultFs.OutOfRange.Log(); - - if (_entry.Size < source.Length + offset) - return ResultFs.InvalidSize.Log(); - - return ParentFs._baseStorage.Write(ParentFs._dataOffset + _entry.Offset + offset, source); - } - - protected override Result DoFlush() - { - if (Mode.HasFlag(OpenMode.Write)) - { - return ParentFs._baseStorage.Flush(); - } - - return Result.Success; - } - - protected override Result DoSetSize(long size) - { - if (Mode.HasFlag(OpenMode.Write)) - { - return ResultFs.UnsupportedWriteForPartitionFile.Log(); - } - - return ResultFs.WriteUnpermitted.Log(); - } - - protected override Result DoGetSize(out long size) - { - size = _entry.Size; - - return Result.Success; - } - - protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan inBuffer) - { - switch (operationId) - { - case OperationId.InvalidateCache: - if (!Mode.HasFlag(OpenMode.Read)) - return ResultFs.ReadUnpermitted.Log(); - - if (Mode.HasFlag(OpenMode.Write)) - return ResultFs.UnsupportedOperateRangeForPartitionFile.Log(); - - break; - case OperationId.QueryRange: - break; - default: - return ResultFs.UnsupportedOperateRangeForPartitionFile.Log(); - } - - if (offset < 0 || offset > _entry.Size) - return ResultFs.OutOfRange.Log(); - - if (size < 0 || offset + size > _entry.Size) - return ResultFs.InvalidSize.Log(); - - long offsetInStorage = ParentFs._dataOffset + _entry.Offset + offset; - - return ParentFs._baseStorage.OperateRange(outBuffer, operationId, offsetInStorage, size, inBuffer); - } - } - - private class PartitionDirectory : IDirectory - { - private PartitionFileSystemCore ParentFs { get; } - private int CurrentIndex { get; set; } - private OpenDirectoryMode Mode { get; } - - public PartitionDirectory(PartitionFileSystemCore parentFs, OpenDirectoryMode mode) - { - ParentFs = parentFs; - CurrentIndex = 0; - Mode = mode; - } - - protected override Result DoRead(out long entriesRead, Span entryBuffer) - { - if (Mode.HasFlag(OpenDirectoryMode.File)) - { - int totalEntryCount = ParentFs._metaData.GetEntryCount(); - int toReadCount = Math.Min(totalEntryCount - CurrentIndex, entryBuffer.Length); - - for (int i = 0; i < toReadCount; i++) - { - entryBuffer[i].Type = DirectoryEntryType.File; - entryBuffer[i].Size = ParentFs._metaData.GetEntry(CurrentIndex).Size; - - U8Span name = ParentFs._metaData.GetName(CurrentIndex); - StringUtils.Copy(entryBuffer[i].Name.Items, name); - entryBuffer[i].Name[PathTool.EntryNameLengthMax] = 0; - - CurrentIndex++; - } - - entriesRead = toReadCount; - } - else - { - entriesRead = 0; - } - - return Result.Success; - } - - protected override Result DoGetEntryCount(out long entryCount) - { - if (Mode.HasFlag(OpenDirectoryMode.File)) - { - entryCount = ParentFs._metaData.GetEntryCount(); - } - else - { - entryCount = 0; - } - - return Result.Success; - } - } -} \ No newline at end of file diff --git a/src/LibHac/FsSystem/PartitionFileSystemMeta.cs b/src/LibHac/FsSystem/PartitionFileSystemMeta.cs new file mode 100644 index 00000000..4cb1f40e --- /dev/null +++ b/src/LibHac/FsSystem/PartitionFileSystemMeta.cs @@ -0,0 +1,366 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using LibHac.Common; +using LibHac.Crypto; +using LibHac.Diag; +using LibHac.Fs; +using LibHac.FsSystem.Impl; +using LibHac.Mem; +using LibHac.Util; +using Buffer = LibHac.Mem.Buffer; + +namespace LibHac.FsSystem +{ + /// + /// Contains values used by for reading + /// and building the metadata of a partition file system. + /// + public interface IPartitionFileSystemFormat + { + /// The signature bytes that are expected to be at the start of the partition file system. + static abstract ReadOnlySpan VersionSignature { get; } + + /// The maximum length of file names inside the partition file system. + static abstract uint EntryNameLengthMax { get; } + + /// The alignment that the start of the data for each file must be aligned to. + static abstract uint FileDataAlignmentSize { get; } + + /// The returned when the is incorrect. + static abstract Result ResultSignatureVerificationFailed { get; } + } + + /// + /// The minimum fields needed for the file entry type in a . + /// + public interface IPartitionFileSystemEntry + { + long Offset { get; } + long Size { get; } + int NameOffset { get; } + } + + /// + /// The minimum fields needed for the header type in a . + /// + public interface IPartitionFileSystemHeader + { + ReadOnlySpan Signature { get; } + int EntryCount { get; } + int NameTableSize { get; } + } + + /// + /// Reads the metadata from a . + /// The metadata has three sections: A single struct of type , a table of + /// structs containing info on each file, and a table of the names of all the files. + /// + /// A traits class that provides values used to read and build the metadata. + /// The type of the header at the beginning of the metadata. + /// The type of the entries in the file table in the metadata. + /// Based on nnSdk 15.3.0 (FS 15.0.0) + public class PartitionFileSystemMetaCore : IDisposable + where TFormat : IPartitionFileSystemFormat + where THeader : unmanaged, IPartitionFileSystemHeader + where TEntry : unmanaged, IPartitionFileSystemEntry + { + protected bool IsInitialized; + protected BufferSegment HeaderBuffer; + protected BufferSegment EntryBuffer; + protected BufferSegment NameTableBuffer; + protected long MetaDataSize; + protected MemoryResource Allocator; + protected Buffer MetaDataBuffer; + + private ref readonly THeader Header => ref MemoryMarshal.GetReference(HeaderBuffer.GetSpan()); + private ReadOnlySpan Entries => EntryBuffer.GetSpan(); + private ReadOnlySpan NameTable => NameTableBuffer.Span; + + public PartitionFileSystemMetaCore() + { + IsInitialized = false; + MetaDataSize = 0; + Allocator = null; + MetaDataBuffer = default; + } + + public virtual void Dispose() + { + DeallocateBuffer(); + } + + protected void DeallocateBuffer() + { + if (!MetaDataBuffer.IsNull) + { + Assert.SdkNotNull(Allocator); + + Allocator.Deallocate(ref MetaDataBuffer); + } + } + + public Result Initialize(IStorage baseStorage, Buffer metaBuffer, int metaDataSize) + { + // Added check for LibHac because Buffer carries a length along with its pointer. + if (metaBuffer.Length < metaDataSize) + return ResultFs.InvalidSize.Log(); + + Span metaSpan = metaBuffer.Span.Slice(0, metaDataSize); + + // Validate size for header. + if (metaDataSize < Unsafe.SizeOf()) + return ResultFs.InvalidSize.Log(); + + // Read the header. + Result res = baseStorage.Read(offset: 0, metaSpan); + if (res.IsFailure()) return res.Miss(); + + // Set and validate the header. + // Get the section of the buffer that contains the header. + HeaderBuffer = metaBuffer.GetSegment(0, Unsafe.SizeOf()); + Span headerSpan = HeaderBuffer.Span; + ref readonly THeader header = ref Unsafe.As(ref MemoryMarshal.GetReference(headerSpan)); + + if (!CryptoUtil.IsSameBytes(headerSpan, TFormat.VersionSignature, TFormat.VersionSignature.Length)) + return ResultFs.PartitionSignatureVerificationFailed.Log(); + + res = QueryMetaDataSize(out MetaDataSize, in header); + if (res.IsFailure()) return res.Miss(); + + int entriesSize = header.EntryCount * Unsafe.SizeOf(); + + // Note: Instead of doing this check after assigning the buffers like in the original, we do the check before + // assigning the buffers because trying to get the buffers when the meta buffer is too small will + // result in an exception in C#. + + // Validate size for header + entries + name table. + if (metaDataSize < Unsafe.SizeOf() + entriesSize + header.NameTableSize) + return ResultFs.InvalidSize.Log(); + + // Setup entries and name table. + EntryBuffer = metaBuffer.GetSegment(Unsafe.SizeOf(), entriesSize); + NameTableBuffer = metaBuffer.GetSegment(Unsafe.SizeOf() + entriesSize, header.NameTableSize); + + // Read entries and name table. + Span destSpan = metaSpan.Slice(Unsafe.SizeOf(), entriesSize + header.NameTableSize); + res = baseStorage.Read(Unsafe.SizeOf(), destSpan); + if (res.IsFailure()) return res.Miss(); + + // Mark as initialized. + IsInitialized = true; + return Result.Success; + } + + public Result Initialize(IStorage baseStorage, MemoryResource allocator) + { + Assert.SdkRequiresNotNull(allocator); + + // Determine the meta data size. + Result res = QueryMetaDataSize(out MetaDataSize, baseStorage); + if (res.IsFailure()) return res.Miss(); + + // Deallocate any old meta buffer and allocate a new one. + DeallocateBuffer(); + Allocator = allocator; + MetaDataBuffer = Allocator.Allocate(MetaDataSize); + if (MetaDataBuffer.IsNull) + return ResultFs.AllocationMemoryFailedInPartitionFileSystemMetaA.Log(); + + // Perform regular initialization. + res = Initialize(baseStorage, MetaDataBuffer, (int)MetaDataSize); + if (res.IsFailure()) return res.Miss(); + + return Result.Success; + } + + /// + /// Queries the size of the metadata by reading the metadata header from the provided storage + /// + /// If the operation returns successfully, contains the size of the metadata. + /// The containing the metadata. + /// : The operation was successful.
+ /// : The header doesn't have + /// the correct file signature.
+ public static Result QueryMetaDataSize(out long outSize, IStorage storage) + { + UnsafeHelpers.SkipParamInit(out outSize); + + Unsafe.SkipInit(out THeader header); + Result res = storage.Read(0, SpanHelpers.AsByteSpan(ref header)); + if (res.IsFailure()) return res.Miss(); + + res = QueryMetaDataSize(out outSize, in header); + if (res.IsFailure()) return res.Miss(); + + return Result.Success; + } + + /// + /// Queries the size of the metadata with the provided header. + /// + /// If the operation returns successfully, contains the size of the metadata. + /// The metadata header. + /// : The operation was successful.
+ /// : The header doesn't have + /// the correct file signature.
+ protected static Result QueryMetaDataSize(out long outSize, in THeader header) + { + UnsafeHelpers.SkipParamInit(out outSize); + + if (!CryptoUtil.IsSameBytes(SpanHelpers.AsReadOnlyByteSpan(header), TFormat.VersionSignature, + TFormat.VersionSignature.Length)) + { + return TFormat.ResultSignatureVerificationFailed.Log(); + } + + outSize = Unsafe.SizeOf() + header.EntryCount * Unsafe.SizeOf() + header.NameTableSize; + return Result.Success; + } + + /// + /// Returns the size of the meta data header. + /// + /// The size of . + public static int GetHeaderSize() + { + return Unsafe.SizeOf(); + } + + public int GetMetaDataSize() + { + return (int)MetaDataSize; + } + + public int GetEntryIndex(U8Span entryName) + { + if (!IsInitialized) + return Result.ConvertResultToReturnType(ResultFs.PreconditionViolation.Value); + + ref readonly THeader header = ref Header; + ReadOnlySpan entries = Entries; + ReadOnlySpan nameTable = NameTable; + + for (int i = 0; i < header.EntryCount; i++) + { + if (entries[i].NameOffset >= header.NameTableSize) + return Result.ConvertResultToReturnType(ResultFs.InvalidPartitionEntryOffset.Value); + + int maxNameLen = header.NameTableSize - entries[i].NameOffset; + if (StringUtils.Compare(nameTable.Slice(entries[i].NameOffset), entryName, maxNameLen) == 0) + { + return i; + } + } + + return -1; + } + + public ref readonly TEntry GetEntry(int entryIndex) + { + Abort.DoAbortUnless(IsInitialized, ResultFs.PreconditionViolation.Value); + Abort.DoAbortUnless(entryIndex >= 0 && entryIndex < Header.EntryCount, ResultFs.PreconditionViolation.Value); + + return ref Entries[entryIndex]; + } + + public int GetEntryCount() + { + if (!IsInitialized) + return Result.ConvertResultToReturnType(ResultFs.PreconditionViolation.Value); + + return Header.EntryCount; + } + + public U8Span GetEntryName(int entryIndex) + { + Abort.DoAbortUnless(IsInitialized, ResultFs.PreconditionViolation.Value); + Abort.DoAbortUnless(entryIndex < Header.EntryCount, ResultFs.PreconditionViolation.Value); + + return new U8Span(NameTable.Slice(GetEntry(entryIndex).NameOffset)); + } + } +} + +namespace LibHac.FsSystem +{ + using TFormat = Sha256PartitionFileSystemFormat; + using THeader = PartitionFileSystemFormat.PartitionFileSystemHeaderImpl; + + /// + /// Reads the metadata for a . + /// + /// Based on nnSdk 15.3.0 (FS 15.0.0) + public class Sha256PartitionFileSystemMeta : PartitionFileSystemMetaCore + { + public Result Initialize(IStorage baseStorage, MemoryResource allocator, ReadOnlySpan hash) + { + Result res = Initialize(baseStorage, allocator, hash, salt: default); + if (res.IsFailure()) return res.Miss(); + + return Result.Success; + } + + public Result Initialize(IStorage baseStorage, MemoryResource allocator, ReadOnlySpan hash, Optional salt) + { + if (hash.Length != Sha256Generator.HashSize) + return ResultFs.PreconditionViolation.Log(); + + Result res = QueryMetaDataSize(out MetaDataSize, baseStorage); + if (res.IsFailure()) return res.Miss(); + + DeallocateBuffer(); + Allocator = allocator; + MetaDataBuffer = Allocator.Allocate(MetaDataSize); + if (MetaDataBuffer.IsNull) + return ResultFs.AllocationMemoryFailedInPartitionFileSystemMetaB.Log(); + + Span metaDataSpan = MetaDataBuffer.Span.Slice(0, (int)MetaDataSize); + + res = baseStorage.Read(offset: 0, metaDataSpan); + if (res.IsFailure()) return res.Miss(); + + Span hashBuffer = stackalloc byte[Sha256Generator.HashSize]; + var generator = new Sha256Generator(); + generator.Initialize(); + generator.Update(metaDataSpan); + if (salt.HasValue) + { + generator.Update(SpanHelpers.AsReadOnlyByteSpan(in salt.ValueRo)); + } + + generator.GetHash(hashBuffer); + + if (!CryptoUtil.IsSameBytes(hash, hashBuffer, hash.Length)) + return ResultFs.Sha256PartitionHashVerificationFailed.Log(); + + HeaderBuffer = MetaDataBuffer.GetSegment(0, Unsafe.SizeOf()); + Span headerSpan = HeaderBuffer.Span; + ref readonly THeader header = ref Unsafe.As(ref MemoryMarshal.GetReference(headerSpan)); + + if (!CryptoUtil.IsSameBytes(headerSpan, TFormat.VersionSignature, TFormat.VersionSignature.Length)) + return TFormat.ResultSignatureVerificationFailed.Log(); + + int entriesSize = header.EntryCount * Unsafe.SizeOf(); + + // Validate size for header + entries + name table. + if (MetaDataSize < Unsafe.SizeOf() + entriesSize + header.NameTableSize) + return ResultFs.InvalidSha256PartitionMetaDataSize.Log(); + + // Setup entries and name table. + EntryBuffer = MetaDataBuffer.GetSegment(Unsafe.SizeOf(), entriesSize); + NameTableBuffer = MetaDataBuffer.GetSegment(Unsafe.SizeOf() + entriesSize, header.NameTableSize); + + // Mark as initialized. + IsInitialized = true; + return Result.Success; + } + } + + /// + /// Reads the metadata for a . + /// + /// Based on nnSdk 15.3.0 (FS 15.0.0) + public class PartitionFileSystemMeta : PartitionFileSystemMetaCore { } +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/PartitionFileSystemMetaCore.cs b/src/LibHac/FsSystem/PartitionFileSystemMetaCore.cs deleted file mode 100644 index a01d1022..00000000 --- a/src/LibHac/FsSystem/PartitionFileSystemMetaCore.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using LibHac.Common; -using LibHac.Fs; -using LibHac.FsSystem.Impl; -using LibHac.Util; - -namespace LibHac.FsSystem; - -public class PartitionFileSystemMetaCore where T : unmanaged, IPartitionFileSystemEntry -{ - private static int HeaderSize => Unsafe.SizeOf
(); - private static int EntrySize => Unsafe.SizeOf(); - - private bool IsInitialized { get; set; } - private int EntryCount { get; set; } - private int StringTableSize { get; set; } - private int StringTableOffset { get; set; } - private byte[] Buffer { get; set; } - - public int Size { get; private set; } - - public Result Initialize(IStorage baseStorage) - { - var header = new Header(); - - Result res = baseStorage.Read(0, SpanHelpers.AsByteSpan(ref header)); - if (res.IsFailure()) return res.Miss(); - - int pfsMetaSize = HeaderSize + header.EntryCount * EntrySize + header.StringTableSize; - Buffer = new byte[pfsMetaSize]; - Size = pfsMetaSize; - - return Initialize(baseStorage, Buffer); - } - - private Result Initialize(IStorage baseStorage, Span buffer) - { - if (buffer.Length < HeaderSize) - return ResultFs.InvalidSize.Log(); - - Result res = baseStorage.Read(0, buffer.Slice(0, HeaderSize)); - if (res.IsFailure()) return res.Miss(); - - ref Header header = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); - - if (header.Magic != GetMagicValue()) - return GetInvalidMagicResult(); - - EntryCount = header.EntryCount; - - int entryTableOffset = HeaderSize; - int entryTableSize = EntryCount * EntrySize; - - StringTableOffset = entryTableOffset + entryTableSize; - StringTableSize = header.StringTableSize; - - int pfsMetaSize = StringTableOffset + StringTableSize; - - if (buffer.Length < pfsMetaSize) - return ResultFs.InvalidSize.Log(); - - res = baseStorage.Read(entryTableOffset, - buffer.Slice(entryTableOffset, entryTableSize + StringTableSize)); - - if (res.IsSuccess()) - { - IsInitialized = true; - } - - return res; - } - - public int GetEntryCount() - { - // FS aborts instead of returning the result value - if (!IsInitialized) - throw new HorizonResultException(ResultFs.PreconditionViolation.Log()); - - return EntryCount; - } - - public int FindEntry(U8Span name) - { - // FS aborts instead of returning the result value - if (!IsInitialized) - throw new HorizonResultException(ResultFs.PreconditionViolation.Log()); - - int stringTableSize = StringTableSize; - - ReadOnlySpan entries = GetEntries(); - ReadOnlySpan names = GetStringTable(); - - for (int i = 0; i < entries.Length; i++) - { - if (stringTableSize <= entries[i].NameOffset) - { - throw new HorizonResultException(ResultFs.InvalidPartitionEntryOffset.Log()); - } - - ReadOnlySpan entryName = names.Slice(entries[i].NameOffset); - - if (StringUtils.Compare(name, entryName) == 0) - { - return i; - } - } - - return -1; - } - - public ref T GetEntry(int index) - { - if (!IsInitialized || index < 0 || index > EntryCount) - throw new HorizonResultException(ResultFs.PreconditionViolation.Log()); - - return ref GetEntries()[index]; - } - - public U8Span GetName(int index) - { - int nameOffset = GetEntry(index).NameOffset; - ReadOnlySpan table = GetStringTable(); - - // Nintendo doesn't check the offset here like they do in FindEntry, but we will for safety - if (table.Length <= nameOffset) - { - throw new HorizonResultException(ResultFs.InvalidPartitionEntryOffset.Log()); - } - - return new U8Span(table.Slice(nameOffset)); - } - - private Span GetEntries() - { - Debug.Assert(IsInitialized); - Debug.Assert(Buffer.Length >= HeaderSize + EntryCount * EntrySize); - - Span entryBuffer = Buffer.AsSpan(HeaderSize, EntryCount * EntrySize); - return MemoryMarshal.Cast(entryBuffer); - } - - private ReadOnlySpan GetStringTable() - { - Debug.Assert(IsInitialized); - Debug.Assert(Buffer.Length >= StringTableOffset + StringTableSize); - - return Buffer.AsSpan(StringTableOffset, StringTableSize); - } - - // You can't attach constant values to interfaces in C#, so workaround that - // by getting the values based on which generic type is used - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Result GetInvalidMagicResult() - { - if (typeof(T) == typeof(StandardEntry)) - { - return ResultFs.PartitionSignatureVerificationFailed.Log(); - } - - if (typeof(T) == typeof(HashedEntry)) - { - return ResultFs.Sha256PartitionSignatureVerificationFailed.Log(); - } - - throw new NotSupportedException(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint GetMagicValue() - { - if (typeof(T) == typeof(StandardEntry)) - { - return 0x30534650; // PFS0 - } - - if (typeof(T) == typeof(HashedEntry)) - { - return 0x30534648; // HFS0 - } - - throw new NotSupportedException(); - } - - [StructLayout(LayoutKind.Sequential, Size = 0x10)] - private struct Header - { - public uint Magic; - public int EntryCount; - public int StringTableSize; - } -} \ No newline at end of file diff --git a/src/LibHac/Mem/Buffer.cs b/src/LibHac/Mem/Buffer.cs index a2070ad9..d86d54b8 100644 --- a/src/LibHac/Mem/Buffer.cs +++ b/src/LibHac/Mem/Buffer.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Runtime.InteropServices; namespace LibHac.Mem; @@ -38,6 +39,25 @@ public struct Buffer : IEquatable Extra = extra; } + /// + /// Forms a out of the current that begins at a specified index. + /// + /// The index at which to begin the segment. + /// A that contains all elements of the current instance + /// from to the end of the instance. + /// The must not be accessed after this parent is deallocated. + internal BufferSegment GetSegment(int start) => new BufferSegment(_memory.Slice(start)); + + /// + /// Forms a out of the current starting at a specified index for a specified length. + /// + /// The index at which to begin the segment. + /// The number of elements to include in the segment. + /// A that contains elements from the current + /// instance starting at . + /// The must not be accessed after this parent is deallocated. + internal BufferSegment GetSegment(int start, int length) => new BufferSegment(_memory.Slice(start, length)); + public static bool operator ==(Buffer left, Buffer right) => left._memory.Equals(right._memory); public static bool operator !=(Buffer left, Buffer right) => !(left == right); @@ -45,4 +65,38 @@ public struct Buffer : IEquatable public override bool Equals(object obj) => obj is Buffer other && Equals(other); public bool Equals(Buffer other) => _memory.Equals(other._memory); public override int GetHashCode() => _memory.GetHashCode(); +} + +/// +/// Represents a region of memory borrowed from a . +/// This must not be accessed after the parent that created it is deallocated. +/// +public readonly struct BufferSegment +{ + private readonly Memory _memory; + + public BufferSegment(Memory memory) + { + _memory = memory; + } + + /// + /// The length of the buffer in bytes. + /// + public int Length => _memory.Length; + + /// + /// Gets a from the . + /// + public Span Span => _memory.Span; + + /// + /// Gets a from the of the specified type. + /// + public Span GetSpan() where T : unmanaged => MemoryMarshal.Cast(Span); + + /// + /// Returns if the is not valid. + /// + public bool IsNull => _memory.IsEmpty; } \ No newline at end of file diff --git a/src/LibHac/Tools/Fs/Xci.cs b/src/LibHac/Tools/Fs/Xci.cs index 1595f6d2..9613410c 100644 --- a/src/LibHac/Tools/Fs/Xci.cs +++ b/src/LibHac/Tools/Fs/Xci.cs @@ -37,9 +37,10 @@ public class Xci { XciPartition root = GetRootPartition(); if (type == XciPartitionType.Root) return root; + string partitionFileName = $"/{type.GetFileName()}"; using var partitionFile = new UniqueRef(); - root.OpenFile(ref partitionFile.Ref, type.GetFileName().ToU8Span(), OpenMode.Read).ThrowIfFailure(); + root.OpenFile(ref partitionFile.Ref, partitionFileName.ToU8Span(), OpenMode.Read).ThrowIfFailure(); return new XciPartition(partitionFile.Release().AsStorage()); } @@ -69,10 +70,13 @@ public class Xci } } -public class XciPartition : PartitionFileSystem +public class XciPartition : Sha256PartitionFileSystem { public long Offset { get; internal set; } public Validity HashValidity { get; set; } = Validity.Unchecked; - public XciPartition(IStorage storage) : base(storage) { } + public XciPartition(IStorage storage) + { + Initialize(storage).ThrowIfFailure(); + } } \ No newline at end of file diff --git a/src/LibHac/Tools/FsSystem/NcaUtils/Nca.cs b/src/LibHac/Tools/FsSystem/NcaUtils/Nca.cs index 3bfd7dff..de5dce32 100644 --- a/src/LibHac/Tools/FsSystem/NcaUtils/Nca.cs +++ b/src/LibHac/Tools/FsSystem/NcaUtils/Nca.cs @@ -481,7 +481,9 @@ public class Nca switch (header.FormatType) { case NcaFormatType.Pfs0: - return new PartitionFileSystem(storage); + var pfs = new PartitionFileSystem(); + pfs.Initialize(storage).ThrowIfFailure(); + return pfs; case NcaFormatType.Romfs: return new RomFsFileSystem(storage); default: diff --git a/src/LibHac/Tools/FsSystem/PartitionFileSystemBuilder.cs b/src/LibHac/Tools/FsSystem/PartitionFileSystemBuilder.cs index 60986ee0..b9858d21 100644 --- a/src/LibHac/Tools/FsSystem/PartitionFileSystemBuilder.cs +++ b/src/LibHac/Tools/FsSystem/PartitionFileSystemBuilder.cs @@ -2,17 +2,24 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using LibHac.Common; using LibHac.Crypto; using LibHac.Fs; using LibHac.Fs.Fsa; -using LibHac.FsSystem; +using LibHac.FsSystem.Impl; using LibHac.Tools.Fs; using LibHac.Util; namespace LibHac.Tools.FsSystem; +public enum PartitionFileSystemType +{ + Standard, + Hashed +} + public class PartitionFileSystemBuilder { private const int HeaderSize = 0x10; @@ -75,7 +82,7 @@ public class PartitionFileSystemBuilder { if (type == PartitionFileSystemType.Hashed) CalculateHashes(); - int entryTableSize = Entries.Count * PartitionFileEntry.GetEntrySize(type); + int entryTableSize = Entries.Count * GetEntrySize(type); int stringTableSize = CalcStringTableSize(HeaderSize + entryTableSize, type); int metaDataSize = HeaderSize + entryTableSize + stringTableSize; @@ -173,6 +180,19 @@ public class PartitionFileSystemBuilder sha.GetHash(entry.Hash); } } + + public static int GetEntrySize(PartitionFileSystemType type) + { + switch (type) + { + case PartitionFileSystemType.Standard: + return Unsafe.SizeOf(); + case PartitionFileSystemType.Hashed: + return Unsafe.SizeOf(); + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } private class Entry { diff --git a/src/hactoolnet/ProcessPfs.cs b/src/hactoolnet/ProcessPfs.cs index db1af4bd..ebd29dab 100644 --- a/src/hactoolnet/ProcessPfs.cs +++ b/src/hactoolnet/ProcessPfs.cs @@ -1,14 +1,19 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Reflection; using System.Text; +using LibHac; +using LibHac.Common; using LibHac.Fs; +using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Tools.Es; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Util; using static hactoolnet.Print; +using Path = LibHac.Fs.Path; namespace hactoolnet; @@ -16,24 +21,58 @@ internal static class ProcessPfs { public static void Process(Context ctx) { - using (var file = new LocalStorage(ctx.Options.InFile, FileAccess.Read)) + using var file = new LocalStorage(ctx.Options.InFile, FileAccess.Read); + + IFileSystem fs = null; + using UniqueRef pfs = new UniqueRef(); + using UniqueRef hfs = new UniqueRef(); + + pfs.Reset(new PartitionFileSystem()); + Result res = pfs.Get.Initialize(file); + if (res.IsSuccess()) { - var pfs = new PartitionFileSystem(file); - ctx.Logger.LogMessage(pfs.Print()); - - if (ctx.Options.OutDir != null) + fs = pfs.Get; + ctx.Logger.LogMessage(pfs.Get.Print()); + } + else if (!ResultFs.PartitionSignatureVerificationFailed.Includes(res)) + { + res.ThrowIfFailure(); + } + else + { + // Reading the input as a PartitionFileSystem didn't work. Try reading it as an Sha256PartitionFileSystem + hfs.Reset(new Sha256PartitionFileSystem()); + res = hfs.Get.Initialize(file); + if (res.IsFailure()) { - pfs.Extract(ctx.Options.OutDir, ctx.Logger); + if (ResultFs.Sha256PartitionSignatureVerificationFailed.Includes(res)) + { + ResultFs.PartitionSignatureVerificationFailed.Value.ThrowIfFailure(); + } + + res.ThrowIfFailure(); } - if (pfs.EnumerateEntries("*.nca", SearchOptions.Default).Any()) - { - ProcessAppFs.Process(ctx, pfs); - } + fs = hfs.Get; + ctx.Logger.LogMessage(hfs.Get.Print()); + } + + if (ctx.Options.OutDir != null) + { + fs.Extract(ctx.Options.OutDir, ctx.Logger); + } + + if (fs.EnumerateEntries("*.nca", SearchOptions.Default).Any()) + { + ProcessAppFs.Process(ctx, fs); } } - private static string Print(this PartitionFileSystem pfs) + private static string Print(this PartitionFileSystemCore pfs) + where TMetaData : PartitionFileSystemMetaCore, new() + where TFormat : IPartitionFileSystemFormat + where THeader : unmanaged, IPartitionFileSystemHeader + where TEntry : unmanaged, IPartitionFileSystemEntry { const int colLen = 36; @@ -42,17 +81,31 @@ internal static class ProcessPfs sb.AppendLine("PFS0:"); - PrintItem(sb, colLen, "Magic:", pfs.Header.Magic); - PrintItem(sb, colLen, "Number of files:", pfs.Header.NumFiles); - - for (int i = 0; i < pfs.Files.Length; i++) + using (var rootDir = new UniqueRef()) { - PartitionFileEntry file = pfs.Files[i]; + using var rootPath = new Path(); + PathFunctions.SetUpFixedPath(ref rootPath.Ref(), "/"u8).ThrowIfFailure(); + pfs.OpenDirectory(ref rootDir.Ref, in rootPath, OpenDirectoryMode.All).ThrowIfFailure(); + rootDir.Get.GetEntryCount(out long entryCount).ThrowIfFailure(); - string label = i == 0 ? "Files:" : ""; - string data = $"pfs0:/{file.Name}"; + PrintItem(sb, colLen, "Magic:", StringUtils.Utf8ZToString(TFormat.VersionSignature)); + PrintItem(sb, colLen, "Number of files:", entryCount); - PrintItem(sb, colLen, label, data); + var dirEntry = new DirectoryEntry(); + bool isFirstFile = true; + + while (true) + { + rootDir.Get.Read(out long entriesRead, new Span(ref dirEntry)).ThrowIfFailure(); + if (entriesRead == 0) + break; + + string label = isFirstFile ? "Files:" : ""; + string printedFilePath = $"pfs0:/{StringUtils.Utf8ZToString(dirEntry.Name)}"; + + PrintItem(sb, colLen, label, printedFilePath); + isFirstFile = false; + } } return sb.ToString(); diff --git a/src/hactoolnet/ProcessXci.cs b/src/hactoolnet/ProcessXci.cs index eb13e85f..b62f4492 100644 --- a/src/hactoolnet/ProcessXci.cs +++ b/src/hactoolnet/ProcessXci.cs @@ -3,11 +3,14 @@ using System.IO; using System.Linq; using LibHac.Common; using LibHac.Fs; +using LibHac.Fs.Fsa; using LibHac.Fs.Impl; using LibHac.FsSystem; using LibHac.Gc.Impl; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; +using LibHac.Util; +using Path = LibHac.Fs.Path; namespace hactoolnet; @@ -15,59 +18,61 @@ internal static class ProcessXci { public static void Process(Context ctx) { - using (var file = new LocalStorage(ctx.Options.InFile, FileAccess.Read)) + using var file = new LocalStorage(ctx.Options.InFile, FileAccess.Read); + var xci = new Xci(ctx.KeySet, file); + + ctx.Logger.LogMessage(xci.Print()); + + if (ctx.Options.RootDir != null) { - var xci = new Xci(ctx.KeySet, file); + xci.OpenPartition(XciPartitionType.Root).Extract(ctx.Options.RootDir, ctx.Logger); + } - ctx.Logger.LogMessage(xci.Print()); + if (ctx.Options.UpdateDir != null && xci.HasPartition(XciPartitionType.Update)) + { + xci.OpenPartition(XciPartitionType.Update).Extract(ctx.Options.UpdateDir, ctx.Logger); + } - if (ctx.Options.RootDir != null) + if (ctx.Options.NormalDir != null && xci.HasPartition(XciPartitionType.Normal)) + { + xci.OpenPartition(XciPartitionType.Normal).Extract(ctx.Options.NormalDir, ctx.Logger); + } + + if (ctx.Options.SecureDir != null && xci.HasPartition(XciPartitionType.Secure)) + { + xci.OpenPartition(XciPartitionType.Secure).Extract(ctx.Options.SecureDir, ctx.Logger); + } + + if (ctx.Options.LogoDir != null && xci.HasPartition(XciPartitionType.Logo)) + { + xci.OpenPartition(XciPartitionType.Logo).Extract(ctx.Options.LogoDir, ctx.Logger); + } + + if (ctx.Options.OutDir != null) + { + XciPartition root = xci.OpenPartition(XciPartitionType.Root); + if (root == null) { - xci.OpenPartition(XciPartitionType.Root).Extract(ctx.Options.RootDir, ctx.Logger); + ctx.Logger.LogMessage("Could not find root partition"); + return; } - if (ctx.Options.UpdateDir != null && xci.HasPartition(XciPartitionType.Update)) + foreach (DirectoryEntryEx sub in root.EnumerateEntries()) { - xci.OpenPartition(XciPartitionType.Update).Extract(ctx.Options.UpdateDir, ctx.Logger); + using var subPfsFile = new UniqueRef(); + root.OpenFile(ref subPfsFile.Ref, sub.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using var subPfs = new UniqueRef(new Sha256PartitionFileSystem()); + subPfs.Get.Initialize(subPfsFile.Get.AsStorage()).ThrowIfFailure(); + + string subDir = System.IO.Path.Combine(ctx.Options.OutDir, sub.Name); + subPfs.Get.Extract(subDir, ctx.Logger); } + } - if (ctx.Options.NormalDir != null && xci.HasPartition(XciPartitionType.Normal)) - { - xci.OpenPartition(XciPartitionType.Normal).Extract(ctx.Options.NormalDir, ctx.Logger); - } - - if (ctx.Options.SecureDir != null && xci.HasPartition(XciPartitionType.Secure)) - { - xci.OpenPartition(XciPartitionType.Secure).Extract(ctx.Options.SecureDir, ctx.Logger); - } - - if (ctx.Options.LogoDir != null && xci.HasPartition(XciPartitionType.Logo)) - { - xci.OpenPartition(XciPartitionType.Logo).Extract(ctx.Options.LogoDir, ctx.Logger); - } - - if (ctx.Options.OutDir != null) - { - XciPartition root = xci.OpenPartition(XciPartitionType.Root); - if (root == null) - { - ctx.Logger.LogMessage("Could not find root partition"); - return; - } - - foreach (PartitionFileEntry sub in root.Files) - { - var subPfs = new PartitionFileSystem(root.OpenFile(sub, OpenMode.Read).AsStorage()); - string subDir = System.IO.Path.Combine(ctx.Options.OutDir, sub.Name); - - subPfs.Extract(subDir, ctx.Logger); - } - } - - if (xci.HasPartition(XciPartitionType.Secure)) - { - ProcessAppFs.Process(ctx, xci.OpenPartition(XciPartitionType.Secure)); - } + if (xci.HasPartition(XciPartitionType.Secure)) + { + ProcessAppFs.Process(ctx, xci.OpenPartition(XciPartitionType.Secure)); } } @@ -173,21 +178,32 @@ internal static class ProcessXci using ScopedIndentation mainHeader = sb.AppendHeader($"{type.Print()} Partition:{partition.HashValidity.GetValidityString()}"); - sb.PrintItem("Magic:", partition.Header.Magic); - sb.PrintItem("Number of files:", partition.Files.Length); + using var rootDir = new UniqueRef(); + using var rootPath = new Path(); + PathFunctions.SetUpFixedPath(ref rootPath.Ref(), "/"u8).ThrowIfFailure(); + partition.OpenDirectory(ref rootDir.Ref, in rootPath, OpenDirectoryMode.All).ThrowIfFailure(); + rootDir.Get.GetEntryCount(out long entryCount).ThrowIfFailure(); - string name = type.GetFileName(); + sb.PrintItem("Magic:", "HFS0"); + sb.PrintItem("Number of files:", entryCount); - if (partition.Files.Length > 0 && partition.Files.Length < 100) + if (entryCount > 0 && entryCount < 100) { - for (int i = 0; i < partition.Files.Length; i++) - { - PartitionFileEntry file = partition.Files[i]; + string partitionName = type.GetFileName(); + var dirEntry = new DirectoryEntry(); + bool isFirstFile = true; - string label = i == 0 ? "Files:" : ""; - string data = $"{name}:/{file.Name}"; + while (true) + { + rootDir.Get.Read(out long entriesRead, new Span(ref dirEntry)).ThrowIfFailure(); + if (entriesRead == 0) + break; + + string label = isFirstFile ? "Files:" : ""; + string data = $"{partitionName}:/{StringUtils.Utf8ZToString(dirEntry.Name)}"; sb.PrintItem(label, data); + isFirstFile = false; } } } diff --git a/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs b/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs index 1ff05b94..e085b8f3 100644 --- a/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs +++ b/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using LibHac.FsSystem; +using LibHac.FsSystem.Impl; using Xunit; using static LibHac.Tests.Common.Layout; @@ -336,4 +337,45 @@ public class TypeLayoutTests Assert.Equal(Constants.IntegrityMaxLayerCount - 1, s.LevelBlockSizes.ItemsRo.Length); } + + [Fact] + public static void PartitionFileSystemFormat_PartitionEntry_Layout() + { + PartitionFileSystemFormat.PartitionEntry s = default; + + Assert.Equal(0x18, Unsafe.SizeOf()); + + Assert.Equal(0x00, GetOffset(in s, in s.Offset)); + Assert.Equal(0x08, GetOffset(in s, in s.Size)); + Assert.Equal(0x10, GetOffset(in s, in s.NameOffset)); + Assert.Equal(0x14, GetOffset(in s, in s.Reserved)); + } + + [Fact] + public static void PartitionFileSystemFormat_PartitionFileSystemHeaderImpl_Layout() + { + PartitionFileSystemFormat.PartitionFileSystemHeaderImpl s = default; + + Assert.Equal(0x10, Unsafe.SizeOf()); + + Assert.Equal(0x0, GetOffset(in s, in s.Signature[0])); + Assert.Equal(0x4, GetOffset(in s, in s.EntryCount)); + Assert.Equal(0x8, GetOffset(in s, in s.NameTableSize)); + Assert.Equal(0xC, GetOffset(in s, in s.Reserved)); + } + + [Fact] + public static void Sha256PartitionFileSystemFormat_PartitionEntry_Layout() + { + Sha256PartitionFileSystemFormat.PartitionEntry s = default; + + Assert.Equal(0x40, Unsafe.SizeOf()); + + Assert.Equal(0x00, GetOffset(in s, in s.Offset)); + Assert.Equal(0x08, GetOffset(in s, in s.Size)); + Assert.Equal(0x10, GetOffset(in s, in s.NameOffset)); + Assert.Equal(0x14, GetOffset(in s, in s.HashTargetSize)); + Assert.Equal(0x18, GetOffset(in s, in s.HashTargetOffset)); + Assert.Equal(0x20, GetOffset(in s, in s.Hash)); + } } \ No newline at end of file