# SPDX-FileCopyrightText: 2025 Blender Authors # # SPDX-License-Identifier: GPL-2.0-or-later ''' This module contains utility classes for reading headers in .blend files. This is a pure Python implementation of the corresponding C++ code in Blender in BLO_core_blend_header.hh and BLO_core_bhead.hh. ''' import os import struct import typing from dataclasses import dataclass class BlendHeaderError(Exception): pass @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 @dataclass class BlockHeaderStruct: # Binary format of the encoded header. struct: struct.Struct # Corresponding Python type for retrieving block header values. type: typing.Type[typing.Union[BHead4, SmallBHead8, LargeBHead8]] @property def size(self) -> int: return self.struct.size def parse(self, data: bytes) -> typing.Union[BHead4, SmallBHead8, LargeBHead8]: return self.type(*self.struct.unpack(data)) class BlendFileHeader: """ 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. """ # Always 'BLENDER'. magic: bytes # Currently always 0 or 1. file_format_version: int # Either 4 or 8. pointer_size: int # Endianness of values stored in the file. is_little_endian: bool # Blender version the file has been written with. # The last two digits are the minor version. So 280 is 2.80. version: int def __init__(self, file: typing.IO[bytes]) -> None: file.seek(0, os.SEEK_SET) bytes_0_6 = file.read(7) if bytes_0_6 != b'BLENDER': raise BlendHeaderError("invalid first bytes {!r}".format(bytes_0_6)) self.magic = bytes_0_6 byte_7 = file.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 BlendHeaderError("invalid pointer size {!r}".format(byte_7)) byte_8 = file.read(1) if byte_8 == b'v': self.is_little_endian = True elif byte_8 == b'V': self.is_little_endian = False else: raise BlendHeaderError("invalid endian indicator {!r}".format(byte_8)) bytes_9_11 = file.read(3) self.version = int(bytes_9_11) else: byte_8 = file.read(1) header_size = int(byte_7 + byte_8) if header_size != 17: raise BlendHeaderError("unknown file header size {:d}".format(header_size)) byte_9 = file.read(1) if byte_9 != b'-': raise BlendHeaderError("invalid file header") self.pointer_size = 8 byte_10_11 = file.read(2) self.file_format_version = int(byte_10_11) if self.file_format_version != 1: raise BlendHeaderError("unsupported file format version {:d}".format(self.file_format_version)) byte_12 = file.read(1) if byte_12 != b'v': raise BlendHeaderError("invalid file header") self.is_little_endian = True byte_13_16 = file.read(4) self.version = int(byte_13_16) def create_block_header_struct(self) -> BlockHeaderStruct: assert self.file_format_version in (0, 1) endian_str = b'<' if self.is_little_endian else b'>' if self.file_format_version == 1: header_struct = struct.Struct(b''.join(( endian_str, # LargeBHead8.code b'4s', # LargeBHead8.SDNAnr b'i', # LargeBHead8.old b'Q', # LargeBHead8.len b'q', # LargeBHead8.nr b'q', ))) return BlockHeaderStruct(header_struct, LargeBHead8) if self.pointer_size == 4: header_struct = struct.Struct(b''.join(( endian_str, # BHead4.code b'4s', # BHead4.len b'i', # BHead4.old b'I', # BHead4.SDNAnr b'i', # BHead4.nr b'i', ))) return BlockHeaderStruct(header_struct, BHead4) assert self.pointer_size == 8 header_struct = struct.Struct(b''.join(( endian_str, # SmallBHead8.code b'4s', # SmallBHead8.len b'i', # SmallBHead8.old b'Q', # SmallBHead8.SDNAnr b'i', # SmallBHead8.nr b'i', ))) return BlockHeaderStruct(header_struct, SmallBHead8) class BlockHeader: """ A .blend file consists of a sequence of blocks whereby each block has a header. This class can parse a header block in a specific .blend file. Note the binary representation of this header is different for different files. This class provides a unified interface for these underlying representations. """ __slots__ = ( "code", "size", "addr_old", "sdna_index", "count", ) # Indicates the type of the block. See BLO_CODE_* in BLO_core_bhead.hh. code: bytes # Number of bytes in the block. size: int # Old pointer/identifier of the block. addr_old: int # DNA struct index of the data in the block. sdna_index: int # Number of DNA structures in the block. count: int def __init__(self, file: typing.IO[bytes], block_header_struct: BlockHeaderStruct) -> None: data = file.read(block_header_struct.size) if len(data) != block_header_struct.size: if len(data) != 8: raise BlendHeaderError("invalid block header size") legacy_endb = struct.Struct(b'4sI') endb_header = legacy_endb.unpack(data) if endb_header[0] != b'ENDB': raise BlendHeaderError("invalid block header") self.code = b'ENDB' self.size = 0 self.addr_old = 0 self.sdna_index = 0 self.count = 0 return header = block_header_struct.parse(data) self.code = header.code.partition(b'\0')[0] self.size = header.len self.addr_old = header.old self.sdna_index = header.SDNAnr self.count = header.nr