Add PartitionFileSystemCore

This commit is contained in:
Alex Barney 2019-12-06 20:30:12 -06:00
parent 0c4aad32a0
commit d08e6b060c
8 changed files with 714 additions and 4 deletions

View file

@ -65,4 +65,63 @@ namespace LibHac.Common
return Bytes.ToHexString();
}
}
[DebuggerDisplay("{ToString()}")]
[StructLayout(LayoutKind.Sequential, Size = 32)]
public struct Buffer32
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy0;
[DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy1;
[DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy2;
[DebuggerBrowsable(DebuggerBrowsableState.Never)] private ulong _dummy3;
public byte this[int i]
{
get => Bytes[i];
set => Bytes[i] = value;
}
public Span<byte> Bytes => SpanHelpers.AsByteSpan(ref this);
// Prevent a defensive copy by changing the read-only in reference to a reference with Unsafe.AsRef()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Span<byte>(in Buffer32 value)
{
return SpanHelpers.AsByteSpan(ref Unsafe.AsRef(in value));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator ReadOnlySpan<byte>(in Buffer32 value)
{
return SpanHelpers.AsReadOnlyByteSpan(ref Unsafe.AsRef(in value));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T As<T>() where T : unmanaged
{
if (Unsafe.SizeOf<T>() > (uint)Unsafe.SizeOf<Buffer32>())
{
throw new ArgumentException();
}
return ref MemoryMarshal.GetReference(AsSpan<T>());
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<T> AsSpan<T>() where T : unmanaged
{
return SpanHelpers.AsSpan<Buffer32, T>(ref this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ReadOnlySpan<T> AsReadOnlySpan<T>() where T : unmanaged
{
return SpanHelpers.AsReadOnlySpan<Buffer32, T>(ref Unsafe.AsRef(in this));
}
public override string ToString()
{
return Bytes.ToHexString();
}
}
}

View file

@ -0,0 +1,30 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace LibHac.Crypto
{
internal static class CryptoUtil
{
public static bool IsSameBytes(ReadOnlySpan<byte> buffer1, ReadOnlySpan<byte> buffer2, int length)
{
if (buffer1.Length < (uint)length || buffer2.Length < (uint)length)
throw new ArgumentOutOfRangeException(nameof(length));
return IsSameBytes(ref MemoryMarshal.GetReference(buffer1), ref MemoryMarshal.GetReference(buffer2), length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsSameBytes(ref byte p1, ref byte p2, int length)
{
int result = 0;
for (int i = 0; i < length; i++)
{
result |= Unsafe.Add(ref p1, i) ^ Unsafe.Add(ref p2, i);
}
return result == 0;
}
}
}

View file

@ -58,8 +58,11 @@
public static Result InvalidHashInIvfc => new Result(ModuleFs, 4604);
public static Result IvfcHashIsEmpty => new Result(ModuleFs, 4612);
public static Result InvalidHashInIvfcTopLayer => new Result(ModuleFs, 4613);
public static Result InvalidPartitionFileSystemHashOffset => new Result(ModuleFs, 4642);
public static Result InvalidPartitionFileSystemHash => new Result(ModuleFs, 4643);
public static Result InvalidPartitionFileSystemMagic => new Result(ModuleFs, 4644);
public static Result InvalidHashedPartitionFileSystemMagic => new Result(ModuleFs, 4645);
public static Result InvalidPartitionFileSystemEntryNameOffset => new Result(ModuleFs, 4646);
public static Result Result4662 => new Result(ModuleFs, 4662);
public static Result SaveDataAllocationTableCorruptedInternal => new Result(ModuleFs, 4722);
@ -127,6 +130,7 @@
public static Result UnsupportedOperationModifyReadOnlyFile => new Result(ModuleFs, 6372);
public static Result UnsupportedOperationModifyPartitionFileSystem => new Result(ModuleFs, 6374);
public static Result UnsupportedOperationInPartitionFileSetSize => new Result(ModuleFs, 6376);
public static Result UnsupportedOperationIdInPartitionFileSystem => new Result(ModuleFs, 6377);
public static Result PermissionDenied => new Result(ModuleFs, 6400);
public static Result ExternalKeyAlreadyRegistered => new Result(ModuleFs, 6452);

View file

@ -1,5 +1,6 @@
using System;
using LibHac.Fs;
using LibHac.Fs;
using LibHac.FsSystem;
using LibHac.FsSystem.Detail;
namespace LibHac.FsService.Creators
{
@ -7,7 +8,17 @@ namespace LibHac.FsService.Creators
{
public Result Create(out IFileSystem fileSystem, IStorage pFsStorage)
{
throw new NotImplementedException();
var partitionFs = new PartitionFileSystemCore<StandardEntry>();
Result rc = partitionFs.Initialize(pFsStorage);
if (rc.IsFailure())
{
fileSystem = default;
return rc;
}
fileSystem = partitionFs;
return Result.Success;
}
}
}

View file

@ -0,0 +1,39 @@
using System.Runtime.InteropServices;
using LibHac.Common;
namespace LibHac.FsSystem.Detail
{
public interface IPartitionFileSystemEntry
{
long Offset { get; }
long Size { get; }
int NameOffset { get; }
}
[StructLayout(LayoutKind.Sequential, Size = 0x18)]
public struct StandardEntry : IPartitionFileSystemEntry
{
public long Offset;
public long Size;
public int NameOffset;
long IPartitionFileSystemEntry.Offset => Offset;
long IPartitionFileSystemEntry.Size => Size;
int IPartitionFileSystemEntry.NameOffset => NameOffset;
}
[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;
}
}

View file

@ -0,0 +1,373 @@
using System;
using System.Runtime.CompilerServices;
using LibHac.Common;
using LibHac.Crypto;
using LibHac.Fs;
using LibHac.FsSystem.Detail;
namespace LibHac.FsSystem
{
public class PartitionFileSystemCore<T> : FileSystemBase where T : unmanaged, IPartitionFileSystemEntry
{
private IStorage BaseStorage { get; set; }
private PartitionFileSystemMetaCore<T> MetaData { get; set; }
private bool IsInitialized { get; set; }
private int DataOffset { get; set; }
public Result Initialize(IStorage baseStorage)
{
if (IsInitialized)
return ResultFs.PreconditionViolation.Log();
MetaData = new PartitionFileSystemMetaCore<T>();
Result rc = MetaData.Initialize(baseStorage);
if (rc.IsFailure()) return rc;
BaseStorage = baseStorage;
DataOffset = MetaData.Size;
IsInitialized = true;
return Result.Success;
}
protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode)
{
directory = default;
if (!IsInitialized)
return ResultFs.PreconditionViolation.Log();
ReadOnlySpan<byte> rootPath = new[] { (byte)'/' };
if (StringUtils.Compare(rootPath, path.ToU8Span(), 2) != 0)
return ResultFs.PathNotFound.Log();
directory = new PartitionDirectory(this, mode);
return Result.Success;
}
protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode)
{
file = default;
if (!IsInitialized)
return ResultFs.PreconditionViolation.Log();
if (!mode.HasFlag(OpenMode.Read) && !mode.HasFlag(OpenMode.Write))
return ResultFs.InvalidArgument.Log();
int entryIndex = MetaData.FindEntry(path.ToU8Span().Slice(1));
if (entryIndex < 0) return ResultFs.PathNotFound.Log();
ref T entry = ref MetaData.GetEntry(entryIndex);
file = new PartitionFile(this, ref entry, mode);
return Result.Success;
}
protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path)
{
entryType = default;
if (!IsInitialized)
return ResultFs.PreconditionViolation.Log();
if (string.IsNullOrEmpty(path) || path[0] != '/')
return ResultFs.InvalidPathFormat.Log();
ReadOnlySpan<byte> rootPath = new[] { (byte)'/' };
if (StringUtils.Compare(rootPath, path.ToU8Span(), 2) == 0)
{
entryType = DirectoryEntryType.Directory;
return Result.Success;
}
if (MetaData.FindEntry(path.ToU8Span().Slice(1)) >= 0)
{
entryType = DirectoryEntryType.File;
return Result.Success;
}
return ResultFs.PathNotFound.Log();
}
protected override Result CommitImpl()
{
return Result.Success;
}
protected override Result CreateDirectoryImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log();
protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log();
protected override Result DeleteDirectoryImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log();
protected override Result DeleteDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log();
protected override Result CleanDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log();
protected override Result DeleteFileImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log();
protected override Result RenameDirectoryImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log();
protected override Result RenameFileImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log();
private class PartitionFile : FileBase
{
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 ReadImpl(out long bytesRead, long offset, Span<byte> destination, ReadOption options)
{
bytesRead = default;
Result rc = ValidateReadParams(out long bytesToRead, offset, destination.Length, Mode);
if (rc.IsFailure()) return rc;
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)
{
rc = 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.InvalidPartitionFileSystemHashOffset.Log();
long storageOffset = fileStorageOffset + offset;
// Nintendo checks for overflow here but not in other places for some reason
if (storageOffset < 0)
return ResultFs.ValueOutOfRange.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)
{
rc = ParentFs.BaseStorage.Read(storageOffset, destination.Slice(0, (int)bytesToRead));
if (rc.IsFailure()) return rc;
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.InvalidPartitionFileSystemHashOffset.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);
rc = ParentFs.BaseStorage.Read(readPos, hashBufferSliced);
if (rc.IsFailure()) return rc;
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.InvalidPartitionFileSystemHash.Log();
}
rc = Result.Success;
}
if (rc.IsSuccess())
bytesRead = bytesToRead;
return rc;
}
protected override Result WriteImpl(long offset, ReadOnlySpan<byte> source, WriteOption options)
{
Result rc = ValidateWriteParams(offset, source.Length, Mode, out bool isResizeNeeded);
if (rc.IsFailure()) return rc;
if (isResizeNeeded)
return ResultFs.UnsupportedOperationInPartitionFileSetSize.Log();
if (_entry.Size < offset)
return ResultFs.ValueOutOfRange.Log();
if (_entry.Size < source.Length + offset)
return ResultFs.InvalidSize.Log();
return ParentFs.BaseStorage.Write(ParentFs.DataOffset + _entry.Offset + offset, source);
}
protected override Result FlushImpl()
{
if (Mode.HasFlag(OpenMode.Write))
{
return ParentFs.BaseStorage.Flush();
}
return Result.Success;
}
protected override Result SetSizeImpl(long size)
{
if (Mode.HasFlag(OpenMode.Write))
{
return ResultFs.UnsupportedOperationInPartitionFileSetSize.Log();
}
return ResultFs.InvalidOpenModeForWrite.Log();
}
protected override Result GetSizeImpl(out long size)
{
size = _entry.Size;
return Result.Success;
}
protected override Result OperateRangeImpl(Span<byte> outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan<byte> inBuffer)
{
switch (operationId)
{
case OperationId.InvalidateCache:
if (!Mode.HasFlag(OpenMode.Read))
return ResultFs.InvalidOpenModeForRead.Log();
if (Mode.HasFlag(OpenMode.Write))
return ResultFs.UnsupportedOperationIdInPartitionFileSystem.Log();
break;
case OperationId.QueryRange:
break;
default:
return ResultFs.UnsupportedOperationIdInPartitionFileSystem.Log();
}
if (offset < 0 || offset > _entry.Size)
return ResultFs.ValueOutOfRange.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;
}
public Result Read(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, name);
entryBuffer[i].Name[FsPath.MaxLength] = 0;
CurrentIndex++;
}
entriesRead = toReadCount;
}
else
{
entriesRead = 0;
}
return Result.Success;
}
public Result GetEntryCount(out long entryCount)
{
if (Mode.HasFlag(OpenDirectoryMode.File))
{
entryCount = ParentFs.MetaData.GetEntryCount();
}
else
{
entryCount = 0;
}
return Result.Success;
}
}
}
}

