Clarified type refinement, simplified API, custom exception for get_pointer

Type refinement is now only done with BlendFileBlock.refine_type(), and
no longer with sdna_index_refine parameters to various functions. This
simplifies the API at the expense of having to call two simple functions
instead of one more complex one.
This commit is contained in:
Sybren A. Stüvel 2018-02-23 12:23:59 +01:00
parent 9c8766a28c
commit 934a8e210e
5 changed files with 123 additions and 49 deletions

View File

@ -315,15 +315,30 @@ class BlendFileBlock:
def dna_type_name(self) -> str: def dna_type_name(self) -> str:
return self.dna_type.dna_type_id.decode('ascii') return self.dna_type.dna_type_id.decode('ascii')
def refine_type_from_index(self, sdna_index_next): # TODO(Sybren): port to BAT def refine_type_from_index(self, sdna_index: int):
assert (type(sdna_index_next) is int) """Change the DNA Struct associated with this block.
sdna_index_curr = self.sdna_index
self.bfile.ensure_subtype_smaller(sdna_index_curr, sdna_index_next)
self.sdna_index = sdna_index_next
def refine_type(self, dna_type_id): # TODO(Sybren): port to BAT Use to make a block type more specific, for example when you have a
assert (type(dna_type_id) is bytes) modifier but need to access it as SubSurfModifier.
self.refine_type_from_index(self.bfile.sdna_index_from_id[dna_type_id])
:param sdna_index: the SDNA index of the DNA type.
"""
assert type(sdna_index) is int
sdna_index_curr = self.sdna_index
self.bfile.ensure_subtype_smaller(sdna_index_curr, sdna_index)
self.sdna_index = sdna_index
def refine_type(self, dna_type_id: bytes):
"""Change the DNA Struct associated with this block.
Use to make a block type more specific, for example when you have a
modifier but need to access it as SubSurfModifier.
:param dna_type_id: the name of the DNA type.
"""
assert isinstance(dna_type_id, bytes)
sdna_index = self.bfile.sdna_index_from_id[dna_type_id]
self.refine_type_from_index(sdna_index)
def get_file_offset(self, path: bytes) -> (int, int): # TODO(Sybren): port to BAT def get_file_offset(self, path: bytes) -> (int, int): # TODO(Sybren): port to BAT
"""Return (offset, length)""" """Return (offset, length)"""
@ -338,7 +353,6 @@ class BlendFileBlock:
def get(self, def get(self,
path: dna.FieldPath, path: dna.FieldPath,
default=..., default=...,
sdna_index_refine=None,
null_terminated=True, null_terminated=True,
as_str=True, as_str=True,
base_index=0, base_index=0,
@ -365,30 +379,17 @@ class BlendFileBlock:
ofs += (self.size // self.count) * base_index ofs += (self.size // self.count) * base_index
self.bfile.fileobj.seek(ofs, os.SEEK_SET) self.bfile.fileobj.seek(ofs, os.SEEK_SET)
dna_struct = self._get_struct(sdna_index_refine) dna_struct = self.bfile.structs[self.sdna_index]
return dna_struct.field_get( return dna_struct.field_get(
self.bfile.header, self.bfile.fileobj, path, self.bfile.header, self.bfile.fileobj, path,
default=default, default=default,
null_terminated=null_terminated, as_str=as_str, null_terminated=null_terminated, as_str=as_str,
) )
def _get_struct(self, sdna_index_refine) -> dna.Struct:
"""Gets the (possibly refined) struct for this block."""
if sdna_index_refine is None:
index = self.sdna_index
else:
self.bfile.ensure_subtype_smaller(self.sdna_index, sdna_index_refine)
index = sdna_index_refine
dna_struct = self.bfile.structs[index]
return dna_struct
def get_recursive_iter(self, def get_recursive_iter(self,
path: dna.FieldPath, path: dna.FieldPath,
path_root: dna.FieldPath = b'', path_root: dna.FieldPath = b'',
default=..., default=...,
sdna_index_refine=None,
null_terminated=True, null_terminated=True,
as_str=True, as_str=True,
base_index=0, base_index=0,
@ -408,8 +409,7 @@ class BlendFileBlock:
try: try:
# Try accessing as simple property # Try accessing as simple property
yield (path_full, yield (path_full,
self.get(path_full, default, sdna_index_refine, null_terminated, as_str, self.get(path_full, default, null_terminated, as_str, base_index))
base_index))
except exceptions.NoReaderImplemented as ex: except exceptions.NoReaderImplemented as ex:
# This was not a simple property, so recurse into its DNA Struct. # This was not a simple property, so recurse into its DNA Struct.
dna_type = ex.dna_type dna_type = ex.dna_type
@ -451,38 +451,31 @@ class BlendFileBlock:
return dna_struct.field_set( return dna_struct.field_set(
self.bfile.header, self.bfile.handle, path, value) self.bfile.header, self.bfile.handle, path, value)
# ---------------
# Utility get/set
#
# avoid inline pointer casting
def get_pointer( def get_pointer(
self, path, self, path: dna.FieldPath,
default=..., default=...,
sdna_index_refine=None,
base_index=0, base_index=0,
): ) -> typing.Union[None, 'BlendFileBlock', typing.Any]:
if sdna_index_refine is None: """Same as get() but dereferences a pointer.
sdna_index_refine = self.sdna_index
result = self.get(path, default, sdna_index_refine=sdna_index_refine, base_index=base_index)
# default :raises exceptions.SegmentationFault: when there is no datablock with
the pointed-to address.
"""
result = self.get(path, default=default, base_index=base_index)
# If it's not an integer, we have no pointer to follow and this may
# actually be a non-pointer property.
if type(result) is not int: if type(result) is not int:
return result return result
assert (self.bfile.structs[sdna_index_refine].field_from_path( if result == 0:
self.bfile.header, self.bfile.handle, path).dna_name.is_pointer)
if result != 0:
# possible (but unlikely)
# that this fails and returns None
# maybe we want to raise some exception in this case
return self.bfile.find_block_from_offset(result)
else:
return None return None
# ---------------------- try:
# Python convenience API return self.bfile.block_from_addr[result]
except KeyError:
raise exceptions.SegmentationFault('address does not exist', path, result)
# dict like access
def __getitem__(self, path: dna.FieldPath): def __getitem__(self, path: dna.FieldPath):
return self.get(path, as_str=False) return self.get(path, as_str=False)

View File

@ -52,3 +52,12 @@ class NoReaderImplemented(NotImplementedError):
super().__init__(message) super().__init__(message)
self.dna_name = dna_name self.dna_name = dna_name
self.dna_type = dna_type self.dna_type = dna_type
class SegmentationFault(Exception):
"""Raised when a pointer to a non-existant datablock was dereferenced."""
def __init__(self, message: str, field_path, address: int):
super().__init__(message)
self.field_path = field_path
self.address = address

View File

@ -0,0 +1,9 @@
from . import BlendFileBlock
def listbase(block: BlendFileBlock) -> BlendFileBlock:
"""Generator, yields all blocks in the ListBase linked list."""
while block:
yield block
next_ptr = block[b'next']
block = block.bfile.find_block_from_address(next_ptr)

Binary file not shown.

View File

@ -4,20 +4,26 @@ import unittest
import os import os
from blender_asset_tracer import blendfile from blender_asset_tracer import blendfile
from blender_asset_tracer.blendfile import iterators
class BlendFileBlockTest(unittest.TestCase): class AbstractBlendFileTest(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.blendfiles = pathlib.Path(__file__).with_name('blendfiles') cls.blendfiles = pathlib.Path(__file__).with_name('blendfiles')
def setUp(self): def setUp(self):
self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend') self.bf = None
def tearDown(self): def tearDown(self):
if self.bf: if self.bf:
self.bf.close() self.bf.close()
class BlendFileBlockTest(AbstractBlendFileTest):
def setUp(self):
self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend')
def test_loading(self): def test_loading(self):
self.assertFalse(self.bf.is_compressed) self.assertFalse(self.bf.is_compressed)
@ -148,3 +154,60 @@ class BlendFileBlockTest(unittest.TestCase):
ob = self.bf.code_index[b'OB'][0] ob = self.bf.code_index[b'OB'][0]
assert isinstance(ob, blendfile.BlendFileBlock) assert isinstance(ob, blendfile.BlendFileBlock)
self.assertEqual('OBümlaut', ob[b'id', b'name'].decode()) self.assertEqual('OBümlaut', ob[b'id', b'name'].decode())
class PointerTest(AbstractBlendFileTest):
def setUp(self):
self.bf = blendfile.BlendFile(self.blendfiles / 'with_sequencer.blend')
def test_get_pointer_and_listbase(self):
scenes = self.bf.code_index[b'SC']
self.assertEqual(1, len(scenes), 'expecting 1 scene')
scene = scenes[0]
self.assertEqual(b'SCScene', scene[b'id', b'name'])
ed_ptr = scene[b'ed']
self.assertEqual(140051431100936, ed_ptr)
ed = scene.get_pointer(b'ed')
self.assertEqual(140051431100936, ed.addr_old)
seqbase = ed.get_pointer((b'seqbase', b'first'))
self.assertIsNotNone(seqbase)
types = {
b'SQBlack': 28,
b'SQCross': 8,
b'SQPink': 28,
}
seq = None
for seq in iterators.listbase(seqbase):
seq.refine_type(b'Sequence')
name = seq[b'name']
expected_type = types[name]
self.assertEqual(expected_type, seq[b'type'])
# The last 'seq' from the loop should be the last in the list.
seq_next = seq.get_pointer(b'next')
self.assertIsNone(seq_next)
def test_refine_sdna_by_name(self):
scene = self.bf.code_index[b'SC'][0]
ed = scene.get_pointer(b'ed')
seq = ed.get_pointer((b'seqbase', b'first'))
# This is very clear to me:
seq.refine_type(b'Sequence')
self.assertEqual(b'SQBlack', seq[b'name'])
self.assertEqual(28, seq[b'type'])
def test_refine_sdna_by_idx(self):
scene = self.bf.code_index[b'SC'][0]
ed = scene.get_pointer(b'ed')
seq = ed.get_pointer((b'seqbase', b'first'))
sdna_idx_sequence = self.bf.sdna_index_from_id[b'Sequence']
seq.refine_type_from_index(sdna_idx_sequence)
self.assertEqual(b'SQBlack', seq[b'name'])
self.assertEqual(28, seq[b'type'])