diff --git a/FModel/PakReader/AESDecryptor.cs b/FModel/PakReader/AESDecryptor.cs index d59b9d10..8967ff93 100644 --- a/FModel/PakReader/AESDecryptor.cs +++ b/FModel/PakReader/AESDecryptor.cs @@ -7,6 +7,7 @@ namespace PakReader { static class AESDecryptor { + public const int ALIGN = 16; public const int BLOCK_SIZE = 16 * 8; // 128 static readonly Rijndael Cipher; static readonly Dictionary CachedTransforms = new Dictionary(); diff --git a/FModel/PakReader/Pak/IO/EIoChunkType.cs b/FModel/PakReader/Pak/IO/EIoChunkType.cs new file mode 100644 index 00000000..3a6ff2bf --- /dev/null +++ b/FModel/PakReader/Pak/IO/EIoChunkType.cs @@ -0,0 +1,20 @@ +namespace PakReader.Pak.IO +{ + /// + /// Addressable chunk types. + /// + public enum EIoChunkType : byte + { + Invalid, + InstallManifest, + ExportBundleData, + BulkData, + OptionalBulkData, + MemoryMappedBulkData, + LoaderGlobalMeta, + LoaderInitialLoadMeta, + LoaderGlobalNames, + LoaderGlobalNameHashes, + ContainerHeader + }; +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/EIoContainerFlags.cs b/FModel/PakReader/Pak/IO/EIoContainerFlags.cs new file mode 100644 index 00000000..a2319c23 --- /dev/null +++ b/FModel/PakReader/Pak/IO/EIoContainerFlags.cs @@ -0,0 +1,11 @@ +namespace PakReader.Pak.IO +{ + public enum EIoContainerFlags : byte + { + None, + Compressed = (1 << 0), + Encrypted = (1 << 1), + Signed = (1 << 2), + Indexed = (1 << 3), + }; +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FFileIoStoreContainerFile.cs b/FModel/PakReader/Pak/IO/FFileIoStoreContainerFile.cs new file mode 100644 index 00000000..b023ba0d --- /dev/null +++ b/FModel/PakReader/Pak/IO/FFileIoStoreContainerFile.cs @@ -0,0 +1,20 @@ +using System.IO; +using PakReader.Parsers.Objects; + +namespace PakReader.Pak.IO +{ + public struct FFileIoStoreContainerFile + { + public Stream FileHandle; + public string FileName; + public long CompressionBlockSize; + public string[] CompressionMethods; + public FIoStoreTocCompressedBlockEntry[] CompressionBlocks; + public FGuid EncryptionKeyGuid; + public byte[] EncryptionKey; + public EIoContainerFlags ContainerFlags; + public FSHAHash[] BlockSignatureHashes; + + public long FileSize => FileHandle.Length; + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FFileIoStoreReader.cs b/FModel/PakReader/Pak/IO/FFileIoStoreReader.cs new file mode 100644 index 00000000..82a9fb04 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FFileIoStoreReader.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using FModel.Utils; +using Ionic.Zlib; +using PakReader.Parsers; +using PakReader.Parsers.Objects; + +namespace PakReader.Pak.IO +{ + public class FFileIoStoreReader + { + public readonly FIoStoreTocResource TocResource; + public readonly Dictionary Toc; + public readonly FFileIoStoreContainerFile ContainerFile; + public readonly FIoContainerId ContainerId; + + private byte[] _aesKey; + public byte[] AesKey + { + get => _aesKey; + set + { + if (value != null && !TestAesKey(value)) //if value not null, test but fail, throw not working + throw new ArgumentException(string.Format(FModel.Properties.Resources.AesNotWorking, value.ToStringKey(), ContainerFile.FileName)); + _aesKey = value; // else, even if value is null, set it + // setting _aesKey to null will disable the corresponding menu item + } + } + + public FGuid EncryptionKeyGuid => ContainerFile.EncryptionKeyGuid; + public bool IsEncrypted => ContainerFile.ContainerFlags.HasAnyFlags(EIoContainerFlags.Encrypted); + + + public FIoDirectoryIndexResource _directoryIndex; + private byte[] _directoryIndexBuffer; + + public FFileIoStoreReader(Stream tocStream, Stream containerStream, EIoStoreTocReadOptions tocReadOptions = EIoStoreTocReadOptions.ReadDirectoryIndex) + { + ContainerFile.FileHandle = containerStream; + var tocResource = new FIoStoreTocResource(tocStream, tocReadOptions); + TocResource = tocResource; + + var containerUncompressedSize = tocResource.Header.TocCompressedBlockEntryCount > 0 + ? tocResource.Header.TocCompressedBlockEntryCount * tocResource.Header.CompressionBlockSize + : containerStream.Length; + + Toc = new Dictionary((int) tocResource.Header.TocEntryCount); + + for (var chunkIndex = 0; chunkIndex < tocResource.Header.TocEntryCount; chunkIndex++) + { + ref var chunkOffsetLength = ref tocResource.ChunkOffsetLengths[chunkIndex]; + if (chunkOffsetLength.Offset + chunkOffsetLength.Length > containerUncompressedSize) + { + throw new FileLoadException("TocEntry out of container bounds"); + } + Toc[tocResource.ChunkIds[chunkIndex]] = chunkOffsetLength; + } + + for (var compressedBlockIndex = 0; compressedBlockIndex < tocResource.CompressionBlocks.Length; compressedBlockIndex++) + { + ref var compressedBlockEntry = ref tocResource.CompressionBlocks[compressedBlockIndex]; + if (compressedBlockEntry.Offset + compressedBlockEntry.CompressedSize > ContainerFile.FileSize) + { + throw new FileLoadException("TocCompressedBlockEntry out of container bounds"); + } + } + + ContainerFile.CompressionMethods = tocResource.CompressionMethods; + ContainerFile.CompressionBlockSize = tocResource.Header.CompressionBlockSize; + ContainerFile.CompressionBlocks = tocResource.CompressionBlocks; + ContainerFile.ContainerFlags = tocResource.Header.ContainerFlags; + ContainerFile.EncryptionKeyGuid = tocResource.Header.EncryptionKeyGuid; + ContainerFile.BlockSignatureHashes = tocResource.ChunkBlockSignatures; + + ContainerId = tocResource.Header.ContainerId; + + _directoryIndexBuffer = tocResource.DirectoryIndexBuffer; + } + + public void ReadIndex() + { + using Stream indexStream = IsEncrypted + ? new MemoryStream(AESDecryptor.DecryptAES(_directoryIndexBuffer, _aesKey)) + : new MemoryStream(_directoryIndexBuffer); + _directoryIndex = new FIoDirectoryIndexResource(indexStream); + } + + public string MountPoint => _directoryIndex.MountPoint; + + public bool TestAesKey(byte[] key) + { + if (!IsEncrypted) + return true; + return TestAesKey(_directoryIndexBuffer, key); + } + + public static bool TestAesKey(byte[] bytes, byte[] key) + { + using BinaryReader indexReader = new BinaryReader(new MemoryStream(AESDecryptor.DecryptAES(bytes, key))); + var stringLen = indexReader.ReadInt32(); + if (stringLen > 128 || stringLen < -128) + { + return false; + } + if (stringLen == 0) + { + return indexReader.ReadUInt16() == 0; + } + if (stringLen < 0) + { + var nullTerminatedPos = 4 - (stringLen - 1) * 2; + indexReader.BaseStream.Seek(nullTerminatedPos, SeekOrigin.Begin); + return indexReader.ReadInt16() == 0; + } + else + { + var nullTerminatedPos = 4 + stringLen - 1; + indexReader.BaseStream.Seek(nullTerminatedPos, SeekOrigin.Begin); + return indexReader.ReadSByte() == 0; + } + } + + public byte[] Read(FIoChunkId chunkId) + { + var offsetAndLength = Toc[chunkId]; + var tocResource = TocResource; + var compressionBlockSize = tocResource.Header.CompressionBlockSize; + var dst = new byte[offsetAndLength.Length]; + var firstBlockIndex = (int) (offsetAndLength.Offset / compressionBlockSize); + var lastBlockIndex = (int) ((BinaryHelper.Align(offsetAndLength.Offset + dst.Length, compressionBlockSize) - 1) / compressionBlockSize); + var offsetInBlock = offsetAndLength.Offset % compressionBlockSize; + + byte[] src; + var remainingSize = dst.Length; + var dstOffset = 0; + for (int blockIndex = firstBlockIndex; blockIndex < lastBlockIndex; blockIndex++) + { + var compressionBlock = tocResource.CompressionBlocks[blockIndex]; + + var rawSize = BinaryHelper.Align(compressionBlock.CompressedSize, AESDecryptor.ALIGN); + var compressedBuffer = new byte[rawSize]; + + var uncompressedSize = compressionBlock.UncompressedSize; + var uncompressedBuffer = new byte[uncompressedSize]; + + var containerStream = ContainerFile.FileHandle; + containerStream.Position = compressionBlock.Offset; + containerStream.Read(compressedBuffer, 0, (int) rawSize); + if (TocResource.Header.ContainerFlags.HasAnyFlags(EIoContainerFlags.Encrypted)) + { + compressedBuffer = AESDecryptor.DecryptAES(compressedBuffer, _aesKey); + } + + if (compressionBlock.CompressionMethodIndex == 0) + { + src = compressedBuffer; + } + else + { + var compressionMethod = tocResource.CompressionMethods[compressionBlock.CompressionMethodIndex - 1]; + Decompress(compressedBuffer, uncompressedBuffer, compressionMethod); + src = uncompressedBuffer; + } + + var sizeInBlock = (int) Math.Min(compressionBlockSize - offsetInBlock, remainingSize); + Buffer.BlockCopy(src, (int) offsetInBlock, dst, dstOffset, sizeInBlock); + offsetInBlock = 0; + remainingSize -= sizeInBlock; + dstOffset += sizeInBlock; + } + + return dst; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FIoDirectoryIndexHandle GetChildDirectory(FIoDirectoryIndexHandle directory) => + directory.IsValid() && IsValidIndex() + ? FIoDirectoryIndexHandle.FromIndex(GetDirectoryEntry(directory).FirstChildEntry) + : FIoDirectoryIndexHandle.InvalidHandle; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FIoDirectoryIndexHandle GetNextDirectory(FIoDirectoryIndexHandle directory) => + directory.IsValid() && IsValidIndex() + ? FIoDirectoryIndexHandle.FromIndex(GetDirectoryEntry(directory).NextSiblingEntry) + : FIoDirectoryIndexHandle.InvalidHandle; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FIoDirectoryIndexHandle GetFile(FIoDirectoryIndexHandle directory) => + directory.IsValid() && IsValidIndex() + ? FIoDirectoryIndexHandle.FromIndex(GetDirectoryEntry(directory).FirstFileEntry) + : FIoDirectoryIndexHandle.InvalidHandle; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FIoDirectoryIndexHandle GetNextFile(FIoDirectoryIndexHandle file) => file.IsValid() && IsValidIndex() + ? FIoDirectoryIndexHandle.FromIndex(GetFileEntry(file).NextFileEntry) + : FIoDirectoryIndexHandle.InvalidHandle; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string GetDirectoryName(FIoDirectoryIndexHandle directory) + { + if (directory.IsValid() && IsValidIndex()) + { + var nameIndex = GetDirectoryEntry(directory).Name; + return _directoryIndex.StringTable[nameIndex]; + } + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string GetFileName(FIoDirectoryIndexHandle file) + { + if (file.IsValid() && IsValidIndex()) + { + var nameIndex = GetFileEntry(file).Name; + return _directoryIndex.StringTable[nameIndex]; + } + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint GetFileData(FIoDirectoryIndexHandle file) => file.IsValid() && IsValidIndex() + ? _directoryIndex.FileEntries[file.ToIndex()].UserData + : 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref FIoDirectoryIndexEntry GetDirectoryEntry(FIoDirectoryIndexHandle directory) => + ref _directoryIndex.DirectoryEntries[directory.ToIndex()]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref FIoFileIndexEntry GetFileEntry(FIoDirectoryIndexHandle file) => + ref _directoryIndex.FileEntries[file.ToIndex()]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsValidIndex() => _directoryIndex.DirectoryEntries.Length > 0; + + + private void Decompress(byte[] src, byte[] outData, string compressionMethod) + { + using var blockMs = new MemoryStream(src, false); + using Stream compressionStream = compressionMethod switch + { + "Zlib" => new ZlibStream(blockMs, CompressionMode.Decompress), + "Gzip" => new GZipStream(blockMs, CompressionMode.Decompress), + "Oodle" => new OodleStream(src, outData.Length), + _ => throw new NotImplementedException($"Decompression not yet implemented ({compressionMethod})") + }; + compressionStream.Read(outData, 0, outData.Length); + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoChunkHash.cs b/FModel/PakReader/Pak/IO/FIoChunkHash.cs new file mode 100644 index 00000000..a8b60537 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoChunkHash.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace PakReader.Pak.IO +{ + public struct FIoChunkHash + { + public byte[] Hash; + + public FIoChunkHash(BinaryReader reader) + { + Hash = reader.ReadBytes(32); + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoChunkId.cs b/FModel/PakReader/Pak/IO/FIoChunkId.cs new file mode 100644 index 00000000..2352442e --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoChunkId.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace PakReader.Pak.IO +{ + public readonly struct FIoChunkId + { + public readonly byte[] Id; + + public ulong ChunkId => BitConverter.ToUInt64(Id); + public ushort ChunkIndex => BitConverter.ToUInt16(Id, 8); + public EIoChunkType ChunkType => (EIoChunkType) Id[11]; + + public FIoChunkId(BinaryReader reader) + { + Id = reader.ReadBytes(12); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { + var hash = 5381; + for (int i = 0; i < 12; i++) + { + hash = hash * 33 + Id[i]; + } + + return hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (!(obj is FIoChunkId cast)) return false; + return Id.SequenceEqual(cast.Id); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(FIoChunkId a, FIoChunkId b) => a.Id.SequenceEqual(b.Id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(FIoChunkId a, FIoChunkId b) => !a.Id.SequenceEqual(b.Id); + + public override string ToString() => BitConverter.ToString(Id).Replace("-",""); + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoContainerId.cs b/FModel/PakReader/Pak/IO/FIoContainerId.cs new file mode 100644 index 00000000..ce41919d --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoContainerId.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace PakReader.Pak.IO +{ + public struct FIoContainerId + { + public ulong Id; + + public FIoContainerId(BinaryReader reader) + { + Id = reader.ReadUInt64(); + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoDirectoryIndexEntry.cs b/FModel/PakReader/Pak/IO/FIoDirectoryIndexEntry.cs new file mode 100644 index 00000000..7abe8f01 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoDirectoryIndexEntry.cs @@ -0,0 +1,20 @@ +using System.IO; + +namespace PakReader.Pak.IO +{ + public readonly struct FIoDirectoryIndexEntry + { + public readonly uint Name; + public readonly uint FirstChildEntry; + public readonly uint NextSiblingEntry; + public readonly uint FirstFileEntry; + + public FIoDirectoryIndexEntry(BinaryReader reader) + { + Name = reader.ReadUInt32(); + FirstChildEntry = reader.ReadUInt32(); + NextSiblingEntry = reader.ReadUInt32(); + FirstFileEntry = reader.ReadUInt32(); + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoDirectoryIndexHandle.cs b/FModel/PakReader/Pak/IO/FIoDirectoryIndexHandle.cs new file mode 100644 index 00000000..5ec4f972 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoDirectoryIndexHandle.cs @@ -0,0 +1,33 @@ +using System.Runtime.CompilerServices; + +namespace PakReader.Pak.IO +{ + public readonly struct FIoDirectoryIndexHandle + { + public static FIoDirectoryIndexHandle InvalidHandle = new FIoDirectoryIndexHandle(uint.MaxValue); + private readonly uint _handle; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ToIndex() => _handle; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FIoDirectoryIndexHandle(uint handle) + { + _handle = handle; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsValid() => this != InvalidHandle; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => (int) _handle; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(FIoDirectoryIndexHandle a, FIoDirectoryIndexHandle b) => a._handle == b._handle; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(FIoDirectoryIndexHandle a, FIoDirectoryIndexHandle b) => a._handle != b._handle; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FIoDirectoryIndexHandle FromIndex(uint index) => new FIoDirectoryIndexHandle(index); + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoDirectoryIndexResource.cs b/FModel/PakReader/Pak/IO/FIoDirectoryIndexResource.cs new file mode 100644 index 00000000..159c2c63 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoDirectoryIndexResource.cs @@ -0,0 +1,30 @@ +using System.IO; + +namespace PakReader.Pak.IO +{ + public class FIoDirectoryIndexResource + { + public readonly string MountPoint; + public readonly FIoDirectoryIndexEntry[] DirectoryEntries; + public readonly FIoFileIndexEntry[] FileEntries; + public readonly string[] StringTable; + + public FIoDirectoryIndexResource(Stream directoryIndexStream) + { + using var reader = new BinaryReader(directoryIndexStream); + MountPoint = reader.ReadFString(); + if (MountPoint.StartsWith("../../..")) + { + MountPoint = MountPoint[9..]; + } + else + { + // Weird mount point location... + MountPoint = ""; + } + DirectoryEntries = reader.ReadTArray(() => new FIoDirectoryIndexEntry(reader)); + FileEntries = reader.ReadTArray(() => new FIoFileIndexEntry(reader)); + StringTable = reader.ReadTArray(reader.ReadFString); + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoFileIndexEntry.cs b/FModel/PakReader/Pak/IO/FIoFileIndexEntry.cs new file mode 100644 index 00000000..76ccaa53 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoFileIndexEntry.cs @@ -0,0 +1,18 @@ +using System.IO; + +namespace PakReader.Pak.IO +{ + public readonly struct FIoFileIndexEntry + { + public readonly uint Name; + public readonly uint NextFileEntry; + public readonly uint UserData; + + public FIoFileIndexEntry(BinaryReader reader) + { + Name = reader.ReadUInt32(); + NextFileEntry = reader.ReadUInt32(); + UserData = reader.ReadUInt32(); + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoOffsetAndLength.cs b/FModel/PakReader/Pak/IO/FIoOffsetAndLength.cs new file mode 100644 index 00000000..a4ecad84 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoOffsetAndLength.cs @@ -0,0 +1,26 @@ +using System.IO; + +namespace PakReader.Pak.IO +{ + public readonly struct FIoOffsetAndLength + { + // We use 5 bytes for offset and size, this is enough to represent + // an offset and size of 1PB + public readonly byte[] OffsetAndLength; + public long Offset => OffsetAndLength[4] + | ((long) OffsetAndLength[3] << 8) + | ((long) OffsetAndLength[2] << 16) + | ((long) OffsetAndLength[1] << 24) + | ((long) OffsetAndLength[0] << 32); + public long Length => OffsetAndLength[9] + | ((long) OffsetAndLength[8] << 8) + | ((long) OffsetAndLength[7] << 16) + | ((long) OffsetAndLength[6] << 24) + | ((long) OffsetAndLength[5] << 32); + + public FIoOffsetAndLength(BinaryReader reader) + { + OffsetAndLength = reader.ReadBytes(5 + 5); + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoStoreTocCompressedBlockEntry.cs b/FModel/PakReader/Pak/IO/FIoStoreTocCompressedBlockEntry.cs new file mode 100644 index 00000000..eb01d933 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoStoreTocCompressedBlockEntry.cs @@ -0,0 +1,69 @@ +using System.IO; +using System.Runtime.CompilerServices; + +namespace PakReader.Pak.IO +{ + public readonly struct FIoStoreTocCompressedBlockEntry + { + private const int OffsetBits = 40; + private const ulong OffsetMask = (1ul << OffsetBits) - 1ul; + private const int SizeBits = 24; + private const uint SizeMask = (1 << SizeBits) - 1; + private const int SizeShift = 8; + + + /* 5 bytes offset, 3 bytes for size / uncompressed size and 1 byte for compresseion method. */ + public readonly byte[] Data; + + public long Offset + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + unsafe { fixed (byte* ptr = Data) { + var offset = (ulong*) ptr; + return (long) (*offset & OffsetMask); + } } + } + } + public uint CompressedSize + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + unsafe { fixed (byte* ptr = Data) { + var size = ((uint*) ptr) + 1; + return (*size >> SizeShift) & SizeMask; + } } + } + } + public uint UncompressedSize + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + unsafe { fixed (byte* ptr = Data) { + var uncompressedSize = ((uint*) ptr) + 2; + return *uncompressedSize & SizeMask; + } } + } + } + + public byte CompressionMethodIndex + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + unsafe { fixed (byte* ptr = Data) { + var index = ((uint*) ptr) + 2; + return (byte) (*index >> SizeBits); + } } + } + } + + public FIoStoreTocCompressedBlockEntry(BinaryReader reader) + { + Data = reader.ReadBytes(5 + 3 + 3 + 1); + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoStoreTocEntryMeta.cs b/FModel/PakReader/Pak/IO/FIoStoreTocEntryMeta.cs new file mode 100644 index 00000000..08e1ff85 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoStoreTocEntryMeta.cs @@ -0,0 +1,18 @@ +using System.IO; + +namespace PakReader.Pak.IO +{ + public readonly struct FIoStoreTocEntryMeta + { + public const int SIZE = 32 + 4; + + public readonly FIoChunkHash ChunkHash; + public readonly FIoStoreTocEntryMetaFlags Flags; + + public FIoStoreTocEntryMeta(BinaryReader reader) + { + ChunkHash = new FIoChunkHash(reader); + Flags = (FIoStoreTocEntryMetaFlags) reader.ReadByte(); + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoStoreTocEntryMetaFlags.cs b/FModel/PakReader/Pak/IO/FIoStoreTocEntryMetaFlags.cs new file mode 100644 index 00000000..cf4566c0 --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoStoreTocEntryMetaFlags.cs @@ -0,0 +1,9 @@ +namespace PakReader.Pak.IO +{ + public enum FIoStoreTocEntryMetaFlags : byte + { + None, + Compressed = (1 << 0), + MemoryMapped = (1 << 1) + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoStoreTocHeader.cs b/FModel/PakReader/Pak/IO/FIoStoreTocHeader.cs new file mode 100644 index 00000000..582abb6a --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoStoreTocHeader.cs @@ -0,0 +1,56 @@ +using System.IO; +using System.Linq; +using PakReader.Parsers.Objects; + +namespace PakReader.Pak.IO +{ + public enum EIoStoreTocVersion : byte + { + Invalid = 0, + Initial, + DirectoryIndex, + LatestPlusOne, + Latest = LatestPlusOne - 1 + } + + public class FIoStoreTocHeader + { + public const int SIZE = 144; + public static byte[] TOC_MAGIC = new byte[] + {0x2D, 0x3D, 0x3D, 0x2D, 0x2D, 0x3D, 0x3D, 0x2D, 0x2D, 0x3D, 0x3D, 0x2D, 0x2D, 0x3D, 0x3D, 0x2D}; + + public byte[] TocMagic; + public EIoStoreTocVersion Version; + public uint TocHeaderSize; + public uint TocEntryCount; + public uint TocCompressedBlockEntryCount; + public uint TocCompressedBlockEntrySize; // For sanity checking + public uint CompressionMethodNameCount; + public uint CompressionMethodNameLength; + public uint CompressionBlockSize; + public long DirectoryIndexSize; + public FIoContainerId ContainerId; + public FGuid EncryptionKeyGuid; + public EIoContainerFlags ContainerFlags; + + public FIoStoreTocHeader(BinaryReader reader) + { + TocMagic = reader.ReadBytes(16); + if (!TOC_MAGIC.SequenceEqual(TocMagic)) + throw new FileLoadException("Invalid utoc magic"); + Version = (EIoStoreTocVersion) reader.ReadInt32(); + TocHeaderSize = reader.ReadUInt32(); + TocEntryCount = reader.ReadUInt32(); + TocCompressedBlockEntryCount = reader.ReadUInt32(); + TocCompressedBlockEntrySize = reader.ReadUInt32(); + CompressionMethodNameCount = reader.ReadUInt32(); + CompressionMethodNameLength = reader.ReadUInt32(); + CompressionBlockSize = reader.ReadUInt32(); + DirectoryIndexSize = reader.ReadInt64(); + ContainerId = new FIoContainerId(reader); + EncryptionKeyGuid = new FGuid(reader); + ContainerFlags = (EIoContainerFlags) reader.ReadInt32(); + reader.BaseStream.Position += 60; // Padding + } + } +} \ No newline at end of file diff --git a/FModel/PakReader/Pak/IO/FIoStoreTocResource.cs b/FModel/PakReader/Pak/IO/FIoStoreTocResource.cs new file mode 100644 index 00000000..1c2a20db --- /dev/null +++ b/FModel/PakReader/Pak/IO/FIoStoreTocResource.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using FModel.Utils; +using PakReader.Parsers.Objects; + +namespace PakReader.Pak.IO +{ + public enum EIoStoreTocReadOptions + { + Default, + ReadDirectoryIndex = (1 << 0), + ReadTocMeta = (1 << 1), + ReadAll = ReadDirectoryIndex | ReadTocMeta + } + + public class FIoStoreTocResource + { + public readonly FIoStoreTocHeader Header; + public readonly FIoChunkId[] ChunkIds; + public readonly FIoOffsetAndLength[] ChunkOffsetLengths; + public readonly FIoStoreTocCompressedBlockEntry[] CompressionBlocks; + public readonly string[] CompressionMethods; + public readonly FSHAHash[] ChunkBlockSignatures; + public readonly byte[] DirectoryIndexBuffer; + public readonly FIoStoreTocEntryMeta[] ChunkMetas; + + public FIoStoreTocResource(Stream tocStream, EIoStoreTocReadOptions readOptions = EIoStoreTocReadOptions.Default) + { + using var reader = new BinaryReader(tocStream); + Header = new FIoStoreTocHeader(reader); + + var totalTocSize = tocStream.Length - FIoStoreTocHeader.SIZE; + var tocMetaSize = Header.TocEntryCount * FIoStoreTocEntryMeta.SIZE; + var defaultTocSize = totalTocSize - Header.DirectoryIndexSize - tocMetaSize; + + var tocSize = defaultTocSize; + if (readOptions.HasAnyFlags(EIoStoreTocReadOptions.ReadTocMeta)) + { + tocSize = totalTocSize; // Meta data is at the end of the TOC file + } + + if (readOptions.HasAnyFlags(EIoStoreTocReadOptions.ReadDirectoryIndex)) + { + tocSize = defaultTocSize + Header.DirectoryIndexSize; + } + + // Chunk IDs + ChunkIds = new FIoChunkId[Header.TocEntryCount]; + for (var i = 0; i < Header.TocEntryCount; i++) + { + ChunkIds[i] = new FIoChunkId(reader); + } + + // Chunk offsets + ChunkOffsetLengths = new FIoOffsetAndLength[Header.TocEntryCount]; + for (var i = 0; i < Header.TocEntryCount; i++) + { + ChunkOffsetLengths[i] = new FIoOffsetAndLength(reader); + } + + // Compression blocks + CompressionBlocks = new FIoStoreTocCompressedBlockEntry[Header.TocCompressedBlockEntryCount]; + for (var i = 0; i < Header.TocCompressedBlockEntryCount; i++) + { + CompressionBlocks[i] = new FIoStoreTocCompressedBlockEntry(reader); + } + + // Compression methods + CompressionMethods = new string[Header.CompressionMethodNameCount]; // Not doing +1 nor adding CompressionMethod none here since the FPakInfo implementation doesn't as well + for (var i = 0; i < Header.CompressionMethodNameCount; i++) + { + CompressionMethods[i] = Encoding.ASCII.GetString(reader.ReadBytes((int) Header.CompressionMethodNameLength)).TrimEnd('\0'); + } + + // Chunk block signatures + if (Header.ContainerFlags.HasAnyFlags(EIoContainerFlags.Signed)) + { + var hashSize = reader.ReadInt32(); + reader.BaseStream.Position += hashSize; // actually: var tocSignature = reader.ReadBytes(hashSize); + reader.BaseStream.Position += hashSize; // actually: var blockSignature = reader.ReadBytes(hashSize); + + ChunkBlockSignatures = new FSHAHash[Header.TocCompressedBlockEntryCount]; + for (var i = 0; i < Header.TocCompressedBlockEntryCount; i++) + { + ChunkBlockSignatures[i] = new FSHAHash(reader); + } + + // You could very hashes here but nah + } + + // Directory index + if (Header.Version >= EIoStoreTocVersion.DirectoryIndex && + readOptions.HasAnyFlags(EIoStoreTocReadOptions.ReadDirectoryIndex) && + Header.ContainerFlags.HasAnyFlags(EIoContainerFlags.Indexed) && + Header.DirectoryIndexSize > 0) + { + DirectoryIndexBuffer = reader.ReadBytes((int) Header.DirectoryIndexSize); + } + + // Meta + if (readOptions.HasAnyFlags(EIoStoreTocReadOptions.ReadTocMeta)) + { + ChunkMetas = new FIoStoreTocEntryMeta[Header.TocEntryCount]; + for (var i = 0; i < Header.TocEntryCount; i++) + { + ChunkMetas[i] = new FIoStoreTocEntryMeta(reader); + } + } + } + } +} \ No newline at end of file diff --git a/FModel/Utils/Enums.cs b/FModel/Utils/Enums.cs new file mode 100644 index 00000000..e1582173 --- /dev/null +++ b/FModel/Utils/Enums.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.CompilerServices; + +namespace FModel.Utils +{ + public static class Enums + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasAnyFlags(this T flags, T contains) where T : System.Enum, IConvertible + { + return (flags.ToInt32(null) & contains.ToInt32(null)) != 0; + } + } +} \ No newline at end of file