Implement PartitionFileSystem classes

This commit is contained in:
Alex Barney 2023-01-03 11:00:58 -07:00
parent a45c541aca
commit a55b1d7c58
18 changed files with 1221 additions and 1006 deletions

View file

@ -5,6 +5,8 @@ namespace LibHac.Crypto;
public class Sha256Generator : IHash
{
public const int HashSize = Sha256.DigestSize;
private Sha256Impl _baseHash;
public Sha256Generator()

View file

@ -157,10 +157,9 @@ public abstract class IFile : IDisposable
return DoOperateRange(Span<byte>.Empty, operationId, offset, size, ReadOnlySpan<byte>.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);

View file

@ -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<IFileSystem> outFileSystem, ref SharedRef<IStorage> baseStorage)
{
using var partitionFs =
new SharedRef<PartitionFileSystemCore<StandardEntry>>(new PartitionFileSystemCore<StandardEntry>());
using var partitionFs = new SharedRef<PartitionFileSystem>(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);

View file

@ -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<StandardEntry>())
{
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);

View file

@ -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<byte> 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<byte> _signature;
public int EntryCount;
public int NameTableSize;
public uint Reserved;
public readonly ReadOnlySpan<byte> Signature
{
get
{
ReadOnlySpan<byte> 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<byte> 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<byte> 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;
}
}

View file

@ -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<DirectoryEntry> 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<byte> 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;
}
}

View file

@ -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<byte> 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<byte> 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<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> inBuffer)
{
return ResultFs.NotImplemented.Log();
}
protected override Result DoSetSize(long size)
{
if (!Mode.HasFlag(OpenMode.Write))
{
return ResultFs.WriteUnpermitted.Log();
}
return ResultFs.UnsupportedWriteForPartitionFile.Log();
}
}

View file

@ -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
/// <summary>
/// The allocator used by a <see cref="PartitionFileSystemCore{TMetaData,TFormat,THeader,TEntry}"/> when none is provided.
/// </summary>
/// <remarks><para>The original allocator in FS simply calls <c>nn::fs::detail::Allocate</c> and
/// <c>nn::fs::detail::Deallocate</c>. In our implementation we use the shared .NET <see cref="ArrayPool{T}"/>.</para>
/// <para>Based on nnSdk 15.3.0 (FS 15.0.0)</para></remarks>
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<string, PartitionFileEntry> 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<byte>.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<IDirectory> 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<byte>.Shared.Return(array);
}
else
{
throw new LibHacException("Buffer was not allocated by this MemoryResource.");
}
}
protected override bool DoIsEqual(MemoryResource other)
{
return ReferenceEquals(this, other);
}
}
/// <summary>
/// Reads a standard partition file system. These files start with "PFS0" and are typically found inside NCAs
/// or as .nsp files.
/// </summary>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
public class PartitionFileSystem : PartitionFileSystemCore<PartitionFileSystemMeta,
Impl.PartitionFileSystemFormat,
Impl.PartitionFileSystemFormat.PartitionFileSystemHeaderImpl,
Impl.PartitionFileSystemFormat.PartitionEntry> { }
/// <summary>
/// Reads a hashed partition file system. These files start with "HFS0" and are typically found inside XCIs.
/// </summary>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
public class Sha256PartitionFileSystem : PartitionFileSystemCore<Sha256PartitionFileSystemMeta,
Impl.Sha256PartitionFileSystemFormat,
Impl.PartitionFileSystemFormat.PartitionFileSystemHeaderImpl,
Impl.Sha256PartitionFileSystemFormat.PartitionEntry> { }
/// <summary>
/// Provides the base for an <see cref="IFileSystem"/> 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.
/// </summary>
/// <typeparam name="TMetaData">The type of the class used to read this file system's metadata.</typeparam>
/// <typeparam name="TFormat">A traits class that provides values used to read and build the metadata.</typeparam>
/// <typeparam name="THeader">The type of the header at the beginning of the metadata.</typeparam>
/// <typeparam name="TEntry">The type of the entries in the file table in the metadata.</typeparam>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
public class PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> : IFileSystem
where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
where TFormat : IPartitionFileSystemFormat
where THeader : unmanaged, IPartitionFileSystemHeader
where TEntry : unmanaged, IPartitionFileSystemEntry
{
private static ReadOnlySpan<byte> RootPath => "/"u8;
private IStorage _baseStorage;
private TMetaData _metaData;
private bool _isInitialized;
private long _metaDataSize;
private UniqueRef<TMetaData> _uniqueMetaData;
private SharedRef<IStorage> _sharedStorage;
/// <summary>
/// Provides access to a file from a <see cref="PartitionFileSystemCore{TMetaData,TFormat,THeader,TEntry}"/>.
/// </summary>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
private class PartitionFile : IFile
{
private TEntry _partitionEntry;
private readonly PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> _parent;
private readonly OpenMode _mode;
public PartitionFile(PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> parent, in TEntry partitionEntry, OpenMode mode)
{
_partitionEntry = partitionEntry;
_parent = parent;
_mode = mode;
}
protected override Result DoWrite(long offset, ReadOnlySpan<byte> 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<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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;
}
}
/// <summary>
/// Provides access to the root directory from a <see cref="PartitionFileSystemCore{TMetaData,TFormat,THeader,TEntry}"/>.
/// </summary>
/// <remarks><para>A <see cref="PartitionFileSystemCore{TMetaData,TFormat,THeader,TEntry}"/> cannot contain any
/// subdirectories, so a <see cref="PartitionDirectory"/> will only access the root directory.</para>
/// <para>Based on nnSdk 15.3.0 (FS 15.0.0)</para></remarks>
private class PartitionDirectory : IDirectory
{
private int _currentIndex;
private readonly PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> _parent;
private readonly OpenDirectoryMode _mode;
public PartitionDirectory(PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> parent, OpenDirectoryMode mode)
{
_currentIndex = 0;
_parent = parent;
_mode = mode;
}
protected override Result DoRead(out long entriesRead, Span<DirectoryEntry> 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<IStorage> baseStorage)
{
_sharedStorage.SetByCopy(in baseStorage);
return Initialize(_sharedStorage.Get).Ret();
}
public Result Initialize(in SharedRef<IStorage> 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<IFile> outFile, in Path path, OpenMode mode)
public Result Initialize(ref UniqueRef<TMetaData> metaData, in SharedRef<IStorage> 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<IStorage> 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<byte> 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<IFile> 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<PartitionFile>(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<IDirectory> outDirectory, in Path path, OpenDirectoryMode mode)
{
if (!_isInitialized)
return ResultFs.PreconditionViolation.Log();
if (!(path == RootPath))
return ResultFs.PathNotFound.Log();
using var directory = new UniqueRef<PartitionDirectory>(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();
}

View file

@ -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<T> : IFileSystem where T : unmanaged, IPartitionFileSystemEntry
{
private IStorage _baseStorage;
private PartitionFileSystemMetaCore<T> _metaData;
private bool _isInitialized;
private int _dataOffset;
private SharedRef<IStorage> _baseStorageShared;
public Result Initialize(ref SharedRef<IStorage> 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<T>();
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<IDirectory> 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<IFile> 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<byte> pathStr = path.GetString();
if (path.IsEmpty() || pathStr[0] != '/')
return ResultFs.InvalidPathFormat.Log();
ReadOnlySpan<byte> 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<T> ParentFs { get; }
private OpenMode Mode { get; }
private T _entry;
public PartitionFile(PartitionFileSystemCore<T> parentFs, ref T entry, OpenMode mode)
{
ParentFs = parentFs;
_entry = entry;
Mode = mode;
}
protected override Result DoRead(out long bytesRead, long offset, Span<byte> 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<T, HashedEntry>(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<T, HashedEntry>(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<byte> 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<byte> hashBuffer = stackalloc byte[hashBufferSize];
while (hashRemaining > 0)
{
int toRead = Math.Min(hashRemaining, hashBufferSize);
Span<byte> 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<byte> 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<byte> outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan<byte> 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<T> ParentFs { get; }
private int CurrentIndex { get; set; }
private OpenDirectoryMode Mode { get; }
public PartitionDirectory(PartitionFileSystemCore<T> parentFs, OpenDirectoryMode mode)
{
ParentFs = parentFs;
CurrentIndex = 0;
Mode = mode;
}
protected override Result DoRead(out long entriesRead, Span<DirectoryEntry> 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;
}
}
}

View file

@ -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
{
/// <summary>
/// Contains values used by <see cref="PartitionFileSystemMetaCore{TFormat,THeader,TEntry}"/> for reading
/// and building the metadata of a partition file system.
/// </summary>
public interface IPartitionFileSystemFormat
{
/// <summary>The signature bytes that are expected to be at the start of the partition file system.</summary>
static abstract ReadOnlySpan<byte> VersionSignature { get; }
/// <summary>The maximum length of file names inside the partition file system.</summary>
static abstract uint EntryNameLengthMax { get; }
/// <summary>The alignment that the start of the data for each file must be aligned to.</summary>
static abstract uint FileDataAlignmentSize { get; }
/// <summary>The <see cref="Result"/> returned when the <see cref="VersionSignature"/> is incorrect.</summary>
static abstract Result ResultSignatureVerificationFailed { get; }
}
/// <summary>
/// The minimum fields needed for the file entry type in a <see cref="PartitionFileSystemMetaCore{TFormat,THeader,TEntry}"/>.
/// </summary>
public interface IPartitionFileSystemEntry
{
long Offset { get; }
long Size { get; }
int NameOffset { get; }
}
/// <summary>
/// The minimum fields needed for the header type in a <see cref="PartitionFileSystemMetaCore{TFormat,THeader,TEntry}"/>.
/// </summary>
public interface IPartitionFileSystemHeader
{
ReadOnlySpan<byte> Signature { get; }
int EntryCount { get; }
int NameTableSize { get; }
}
/// <summary>
/// Reads the metadata from a <see cref="PartitionFileSystemCore{TMetaData,TFormat,THeader,TEntry}"/>.
/// The metadata has three sections: A single struct of type <typeparamref name="TFormat"/>, a table of
/// <typeparamref name="TEntry"/> structs containing info on each file, and a table of the names of all the files.
/// </summary>
/// <typeparam name="TFormat">A traits class that provides values used to read and build the metadata.</typeparam>
/// <typeparam name="THeader">The type of the header at the beginning of the metadata.</typeparam>
/// <typeparam name="TEntry">The type of the entries in the file table in the metadata.</typeparam>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
public class PartitionFileSystemMetaCore<TFormat, THeader, TEntry> : 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<THeader>());
private ReadOnlySpan<TEntry> Entries => EntryBuffer.GetSpan<TEntry>();
private ReadOnlySpan<byte> 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<byte> metaSpan = metaBuffer.Span.Slice(0, metaDataSize);
// Validate size for header.
if (metaDataSize < Unsafe.SizeOf<THeader>())
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<THeader>());
Span<byte> headerSpan = HeaderBuffer.Span;
ref readonly THeader header = ref Unsafe.As<byte, THeader>(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<TEntry>();
// 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<THeader>() + entriesSize + header.NameTableSize)
return ResultFs.InvalidSize.Log();
// Setup entries and name table.
EntryBuffer = metaBuffer.GetSegment(Unsafe.SizeOf<THeader>(), entriesSize);
NameTableBuffer = metaBuffer.GetSegment(Unsafe.SizeOf<THeader>() + entriesSize, header.NameTableSize);
// Read entries and name table.
Span<byte> destSpan = metaSpan.Slice(Unsafe.SizeOf<THeader>(), entriesSize + header.NameTableSize);
res = baseStorage.Read(Unsafe.SizeOf<THeader>(), 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;
}
/// <summary>
/// Queries the size of the metadata by reading the metadata header from the provided storage
/// </summary>
/// <param name="outSize">If the operation returns successfully, contains the size of the metadata.</param>
/// <param name="storage">The <see cref="IStorage"/> containing the metadata.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="IPartitionFileSystemFormat.ResultSignatureVerificationFailed"/>: The header doesn't have
/// the correct file signature.</returns>
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;
}
/// <summary>
/// Queries the size of the metadata with the provided header.
/// </summary>
/// <param name="outSize">If the operation returns successfully, contains the size of the metadata.</param>
/// <param name="header">The metadata header.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="IPartitionFileSystemFormat.ResultSignatureVerificationFailed"/>: The header doesn't have
/// the correct file signature.</returns>
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<THeader>() + header.EntryCount * Unsafe.SizeOf<TEntry>() + header.NameTableSize;
return Result.Success;
}
/// <summary>
/// Returns the size of the meta data header.
/// </summary>
/// <returns>The size of <typeparamref name="THeader"/>.</returns>
public static int GetHeaderSize()
{
return Unsafe.SizeOf<THeader>();
}
public int GetMetaDataSize()
{
return (int)MetaDataSize;
}
public int GetEntryIndex(U8Span entryName)
{
if (!IsInitialized)
return Result.ConvertResultToReturnType<int>(ResultFs.PreconditionViolation.Value);
ref readonly THeader header = ref Header;
ReadOnlySpan<TEntry> entries = Entries;
ReadOnlySpan<byte> nameTable = NameTable;
for (int i = 0; i < header.EntryCount; i++)
{
if (entries[i].NameOffset >= header.NameTableSize)
return Result.ConvertResultToReturnType<int>(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<int>(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;
/// <summary>
/// Reads the metadata for a <see cref="Sha256PartitionFileSystem"/>.
/// </summary>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
public class Sha256PartitionFileSystemMeta : PartitionFileSystemMetaCore<TFormat, THeader, TFormat.PartitionEntry>
{
public Result Initialize(IStorage baseStorage, MemoryResource allocator, ReadOnlySpan<byte> 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<byte> hash, Optional<byte> 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<byte> metaDataSpan = MetaDataBuffer.Span.Slice(0, (int)MetaDataSize);
res = baseStorage.Read(offset: 0, metaDataSpan);
if (res.IsFailure()) return res.Miss();
Span<byte> 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<THeader>());
Span<byte> headerSpan = HeaderBuffer.Span;
ref readonly THeader header = ref Unsafe.As<byte, THeader>(ref MemoryMarshal.GetReference(headerSpan));
if (!CryptoUtil.IsSameBytes(headerSpan, TFormat.VersionSignature, TFormat.VersionSignature.Length))
return TFormat.ResultSignatureVerificationFailed.Log();
int entriesSize = header.EntryCount * Unsafe.SizeOf<TFormat.PartitionEntry>();
// Validate size for header + entries + name table.
if (MetaDataSize < Unsafe.SizeOf<THeader>() + entriesSize + header.NameTableSize)
return ResultFs.InvalidSha256PartitionMetaDataSize.Log();
// Setup entries and name table.
EntryBuffer = MetaDataBuffer.GetSegment(Unsafe.SizeOf<THeader>(), entriesSize);
NameTableBuffer = MetaDataBuffer.GetSegment(Unsafe.SizeOf<THeader>() + entriesSize, header.NameTableSize);
// Mark as initialized.
IsInitialized = true;
return Result.Success;
}
}
/// <summary>
/// Reads the metadata for a <see cref="PartitionFileSystem"/>.
/// </summary>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
public class PartitionFileSystemMeta : PartitionFileSystemMetaCore<PartitionFileSystemFormat,
PartitionFileSystemFormat.PartitionFileSystemHeaderImpl, PartitionFileSystemFormat.PartitionEntry> { }
}

View file

@ -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<T> where T : unmanaged, IPartitionFileSystemEntry
{
private static int HeaderSize => Unsafe.SizeOf<Header>();
private static int EntrySize => Unsafe.SizeOf<T>();
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<byte> 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<byte, Header>(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<T> entries = GetEntries();
ReadOnlySpan<byte> names = GetStringTable();
for (int i = 0; i < entries.Length; i++)
{
if (stringTableSize <= entries[i].NameOffset)
{
throw new HorizonResultException(ResultFs.InvalidPartitionEntryOffset.Log());
}
ReadOnlySpan<byte> 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<byte> 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<T> GetEntries()
{
Debug.Assert(IsInitialized);
Debug.Assert(Buffer.Length >= HeaderSize + EntryCount * EntrySize);
Span<byte> entryBuffer = Buffer.AsSpan(HeaderSize, EntryCount * EntrySize);
return MemoryMarshal.Cast<byte, T>(entryBuffer);
}
private ReadOnlySpan<byte> 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;
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace LibHac.Mem;
@ -38,6 +39,25 @@ public struct Buffer : IEquatable<Buffer>
Extra = extra;
}
/// <summary>
/// Forms a <see cref="BufferSegment"/> out of the current <see cref="Buffer"/> that begins at a specified index.
/// </summary>
/// <param name="start">The index at which to begin the segment.</param>
/// <returns><para>A <see cref="BufferSegment"/> that contains all elements of the current <see cref="Buffer"/> instance
/// from <paramref name="start"/> to the end of the instance.</para>
/// <para> The <see cref="BufferSegment"/> must not be accessed after this parent <see cref="Buffer"/> is deallocated.</para></returns>
internal BufferSegment GetSegment(int start) => new BufferSegment(_memory.Slice(start));
/// <summary>
/// Forms a <see cref="BufferSegment"/> out of the current <see cref="Buffer"/> starting at a specified index for a specified length.
/// </summary>
/// <param name="start">The index at which to begin the segment.</param>
/// <param name="length">The number of elements to include in the segment.</param>
/// <returns><para>A <see cref="BufferSegment"/> that contains <paramref name="length"/> elements from the current
/// <see cref="Buffer"/> instance starting at <paramref name="start"/>.</para>
/// <para> The <see cref="BufferSegment"/> must not be accessed after this parent <see cref="Buffer"/> is deallocated.</para></returns>
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<Buffer>
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();
}
/// <summary>
/// Represents a region of memory borrowed from a <see cref="Buffer"/>.
/// This <see cref="BufferSegment"/> must not be accessed after the parent <see cref="Buffer"/> that created it is deallocated.
/// </summary>
public readonly struct BufferSegment
{
private readonly Memory<byte> _memory;
public BufferSegment(Memory<byte> memory)
{
_memory = memory;
}
/// <summary>
/// The length of the buffer in bytes.
/// </summary>
public int Length => _memory.Length;
/// <summary>
/// Gets a <see cref="Span{T}"/> from the <see cref="BufferSegment"/>.
/// </summary>
public Span<byte> Span => _memory.Span;
/// <summary>
/// Gets a <see cref="Span{T}"/> from the <see cref="BufferSegment"/> of the specified type.
/// </summary>
public Span<T> GetSpan<T>() where T : unmanaged => MemoryMarshal.Cast<byte, T>(Span);
/// <summary>
/// Returns <see langword="true"/> if the <see cref="BufferSegment"/> is not valid.
/// </summary>
public bool IsNull => _memory.IsEmpty;
}

View file

@ -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<IFile>();
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();
}
}

View file

@ -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:

View file

@ -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<PartitionFileSystemFormat.PartitionEntry>();
case PartitionFileSystemType.Hashed:
return Unsafe.SizeOf<Sha256PartitionFileSystemFormat.PartitionEntry>();
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
private class Entry
{

View file

@ -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<PartitionFileSystem> pfs = new UniqueRef<PartitionFileSystem>();
using UniqueRef<Sha256PartitionFileSystem> hfs = new UniqueRef<Sha256PartitionFileSystem>();
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<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> pfs)
where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, 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<IDirectory>())
{
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<DirectoryEntry>(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();

View file

@ -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<IFile>();
root.OpenFile(ref subPfsFile.Ref, sub.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
using var subPfs = new UniqueRef<Sha256PartitionFileSystem>(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<IDirectory>();
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<DirectoryEntry>(ref dirEntry)).ThrowIfFailure();
if (entriesRead == 0)
break;
string label = isFirstFile ? "Files:" : "";
string data = $"{partitionName}:/{StringUtils.Utf8ZToString(dirEntry.Name)}";
sb.PrintItem(label, data);
isFirstFile = false;
}
}
}

View file

@ -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<PartitionFileSystemFormat.PartitionEntry>());
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<PartitionFileSystemFormat.PartitionFileSystemHeaderImpl>());
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<Sha256PartitionFileSystemFormat.PartitionEntry>());
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));
}
}