improve header loading

The header is now loaded according to a struct, and all of its fields are validated. The same commit also changes BlendStruct to subclass collections.abc.Mapping.
master
Josiah 2021-07-24 23:07:18 -05:00
parent c618ab5d35
commit 32ddaaf109
No known key found for this signature in database
GPG Key ID: C7BB8573A4ABC4B9
1 changed files with 63 additions and 17 deletions

View File

@ -1,10 +1,24 @@
# A purpose-built class to parse certain information from .blend files.
import collections
import json
import io
import re
import struct
class BlendStruct:
# The .blend format begins with a file header consisting of four fields.
# A 7 byte identifier string, which will always be "BLENDER"
# A single char representing pointer size: "-" for 8 byte and "_" for 4 byte.
# A single char representing endianness: "v" for little endian and "V" for big.
# A 3 byte version string. For example "293" indicates version 2.93.
blend_header_struct = struct.Struct("7scc3s")
BlendHeader = collections.namedtuple(
"FileHeader", ("identifier", "pointer_size", "endianness", "version"))
class BlendDecodeError(Exception):
pass
class BlendStruct(collections.abc.Mapping):
"""
Read-only dict-like datatype representing a structure in a .blend file.
"""
@ -52,6 +66,11 @@ class BlendStruct:
self._structure = self._load_cb()
return self._structure.__iter__()
def __len__(self):
if self._structure is None:
self._structure = self._load_cb()
return len(self._structure)
def inspect(self):
"""
Return a human readable representation of the struct.
@ -69,12 +88,15 @@ class Blendfile(io.FileIO):
super().__init__(filename, "rb")
# Offset of first file block header.
self._block_start = 12
self.read_header()
self._load_header()
# Offsets of each file block by code.
self._block_offsets = self._read_block_offsets()
# SDNA structures by name.
self._sdna = self._load_sdna()
def __str__(self):
return f"Blender file version {self.version}"
def _read_c_string(self):
"""
Utility method to read a null-terminated C-string.
@ -120,6 +142,45 @@ class Blendfile(io.FileIO):
self.seek(size, io.SEEK_CUR)
return type
def _load_header(self):
"""
Unpack the file header.
Verifies the file identifier, and sets the pointer size, endianess, and
blender version.
:raises BlendDecodeError
"""
self.seek(0)
size = blend_header_struct.size
# Does not decode bytes. Decoding bytes could raise an exception, so
# by validating the fields ourselves by comparing bytes, we can raise
# an exception with more useful information.
header = BlendHeader(*blend_header_struct.unpack_from(self.read(size)))
if header.identifier != bytes("BLENDER", "utf-8"):
raise BlendDecodeError("File identifier is not 'BLENDER'!")
if header.pointer_size == bytes("-", "utf-8"):
self.pointer_size = 8
elif header.pointer_size == bytes("_", "utf-8"):
self.pointer_size = 4
else:
raise BlendDecodeError(
f"Invalid pointer size character {header.pointer_size}; " \
"must be '-' or '_'!")
if header.endianness == bytes("v", "utf-8"):
self.endianness = "little"
elif header.endianess == bytes("V", "utf-8"):
self.endianness = "big"
else:
raise BlendDecodeError(
f"Invalid endianness character {header.endianess}; "\
"must be 'v' or 'V'")
if not header.version.isdigit():
raise BlendDecodeError(
f"Invalid version string {header.version}!")
self.version = "v{}.{}{}".format(*header.version.decode("utf-8"))
def _read_block_offsets(self):
"""
Cache the offset to each file block.
@ -252,21 +313,6 @@ class Blendfile(io.FileIO):
structure[name] = self._construct_value(type, is_ptr, length)
return structure
def read_header(self):
self.seek(0)
# File identifier, 8-byte string; should always be "BLENDER"
self.identifier = self.read(7).decode("utf-8")
# Pointer size, 1-byte char; '-' indicates 8 bytes, '_' indicates 4
self.pointer_size = 8 if self.read(1).decode("utf-8") == "-" else 4
# Endianness, 1-byte char; 'v' indicates little endian, 'V' indicates big
self.endianness = "little" if self.read(1).decode("utf-8") == "v" else "big"
# Blender version, 3-byte int; v2.93 is represented as 293, and so on
self.version = int(self.read(3))
def get_blocks(self, match=""):
"""
Get file blocks from the blend file.