diff --git a/README.md b/README.md index 3d2f2d70..ef8ff007 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | UEFI Boot Entry | | [`patterns/uefi_boot_entry.hexpat`](patterns/uefi_boot_entry.hexpat) | UEFI Boot Entry (Load option) | | UEFI Variable Store | | [`patterns/uefi_fv_varstore.hexpat`](patterns/uefi_fv_varstore.hexpat) | UEFI Firmware Volume Variable Store | | UF2 | | [`patterns/uf2.hexpat`](patterns/uf2.hexpat) | [USB Flashing Format](https://github.com/microsoft/uf2) | +| Unity Asset Bundle | | [`patterns/unity-asset-bundle.hexpat`](patterns/unity-asset-bundle.hexpat) | Unity Asset Bundle | | Valve VPK | | [`patterns/valve_vpk.hexpat`](valve_vpk.hexpat) | Valve Package File | | VBMeta | | [`patterns/vbmeta.hexpat`](patterns/vbmeta.hexpat) | Android VBMeta image | | VDF | | [`patterns/vdf.hexpat`](patterns/vdf.hexpat) | Binary Value Data Format (.vdf) files | diff --git a/patterns/unity-asset-bundle.hexpat b/patterns/unity-asset-bundle.hexpat new file mode 100644 index 00000000..2ef7e09a --- /dev/null +++ b/patterns/unity-asset-bundle.hexpat @@ -0,0 +1,177 @@ +#pragma author Khoo Hao Yit +#pragma description Unity Asset Bundle + +#pragma magic [ "UnityWeb\0" ] @ 0x00 +#pragma magic [ "UnityRaw\0" ] @ 0x00 +#pragma magic [ "UnityArchive\0" ] @ 0x00 +#pragma magic [ "UnityFS\0" ] @ 0x00 +#pragma endian big + +// Reference: +// https://archive.vg-resource.com/thread-43269.html +// https://github.com/K0lb3/UnityPy +// https://github.com/Perfare/AssetStudio +// https://docs.unity3d.com/550/Documentation/Manual/AssetBundleInternalStructure.html + +import std.core; +import std.sys; +import hex.dec; + +enum Compression: u8 { + None = 0, + Lzma = 1, + Lz4 = 2, + Lz4hc = 3, + Lzham = 4, +}; + +enum Flag : u32 { + CompressionMask = 0b0000111111, + BlocksAndDirectoryInfoCombined = 0b0001000000, + BlockInfoAtTheEnd = 0b0010000000, + OldWebPluginCompatibility = 0b0100000000, + BlockInfoNeedPaddingAtStart = 0b1000000000, +}; + +std::mem::Section blockData; +auto cursor = -1; + +namespace v1 { + struct Level { + u32 compressedSize; + u32 decompressedSize; + }; + + struct DirectoryRecord { + char path[]; + u32 offset; + u32 size; + + std::mem::Bytes data @ offset in blockData; + }; + + struct Header { + u32 nodeCount; + v1::DirectoryRecord nodes[nodeCount]; + }; +} + +namespace v2 { + struct BlockInfo { + u32 decompressedSize; + u32 compressedSize; + u16 flags; + + u8 compressedData[compressedSize] @ cursor in 0 [[sealed, name(std::format("blockInfo{:d}", std::core::array_index()))]]; + cursor += compressedSize; + + std::mem::Section decompressedData = std::mem::create_section("decompressedData"); + match (flags & Flag::CompressionMask) { + (_): std::unimplemented(); + (Compression::None): std::mem::copy_value_to_section(compressedData, decompressedData, 0); + (Compression::Lzma): hex::dec::lzma_decompress(compressedData, decompressedData); + (Compression::Lz4): hex::dec::lz4_decompress(compressedData, decompressedData, false); + (Compression::Lz4hc): hex::dec::lz4_decompress(compressedData, decompressedData, false); + (Compression::Lzham): std::unimplemented(); + } + std::mem::copy_section_to_section( + decompressedData, + 0, + blockData, + std::mem::get_section_size(blockData), + std::mem::get_section_size(decompressedData) + ); + std::mem::delete_section(decompressedData); + }; + + struct DirectoryRecord { + s64 offset; + s64 size; + u32 flags; + char path[]; + + std::mem::Bytes data @ offset in blockData; + }; + + struct Header { + blockData = std::mem::create_section("BlockData"); + std::mem::Bytes<16> dataHash; + s32 blockInfoCount; + v2::BlockInfo blockInfos[blockInfoCount]; + u32 nodeCount; + v2::DirectoryRecord nodes[nodeCount]; + }; +} + +struct AssetBundle { + char signature[]; + u32 version; + char unityVersion[]; + char unityRevision[]; + if (signature == "UnityArchive\0") { + std::unimplemented(); + } + if (signature != "UnityFS\0" && version != 6) { + if (version >= 4) { + std::mem::Bytes<16> hash; + u32 crc; + } + u32 minimumStreamedBytes; + u32 size; + u32 numberOfLevelsToDownloadBeforeStreaming; + s32 levelCount; + v1::Level levels[levelCount]; + if (version >= 2) { + u32 completeFileSize; + } + if (version >= 3) { + u32 fileInfoHeaderSize; + } + $ = size; + u8 compressedBlockInfo[levels[std::core::member_count(levels) - 1].compressedSize] [[sealed]]; + blockData = std::mem::create_section("BlockInfoAndData"); + if (signature == "UnityWeb\0") { + hex::dec::lzma_decompress(compressedBlockInfo, blockData); + } else { + std::mem::copy_value_to_section(compressedBlockInfo, blockData, 0); + } + v1::Header header @ 0 in blockData; + return; + } + s64 size; + u32 compressedBlockInfoSize; + u32 decompressedBlockInfoSize; + u32 flags; + if (signature != "UnityFS\0") { + $ += 1; + } + if (version >= 7) { + $ += -$ & 0b1111; + } + + if (flags & Flag::BlockInfoAtTheEnd) { + u8 compressedBlockInfo[compressedBlockInfoSize] @ std::mem::size() - compressedBlockInfo [[sealed]]; + } else { + std::assert_warn(flags & Flag::BlocksAndDirectoryInfoCombined, "Expected BlocksAndDirectoryInfoCombined to be true"); + u8 compressedBlockInfo[compressedBlockInfoSize] [[sealed]]; + } + + if (flags & Flag::BlockInfoNeedPaddingAtStart) { + $ += -$ & 0b1111; + } + + std::mem::Section blockInfo = std::mem::create_section("BlockInfo"); + match (flags & Flag::CompressionMask) { + (_): std::unimplemented(); + (Compression::None): std::mem::copy_value_to_section(compressedBlockInfo, blockInfo, 0); + (Compression::Lzma): hex::dec::lzma_decompress(compressedBlockInfo, blockInfo); + (Compression::Lz4): hex::dec::lz4_decompress(compressedBlockInfo, blockInfo, false); + (Compression::Lz4hc): hex::dec::lz4_decompress(compressedBlockInfo, blockInfo, false); + (Compression::Lzham): std::unimplemented(); + } + + cursor = $; + v2::Header header @ 0 in blockInfo; +}; + +AssetBundle assetBundle @ 0;