diff --git a/blender_asset_tracer/blendfile/__init__.py b/blender_asset_tracer/blendfile/__init__.py index 3894b16..041db61 100644 --- a/blender_asset_tracer/blendfile/__init__.py +++ b/blender_asset_tracer/blendfile/__init__.py @@ -135,7 +135,7 @@ class BlendFile: self.block_from_addr = {} # type: typing.Dict[int, BlendFileBlock] self.header = header.BlendFileHeader(self.fileobj, self.raw_filepath) - self.block_header_struct = self.header.create_block_header_struct() + self.block_header_struct, self.block_header_fields = self.header.create_block_header_struct() self._load_blocks() def _open_file(self, path: pathlib.Path, mode: str) -> typing.IO[bytes]: @@ -455,23 +455,13 @@ class BlendFileBlock: self.code = b"ENDB" return - # header size can be 8, 20, or 24 bytes long - # 8: old blend files ENDB block (exception) - # 20: normal headers 32 bit platform - # 24: normal headers 64 bit platform - if len(data) <= 15: - self.log.debug("interpreting block as old-style ENB block") - blockheader = self.old_structure.unpack(data) - self.code = self.endian.read_data0(blockheader[0]) - return - - blockheader = header_struct.unpack(data) - self.code = self.endian.read_data0(blockheader[0]) + blockheader = bfile.block_header_fields(*header_struct.unpack(data)) + self.code = self.endian.read_data0(blockheader.code) if self.code != b"ENDB": - self.size = blockheader[1] - self.addr_old = blockheader[2] - self.sdna_index = blockheader[3] - self.count = blockheader[4] + self.size = blockheader.len + self.addr_old = blockheader.old + self.sdna_index = blockheader.SDNAnr + self.count = blockheader.nr self.file_offset = bfile.fileobj.tell() def __repr__(self) -> str: diff --git a/blender_asset_tracer/blendfile/header.py b/blender_asset_tracer/blendfile/header.py index e05fe47..1926cf7 100644 --- a/blender_asset_tracer/blendfile/header.py +++ b/blender_asset_tracer/blendfile/header.py @@ -19,6 +19,8 @@ # (c) 2009, At Mind B.V. - Jeroen Bakker # (c) 2014, Blender Foundation - Campbell Barton # (c) 2018, Blender Foundation - Sybren A. Stüvel + +from dataclasses import dataclass import logging import os import pathlib @@ -30,58 +32,151 @@ from . import dna_io, exceptions log = logging.getLogger(__name__) +@dataclass +class BHead4: + code: bytes + len: int + old: int + SDNAnr: int + nr: int + +@dataclass +class SmallBHead8: + code: bytes + len: int + old: int + SDNAnr: int + nr: int + +@dataclass +class LargeBHead8: + code: bytes + SDNAnr: int + old: int + len: int + nr: int + class BlendFileHeader: """ - BlendFileHeader represents the first 12 bytes of a blend file. + BlendFileHeader represents the first 12-17 bytes of a blend file. It contains information about the hardware architecture, which is relevant to the structure of the rest of the file. """ - structure = struct.Struct(b"7s1s1s3s") + magic: bytes + file_format_version: int + pointer_size: int + is_little_endian: bool + endian: typing.Type[dna_io.EndianIO] + endian_str: bytes def __init__(self, fileobj: typing.IO[bytes], path: pathlib.Path) -> None: log.debug("reading blend-file-header %s", path) fileobj.seek(0, os.SEEK_SET) - header = fileobj.read(self.structure.size) - values = self.structure.unpack(header) - self.magic = values[0] + bytes_0_6 = fileobj.read(7) + if bytes_0_6 != b'BLENDER': + raise exceptions.BlendFileError("invalid first bytes %r" % bytes_0_6, path) + self.magic = bytes_0_6 - pointer_size_id = values[1] - if pointer_size_id == b"-": + byte_7 = fileobj.read(1) + is_legacy_header = byte_7 in (b'_', b'-') + if is_legacy_header: + self.file_format_version = 0 + if byte_7 == b'_': + self.pointer_size = 4 + elif byte_7 == b'-': + self.pointer_size = 8 + else: + raise exceptions.BlendFileError("invalid pointer size %r" % byte_7, path) + byte_8 = fileobj.read(1) + if byte_8 == b'v': + self.is_little_endian = True + elif byte_8 == b'V': + self.is_little_endian = False + else: + raise exceptions.BlendFileError("invalid endian indicator %r" % byte_8, path) + bytes_9_11 = fileobj.read(3) + self.version = int(bytes_9_11) + else: + byte_8 = fileobj.read(1) + header_size = int(byte_7 + byte_8) + if header_size != 17: + raise exceptions.BlendFileError("unknown file header size %d" % header_size, path) + byte_9 = fileobj.read(1) + if byte_9 != b'-': + raise exceptions.BlendFileError("invalid file header", path) self.pointer_size = 8 - elif pointer_size_id == b"_": - self.pointer_size = 4 - else: - raise exceptions.BlendFileError( - "invalid pointer size %r" % pointer_size_id, path - ) + byte_10_11 = fileobj.read(2) + self.file_format_version = int(byte_10_11) + if self.file_format_version != 1: + raise exceptions.BlendFileError("unsupported file format version %r" % self.file_format_version, path) + byte_12 = fileobj.read(1) + if byte_12 != b'v': + raise exceptions.BlendFileError("invalid file header", path) + self.is_little_endian = True + byte_13_16 = fileobj.read(4) + self.version = int(byte_13_16) - endian_id = values[2] - if endian_id == b"v": + if self.is_little_endian: + self.endian_str = b'<' self.endian = dna_io.LittleEndianTypes - self.endian_str = b"<" # indication for struct.Struct() - elif endian_id == b"V": - self.endian = dna_io.BigEndianTypes - self.endian_str = b">" # indication for struct.Struct() else: - raise exceptions.BlendFileError( - "invalid endian indicator %r" % endian_id, path - ) + self.endian_str = b'>' + self.endian = dna_io.BigEndianTypes - version_id = values[3] - self.version = int(version_id) + def create_block_header_struct(self) -> typing.Tuple[struct.Struct, typing.Type[typing.Union[BHead4, SmallBHead8, LargeBHead8]]]: + """ + Returns a Struct instance for parsing data block headers and a corresponding + Python class for accessing the right members. Ddepending on the .blend file, + the order of the data members in the block header may be different. + """ + assert self.file_format_version in (0, 1) + if self.file_format_version == 1: + header_struct = struct.Struct(b''.join(( + self.endian_str, + # LargeBHead8.code + b'4s', + # LargeBHead8.SDNAnr + b'i', + # LargeBHead8.old + b'Q', + # LargeBHead8.len + b'q', + # LargeBHead8.nr + b'q', + ))) + return header_struct, LargeBHead8 - def create_block_header_struct(self) -> struct.Struct: - """Create a Struct instance for parsing data block headers.""" - return struct.Struct( - b"".join( - ( - self.endian_str, - b"4sI", - b"I" if self.pointer_size == 4 else b"Q", - b"II", - ) - ) - ) + if self.pointer_size == 4: + header_struct = struct.Struct(b''.join(( + self.endian_str, + # BHead4.code + b'4s', + # BHead4.len + b'i', + # BHead4.old + b'I', + # BHead4.SDNAnr + b'i', + # BHead4.nr + b'i', + ))) + return header_struct, BHead4 + + assert self.pointer_size == 8 + header_struct = struct.Struct(b''.join(( + self.endian_str, + # SmallBHead8.code + b'4s', + # SmallBHead8.len + b'i', + # SmallBHead8.old + b'Q', + # SmallBHead8.SDNAnr + b'i', + # SmallBHead8.nr + b'i', + ))) + return header_struct, SmallBHead8 diff --git a/tests/blendfiles/basic_file_large_bhead8.blend b/tests/blendfiles/basic_file_large_bhead8.blend new file mode 100644 index 0000000..9312744 Binary files /dev/null and b/tests/blendfiles/basic_file_large_bhead8.blend differ diff --git a/tests/test_blendfile_loading.py b/tests/test_blendfile_loading.py index 914c071..505b101 100644 --- a/tests/test_blendfile_loading.py +++ b/tests/test_blendfile_loading.py @@ -13,6 +13,7 @@ class BlendFileBlockTest(AbstractBlendFileTest): def test_loading(self): self.assertFalse(self.bf.is_compressed) + self.assertEqual(0, self.bf.header.file_format_version) def test_some_properties(self): ob = self.bf.code_index[b"OB"][0] @@ -154,6 +155,23 @@ class BlendFileBlockTest(AbstractBlendFileTest): self.assertEqual("OBümlaut", ob.id_name.decode()) +class BlendFileLargeBhead8Test(AbstractBlendFileTest): + def setUp(self): + self.bf = blendfile.BlendFile(self.blendfiles / "basic_file_large_bhead8.blend") + + def test_loading(self): + self.assertFalse(self.bf.is_compressed) + self.assertEqual(1, self.bf.header.file_format_version) + + def test_some_properties(self): + ob = self.bf.code_index[b"OB"][0] + self.assertEqual("Object", ob.dna_type_name) + + # Try high level operation to read the object location. + loc = ob.get(b"loc") + self.assertEqual([2.0, 3.0, 5.0], loc) + + class PointerTest(AbstractBlendFileTest): def setUp(self): self.bf = blendfile.BlendFile(self.blendfiles / "with_sequencer.blend")