Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
177 changes: 177 additions & 0 deletions patterns/unity-asset-bundle.hexpat
Original file line number Diff line number Diff line change
@@ -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<size> 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<size> 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;