Skip to content

Commit

Permalink
patterns: Added pattern for Blender project files (#303)
Browse files Browse the repository at this point in the history
* patterns: Added pattern for Blender project files

* patterns/blend: Added pattern file and test data

* patterns/blend: Fix the thumbnail bugs by passing the source data by reference

* patterns/blend: Added ZSTD support and test data
  • Loading branch information
Hikodroid authored Nov 17, 2024
1 parent abc78d1 commit e856458
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi
| Bastion | | [`patterns/bastion/*`](https://gitlab.com/EvelynTSMG/imhex-bastion-pats) | Various [Bastion](https://en.wikipedia.org/wiki/Bastion_(video_game)) files |
| Bencode | `application/x-bittorrent` | [`patterns/bencode.hexpat`](patterns/bencode.hexpat) | Bencode encoding, used by Torrent files |
| Prusa BGCODE | | [`patterns/bgcode.hexpat`](patterns/bgcode.hexpat) | PrusaSlicer Binary G-Code files |
| BLEND | | [`patterns/blend.hexpat`](patterns/blend.hexpat) | Blender Project file |
| BMP | `image/bmp` | [`patterns/bmp.hexpat`](patterns/bmp.hexpat) | OS2/Windows Bitmap files |
| BIN | | [`patterns/selinux.hexpat`](patterns/selinux.pat) | SE Linux modules |
| BSON | `application/bson` | [`patterns/bson.hexpat`](patterns/bson.hexpat) | BSON (Binary JSON) format |
Expand Down
196 changes: 196 additions & 0 deletions patterns/blend.hexpat
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#pragma description Blender file
#pragma magic [42 4C 45 4E 44 45 52] @ 0x00

/*
* References:
* https://projects.blender.org/blender/blender
* https://github.com/facebook/zstd/blob/master/contrib/seekable_format/zstd_seekable_compression_format.md
*
* Refer to the following files/structs:
* source/blender/blenloader/intern/writefile.cc
* source/blender/blenkernel/BKE_main.hh BlendThumbnail
* source/blender/makesdna/DNA_sdna_types.h BHead
*/

// Increased the pattern limit to be able to evaluate all pixels of the embedded thumbnail.
#pragma pattern_limit 1000000

#ifdef __IMHEX__
import hex.dec;
#endif

import std.core;
import std.io;
import std.mem;
import std.string;
import std.sys;
import type.color;
import type.magic;

// Useful for extracting the thumbnail if the rest of the blend file is corrupted or truncated.
bool quitAfterThumbnailIsParsed in;
// Allow the pattern evaluator to skip the thumbnail e.g. if evaluation takes too long.
bool skipThumbnail in;

struct BHead<Ptr> {
char code[4];
s32 len;
Ptr old;
s32 SDNAnr;
s32 nr;

// ENDB marks the last data block in the file.
if (code == "ENDB") {
break;
}
};

struct ThumbnailLine<auto width> {
type::RGBA8 pixels[width];
};

fn copyThumbnail(u32 height, ref auto lines, std::mem::Section target) {
for (s64 l = (height - 1), l >= 0, l = l - 1) {
u64 currentSectionSize = std::mem::get_section_size(target);
// Append the current line to section.
std::mem::copy_value_to_section(lines[l], target, currentSectionSize);
}
};

struct Thumbnail {
u32 width;
u32 height;
u128 size = width * height;
ThumbnailLine<width> lines[height];

// Generate the thumbnail section.
std::mem::Section thumbnailFlipped = std::mem::create_section("thumbnail");
copyThumbnail(height, lines, thumbnailFlipped);
type::RGBA8 image[size] @ 0x00 in thumbnailFlipped;
}
#ifdef __IMHEX__
[[hex::visualize("bitmap", image, width, height)]]
#endif
;

struct DataBlock<Ptr> {
BHead<Ptr> bHead;

if (bHead.SDNAnr == 0 && bHead.code == "TEST") {
if (skipThumbnail) {
u8 thumbnail[bHead.len]; // Interpret as raw binary data.
} else {
Thumbnail thumbnail;
auto thumbnailSize = sizeof(thumbnail);
std::assert(thumbnailSize == bHead.len,
std::format("The thumbnail (size={:#x}) does not fit exactly into its DataBlock (len={:#x})!",
thumbnailSize, bHead.len));
}

if (quitAfterThumbnailIsParsed) {
break;
}
} else {
u8 data[bHead.len]; // Unknown. Interpret as raw binary data.
}
};

enum PointerSize : char {
POINTER_4BYTE = '_',
POINTER_8BYTE = '-'
};

enum Endianness : char {
BIG_ENDIAN = 'V',
LITTLE_ENDIAN = 'v'
};

struct Blend<auto inputSize> {
type::Magic<"BLENDER"> magic;
PointerSize pointerSize;
Endianness endianness;
char version[3];

match (endianness) {
(Endianness::LITTLE_ENDIAN): std::core::set_endian(std::mem::Endian::Little);
(Endianness::BIG_ENDIAN): std::core::set_endian(std::mem::Endian::Big);
(_): std::error("Invalid value for endianness!");
}

match (pointerSize) {
(PointerSize::POINTER_4BYTE): DataBlock<u32> dataBlock[while($ < inputSize)];
(PointerSize::POINTER_8BYTE): DataBlock<u64> dataBlock[while($ < inputSize)];
(_): std::error("Invalid pointer size!");
}
};

struct BlendWrapper {
u128 currentPos = $;
char magic[4] @ currentPos [[hidden]];

if (magic != "\x28\xB5\x2F\xFD") { // ZSTD magic
// Assume the blend file is uncompressed.
Blend<sizeof($)> blend @ currentPos;
return;
}
} [[inline]];

BlendWrapper blendWrapper @ 0x00;

// Assume the blend file is ZSTD compressed.

struct SeekTableFooter {
u32 numFrames;
char flag;
type::Magic<"\xB1\xEA\x92\x8F"> footerMagic;
};

u128 seekTableFooterSize = 9;
SeekTableFooter seekTableFooter @ (sizeof($) - seekTableFooterSize);

struct SeekTableEntry {
u32 compressedSize;
u32 uncompressedSize;
};

u128 seekTableEntrySize = 8;
SeekTableEntry seekTableEntries[seekTableFooter.numFrames]
@ (addressof(seekTableFooter) - seekTableFooter.numFrames * seekTableEntrySize);

struct SeekTableHeader {
type::Magic<"\x5E\x2A\x4D\x18"> magic;
u32 frameSize;
};

u128 seekTableHeaderSize = 8;
std::assert(seekTableFooter.numFrames > 0, "The seek table must contain entries!");
SeekTableHeader seekTableHeader @ (addressof(seekTableEntries[0]) - seekTableHeaderSize);

u32 frameIndex = 0;

struct ZSTDFrame {
u8 data[seekTableEntries[frameIndex].compressedSize];
frameIndex = frameIndex + 1;
};

ZSTDFrame zstdFrames[seekTableFooter.numFrames] @ 0x00;

#ifdef __IMHEX__
std::mem::Section decompressedSection = std::mem::create_section("decompressedBlend");
u128 previousSectionSize = 0;

for (u32 i = 0, i < seekTableFooter.numFrames, i = i + 1) {
std::assert(hex::dec::zstd_decompress(zstdFrames[i].data, decompressedSection),
"Decompression failed!");
u32 uncompressedSize = seekTableEntries[i].uncompressedSize;
u128 currentSectionSize = std::mem::get_section_size(decompressedSection)
- previousSectionSize;
std::assert_warn(uncompressedSize == currentSectionSize,
std::format("The uncompressedSize {} for ZSTDFrame #{} "
+ "must be equal to its actual decompressed size{}!",
uncompressedSize, i, currentSectionSize));
previousSectionSize += currentSectionSize;
};

Blend<previousSectionSize> blend @ 0x00 in decompressedSection;
#endif
Binary file not shown.
Binary file not shown.

0 comments on commit e856458

Please sign in to comment.