---
The blendfile module within BAT supports reading data from structs, such
as the Count property on an array modifier. However, blendfile only
supports modifying structs with type "char". This patch adds support for
writing structs of more types in blendfile blocks. Now, writing is
supported for ushort, short, uint, int, float, and ulong types.
The use case that inspired this patch was an instance where a file had
several array modifiers that prevented the file from being opened in
Blender on machines without large amounts of RAM. A solution using the
blendfile module may look like:
```
from blender_asset_tracer import blendfile
from pathlib import Path
b = blendfile.open_cached(Path('flag.blend'), mode='rb+')
for block in b.blocks:
if 'ArrayModifierData' in block.__str__():
try:
print('previous:', block.get(b'count'))
block.set(b'count', 1)
print('current:', block.get(b'count'))
except KeyError:
continue
b.close()
```
This would fail with the exception
`blender_asset_tracer.blendfile.exceptions.NoWriterImplemented: Setting
type Struct(b'int') is not supported for ArrayModifierData.count`. With
this patch, the above code succeeds and the count struct can be set to
a lower number that allows the file to be opened.
This solution implements missing functionality without adding any new
interfaces. A few details are:
* When deciding what type to write to the struct, the value is inferred
from what is given by the caller. If the caller gives a Python int, the
exact type is inferred from the DNA type ID. If they give a float, a
float is written. Otherwise, the existing logic is used to determine
whether to write a string or byte sequence.
* A \_write method was added to dna\_io.py that takes a Python struct
object and a value to write a byte sequence to the file object. This
method is used by public methods appropriately named to indicate what
type they will write.
* The check for whether the caller is trying to write an unsupported
type is left in place, but it has been changed to include types which
are now supported.
* Tests have been added that provide a mock file object, call the new
methods, and confirm that the correct bytes were written.
Reviewed By: sybren
Differential Revision: https://developer.blender.org/D14374
224 lines
7.0 KiB
Python
224 lines
7.0 KiB
Python
# ***** BEGIN GPL LICENSE BLOCK *****
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
#
|
|
# ***** END GPL LICENCE BLOCK *****
|
|
#
|
|
# (c) 2009, At Mind B.V. - Jeroen Bakker
|
|
# (c) 2014, Blender Foundation - Campbell Barton
|
|
# (c) 2018, Blender Foundation - Sybren A. Stüvel
|
|
"""Read-write utility functions."""
|
|
|
|
import struct
|
|
import typing
|
|
|
|
|
|
class EndianIO:
|
|
# TODO(Sybren): note as UCHAR: struct.Struct = None and move actual structs to LittleEndianTypes
|
|
UCHAR = struct.Struct(b"<B")
|
|
USHORT = struct.Struct(b"<H")
|
|
USHORT2 = struct.Struct(b"<HH") # two shorts in a row
|
|
SSHORT = struct.Struct(b"<h")
|
|
UINT = struct.Struct(b"<I")
|
|
SINT = struct.Struct(b"<i")
|
|
FLOAT = struct.Struct(b"<f")
|
|
ULONG = struct.Struct(b"<Q")
|
|
|
|
@classmethod
|
|
def _read(cls, fileobj: typing.IO[bytes], typestruct: struct.Struct):
|
|
data = fileobj.read(typestruct.size)
|
|
try:
|
|
return typestruct.unpack(data)[0]
|
|
except struct.error as ex:
|
|
raise struct.error("%s (read %d bytes)" % (ex, len(data))) from None
|
|
|
|
@classmethod
|
|
def _write(cls, fileobj: typing.IO[bytes], typestruct: struct.Struct, value: typing.Any):
|
|
try:
|
|
data = typestruct.pack(value)
|
|
except struct.error as ex:
|
|
raise struct.error(f"{ex} (write '{value}')")
|
|
return fileobj.write(data)
|
|
|
|
@classmethod
|
|
def read_char(cls, fileobj: typing.IO[bytes]):
|
|
return cls._read(fileobj, cls.UCHAR)
|
|
|
|
@classmethod
|
|
def write_char(cls, fileobj: typing.IO[bytes], value: int):
|
|
return cls._write(fileobj, cls.UCHAR, value)
|
|
|
|
@classmethod
|
|
def read_ushort(cls, fileobj: typing.IO[bytes]):
|
|
return cls._read(fileobj, cls.USHORT)
|
|
|
|
@classmethod
|
|
def write_ushort(cls, fileobj: typing.IO[bytes], value: int):
|
|
return cls._write(fileobj, cls.USHORT, value)
|
|
|
|
@classmethod
|
|
def read_short(cls, fileobj: typing.IO[bytes]):
|
|
return cls._read(fileobj, cls.SSHORT)
|
|
|
|
@classmethod
|
|
def write_short(cls, fileobj: typing.IO[bytes], value: int):
|
|
return cls._write(fileobj, cls.SSHORT, value)
|
|
|
|
@classmethod
|
|
def read_uint(cls, fileobj: typing.IO[bytes]):
|
|
return cls._read(fileobj, cls.UINT)
|
|
|
|
@classmethod
|
|
def write_uint(cls, fileobj: typing.IO[bytes], value: int):
|
|
return cls._write(fileobj, cls.UINT, value)
|
|
|
|
@classmethod
|
|
def read_int(cls, fileobj: typing.IO[bytes]):
|
|
return cls._read(fileobj, cls.SINT)
|
|
|
|
@classmethod
|
|
def write_int(cls, fileobj: typing.IO[bytes], value: int):
|
|
return cls._write(fileobj, cls.SINT, value)
|
|
|
|
@classmethod
|
|
def read_float(cls, fileobj: typing.IO[bytes]):
|
|
return cls._read(fileobj, cls.FLOAT)
|
|
|
|
@classmethod
|
|
def write_float(cls, fileobj: typing.IO[bytes], value: float):
|
|
return cls._write(fileobj, cls.FLOAT, value)
|
|
|
|
@classmethod
|
|
def read_ulong(cls, fileobj: typing.IO[bytes]):
|
|
return cls._read(fileobj, cls.ULONG)
|
|
|
|
@classmethod
|
|
def write_ulong(cls, fileobj: typing.IO[bytes], value: int):
|
|
return cls._write(fileobj, cls.ULONG, value)
|
|
|
|
@classmethod
|
|
def read_pointer(cls, fileobj: typing.IO[bytes], pointer_size: int):
|
|
"""Read a pointer from a file."""
|
|
|
|
if pointer_size == 4:
|
|
return cls.read_uint(fileobj)
|
|
if pointer_size == 8:
|
|
return cls.read_ulong(fileobj)
|
|
raise ValueError("unsupported pointer size %d" % pointer_size)
|
|
|
|
@classmethod
|
|
def write_pointer(cls, fileobj: typing.IO[bytes], pointer_size: int, value: int):
|
|
"""Write a pointer to a file."""
|
|
|
|
if pointer_size == 4:
|
|
return cls.write_uint(fileobj, value)
|
|
if pointer_size == 8:
|
|
return cls.write_ulong(fileobj, value)
|
|
raise ValueError("unsupported pointer size %d" % pointer_size)
|
|
|
|
@classmethod
|
|
def write_string(
|
|
cls, fileobj: typing.IO[bytes], astring: str, fieldlen: int
|
|
) -> int:
|
|
"""Write a (truncated) string as UTF-8.
|
|
|
|
The string will always be written 0-terminated.
|
|
|
|
:param fileobj: the file to write to.
|
|
:param astring: the string to write.
|
|
:param fieldlen: the field length in bytes.
|
|
:returns: the number of bytes written.
|
|
"""
|
|
assert isinstance(astring, str)
|
|
encoded = astring.encode("utf-8")
|
|
|
|
# Take into account we also need space for a trailing 0-byte.
|
|
maxlen = fieldlen - 1
|
|
|
|
if len(encoded) >= maxlen:
|
|
encoded = encoded[:maxlen]
|
|
|
|
# Keep stripping off the last byte until the string
|
|
# is valid UTF-8 again.
|
|
while True:
|
|
try:
|
|
encoded.decode("utf8")
|
|
except UnicodeDecodeError:
|
|
encoded = encoded[:-1]
|
|
else:
|
|
break
|
|
|
|
return fileobj.write(encoded + b"\0")
|
|
|
|
@classmethod
|
|
def write_bytes(cls, fileobj: typing.IO[bytes], data: bytes, fieldlen: int) -> int:
|
|
"""Write (truncated) bytes.
|
|
|
|
When len(data) < fieldlen, a terminating b'\0' will be appended.
|
|
|
|
:returns: the number of bytes written.
|
|
"""
|
|
assert isinstance(data, (bytes, bytearray))
|
|
if len(data) >= fieldlen:
|
|
to_write = data[0:fieldlen]
|
|
else:
|
|
to_write = data + b"\0"
|
|
|
|
return fileobj.write(to_write)
|
|
|
|
@classmethod
|
|
def read_bytes0(cls, fileobj, length):
|
|
data = fileobj.read(length)
|
|
return cls.read_data0(data)
|
|
|
|
@classmethod
|
|
def read_data0_offset(cls, data, offset):
|
|
add = data.find(b"\0", offset) - offset
|
|
return data[offset : offset + add]
|
|
|
|
@classmethod
|
|
def read_data0(cls, data):
|
|
add = data.find(b"\0")
|
|
if add < 0:
|
|
return data
|
|
return data[:add]
|
|
|
|
@classmethod
|
|
def accepted_types(cls):
|
|
return {
|
|
b"char": cls.write_char,
|
|
b"ushort": cls.write_ushort,
|
|
b"short": cls.write_short,
|
|
b"uint": cls.write_uint,
|
|
b"int": cls.write_int,
|
|
b"ulong": cls.write_ulong,
|
|
b"float": cls.write_float
|
|
}
|
|
|
|
|
|
class LittleEndianTypes(EndianIO):
|
|
pass
|
|
|
|
|
|
class BigEndianTypes(LittleEndianTypes):
|
|
UCHAR = struct.Struct(b">B")
|
|
USHORT = struct.Struct(b">H")
|
|
USHORT2 = struct.Struct(b">HH") # two shorts in a row
|
|
SSHORT = struct.Struct(b">h")
|
|
UINT = struct.Struct(b">I")
|
|
SINT = struct.Struct(b">i")
|
|
FLOAT = struct.Struct(b">f")
|
|
ULONG = struct.Struct(b">Q")
|