View file

@ -0,0 +1,194 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using LibHac.Common;
using LibHac.Fs;
using LibHac.FsSystem.Detail;
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 rc = baseStorage.Read(0, SpanHelpers.AsByteSpan(ref header));
if (rc.IsFailure()) return rc;
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 rc = baseStorage.Read(0, buffer.Slice(0, HeaderSize));
if (rc.IsFailure()) return rc;
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();
rc = baseStorage.Read(entryTableOffset,
buffer.Slice(entryTableOffset, entryTableSize + StringTableSize));
if (rc.IsSuccess())
{
IsInitialized = true;
}
return rc;
}
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.InvalidPartitionFileSystemEntryNameOffset.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.InvalidPartitionFileSystemEntryNameOffset.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.InvalidPartitionFileSystemMagic;
}
if (typeof(T) == typeof(HashedEntry))
{
return ResultFs.InvalidHashedPartitionFileSystemMagic;
}
throw new NotSupportedException();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint GetMagicValue()
{
if (typeof(T) == typeof(StandardEntry))
{
return 0x30534650; // PFS0
}
if (typeof(T) == typeof(HashedEntry))
{
return 0x30534648; // HFS0
}
throw new NotSupportedException();
}
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
private struct Header
{
public uint Magic;
public int EntryCount;
public int StringTableSize;
}
}
}

View file

@ -35,7 +35,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19554-01" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19554-01" PrivateAssets="All" />
</ItemGroup>
</Project>