mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Implement PartitionFileSystem classes
This commit is contained in:
parent
a45c541aca
commit
a55b1d7c58
18 changed files with 1221 additions and 1006 deletions
|
@ -5,6 +5,8 @@ namespace LibHac.Crypto;
|
|||
|
||||
public class Sha256Generator : IHash
|
||||
{
|
||||
public const int HashSize = Sha256.DigestSize;
|
||||
|
||||
private Sha256Impl _baseHash;
|
||||
|
||||
public Sha256Generator()
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
366
src/LibHac/FsSystem/PartitionFileSystemMeta.cs
Normal file
366
src/LibHac/FsSystem/PartitionFileSystemMeta.cs
Normal 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> { }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue