# Copyright 2017 Martin Olejar
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from struct import pack, Struct
from string import printable
from .header import Header, DTB_PROP, DTB_BEGIN_NODE, DTB_END_NODE
from .misc import is_string, line_offset
BIGENDIAN_WORD = Struct(">I")
########################################################################################################################
# Helper methods
########################################################################################################################
def new_property(name: str, raw_value: bytes) -> object:
"""
Instantiate property with raw value type
:param name: Property name
:param raw_value: Property raw data
"""
if is_string(raw_value):
obj = PropStrings(name)
# Extract strings from raw value
for st in raw_value.decode('ascii').split('\0'):
if st:
obj.append(st)
return obj
elif len(raw_value) and len(raw_value) % 4 == 0:
obj = PropWords(name)
# Extract words from raw value
obj.data = [BIGENDIAN_WORD.unpack(raw_value[i:i + 4])[0] for i in range(0, len(raw_value), 4)]
return obj
elif len(raw_value):
return PropBytes(name, data=raw_value)
else:
return Property(name)
########################################################################################################################
# Base Class
########################################################################################################################
class BaseItem:
@property
def name(self):
return self._name
@property
def label(self):
return self._label
@property
def parent(self):
return self._parent
@property
def path(self):
node = self._parent
path = ""
while node:
if node.name == '/': break
path = '/' + node.name + path
node = node.parent
return path if path else '/'
def __init__(self, name: str):
"""
BaseItem constructor
:param name: Item name
"""
assert isinstance(name, str)
assert all(c in printable for c in name), "The value must contain just printable chars !"
self._name = name
self._label = None
self._parent = None
def __str__(self):
""" String representation """
return "{}".format(self.name)
def set_name(self, value: str):
"""
Set item name
:param value: The name in string format
"""
assert isinstance(value, str)
assert all(c in printable for c in value), "The value must contain just printable chars !"
self._name = value
def set_label(self, value: str):
"""
Set item label
:param value: The label in string format
"""
assert isinstance(value, str)
assert all(c in printable for c in value), "The value must contain just printable chars !"
self._label = value
def set_parent(self, value):
"""
Set item parent
:param value: The parent node
"""
assert isinstance(value, Node)
self._parent = value
def to_dts(self, tabsize: int = 4, depth: int = 0):
raise NotImplementedError()
def to_dtb(self, strings: str, pos: int = 0, version: int = Header.MAX_VERSION):
raise NotImplementedError()
########################################################################################################################
# Property Classes
########################################################################################################################
class Property(BaseItem):
def __getitem__(self, value):
""" Returns No Items """
return None
def __eq__(self, obj):
""" Check Property object equality """
return isinstance(obj, Property) and self.name == obj.name
def copy(self):
""" Get object copy """
return Property(self.name)
def to_dts(self, tabsize: int = 4, depth: int = 0):
"""
Get string representation
:param tabsize: Tabulator size in count of spaces
:param depth: Start depth for line
"""
return line_offset(tabsize, depth, '{};\n'.format(self.name))
def to_dtb(self, strings: str, pos: int = 0, version: int = Header.MAX_VERSION):
"""
Get binary blob representation
:param strings:
:param pos:
:param version:
"""
strpos = strings.find(self.name + '\0')
if strpos < 0:
strpos = len(strings)
strings += self.name + '\0'
pos += 12
return pack('>III', DTB_PROP, 0, strpos), strings, pos
[docs]
class PropStrings(Property):
"""Property with strings as value"""
@property
def value(self):
return self.data[0] if self.data else None
def __init__(self, name: str, *args):
"""
PropStrings constructor
:param name: Property name
:param args: str1, str2, ...
"""
super().__init__(name)
self.data = []
for arg in args:
self.append(arg)
def __str__(self):
""" String representation """
return "{} = {}".format(self.name, self.data)
def __len__(self):
""" Get strings count """
return len(self.data)
def __getitem__(self, index):
""" Get string by index """
return self.data[index]
def __eq__(self, obj):
""" Check PropStrings object equality """
if not isinstance(obj, PropStrings) or self.name != obj.name or len(self) != len(obj):
return False
for index in range(len(self)):
if self.data[index] != obj[index]:
return False
return True
[docs]
def copy(self):
""" Get object copy """
return PropStrings(self.name, *self.data)
def append(self, value: str):
assert isinstance(value, str)
assert len(value) > 0, "Invalid strings value"
assert all(c in printable or c in ('\r', '\n') for c in value), "Invalid chars in strings value"
self.data.append(value)
def pop(self, index: int):
assert 0 <= index < len(self.data), "Index out of range"
return self.data.pop(index)
def clear(self):
self.data.clear()
[docs]
def to_dts(self, tabsize: int = 4, depth: int = 0):
"""
Get string representation
:param tabsize: Tabulator size in count of spaces
:param depth: Start depth for line
"""
result = line_offset(tabsize, depth, self.name)
result += ' = "'
result += '", "'.join([item.replace('"', '\\"') for item in self.data])
result += '";\n'
return result
[docs]
def to_dtb(self, strings: str, pos: int = 0, version: int = Header.MAX_VERSION):
"""
Get blob representation
:param strings:
:param pos:
:param version:
"""
blob = pack('')
for chars in self.data:
blob += chars.encode('ascii') + pack('b', 0)
blob_len = len(blob)
if version < 16 and (pos + 12) % 8 != 0:
blob = pack('b', 0) * (8 - ((pos + 12) % 8)) + blob
if blob_len % 4:
blob += pack('b', 0) * (4 - (blob_len % 4))
strpos = strings.find(self.name + '\0')
if strpos < 0:
strpos = len(strings)
strings += self.name + '\0'
blob = pack('>III', DTB_PROP, blob_len, strpos) + blob
pos += len(blob)
return blob, strings, pos
[docs]
class PropWords(Property):
"""Property with words as value"""
@property
def value(self):
return self.data[0] if self.data else None
def __init__(self, name, *args):
"""
PropWords constructor
:param name: Property name
:param args: word1, word2, ...
"""
super().__init__(name)
self.data = []
self.word_size = 32
for val in args:
self.append(val)
def __str__(self):
""" String representation """
return "{} = {}".format(self.name, self.data)
def __getitem__(self, index):
""" Get word by index """
return self.data[index]
def __len__(self):
""" Get words count """
return len(self.data)
def __eq__(self, prop):
""" Check PropWords object equality """
if not isinstance(prop, PropWords):
return False
if self.name != prop.name:
return False
if len(self) != len(prop):
return False
for index in range(len(self)):
if self.data[index] != prop[index]:
return False
return True
[docs]
def copy(self):
return PropWords(self.name, *self.data)
def append(self, value):
assert isinstance(value, int), "Invalid object type"
assert 0 <= value < 2**self.word_size, "Invalid word value {}, use <0x0 - 0x{:X}>".format(
value, 2**self.word_size - 1)
self.data.append(value)
def pop(self, index):
assert 0 <= index < len(self.data), "Index out of range"
return self.data.pop(index)
def clear(self):
self.data.clear()
[docs]
def to_dts(self, tabsize: int = 4, depth: int = 0):
"""
Get string representation
:param tabsize: Tabulator size in count of spaces
:param depth: Start depth for line
"""
result = line_offset(tabsize, depth, self.name)
result += ' = <'
result += ' '.join(["0x{:X}".format(word) for word in self.data])
result += ">;\n"
return result
[docs]
def to_dtb(self, strings: str, pos: int = 0, version: int = Header.MAX_VERSION):
"""
Get blob representation
:param strings:
:param pos:
:param version:
"""
strpos = strings.find(self.name + '\0')
if strpos < 0:
strpos = len(strings)
strings += self.name + '\0'
blob = pack('>III', DTB_PROP, len(self.data) * 4, strpos)
blob += bytes().join([BIGENDIAN_WORD.pack(word) for word in self.data])
pos += len(blob)
return blob, strings, pos
[docs]
class PropBytes(Property):
"""Property with bytes as value"""
def __init__(self, name, *args, data=None):
"""
PropBytes constructor
:param name: Property name
:param args: byte0, byte1, ...
:param data: Data as list, bytes or bytearray
"""
super().__init__(name)
self.data = bytearray(args)
if data:
assert isinstance(data, (list, bytes, bytearray))
self.data += bytearray(data)
def __str__(self):
""" String representation """
return "{} = {}".format(self.name, self.data)
def __getitem__(self, index):
"""Get byte by index """
return self.data[index]
def __len__(self):
""" Get bytes count """
return len(self.data)
def __eq__(self, prop):
""" Check PropBytes object equality """
if not isinstance(prop, PropBytes):
return False
if self.name != prop.name:
return False
if len(self) != len(prop):
return False
for index in range(len(self)):
if self.data[index] != prop[index]:
return False
return True
[docs]
def copy(self):
""" Create a copy of object """
return PropBytes(self.name, data=self.data)
def append(self, value):
assert isinstance(value, int), "Invalid object type"
assert 0 <= value <= 0xFF, "Invalid byte value {}, use <0 - 255>".format(value)
self.data.append(value)
def pop(self, index):
assert 0 <= index < len(self.data), "Index out of range"
return self.data.pop(index)
def clear(self):
self.data = bytearray()
[docs]
def to_dts(self, tabsize: int = 4, depth: int = 0):
"""
Get string representation
:param tabsize: Tabulator size in count of spaces
:param depth: Start depth for line
"""
result = line_offset(tabsize, depth, self.name)
result += ' = ['
result += ' '.join(["{:02X}".format(byte) for byte in self.data])
result += '];\n'
return result
[docs]
def to_dtb(self, strings: str, pos: int = 0, version: int = Header.MAX_VERSION):
"""
Get blob representation
:param strings:
:param pos:
:param version:
"""
strpos = strings.find(self.name + '\0')
if strpos < 0:
strpos = len(strings)
strings += self.name + '\0'
blob = pack('>III', DTB_PROP, len(self.data), strpos)
blob += bytes(self.data)
if len(blob) % 4:
blob += bytes([0] * (4 - (len(blob) % 4)))
pos += len(blob)
return blob, strings, pos
[docs]
class PropIncBin(PropBytes):
"""Property with bytes as value"""
def __init__(self, name, data=None, file_name=None, rpath=None):
"""
PropIncBin constructor
:param name: Property name
:param data: Data as list, bytes or bytearray
:param file_name: File name
:param rpath: Relative path
"""
super().__init__(name, data)
self.file_name = file_name
self.relative_path = rpath
def __eq__(self, prop):
""" Check PropIncBin object equality """
if not isinstance(prop, PropIncBin):
return False
if self.name != prop.name:
return False
if self.file_name != prop.file_name:
return False
if self.relative_path != prop.relative_path:
return False
if self.data != prop.data:
return False
return True
[docs]
def copy(self):
""" Create a copy of object """
return PropIncBin(self.name, self.data, self.file_name, self.relative_path)
[docs]
def to_dts(self, tabsize: int = 4, depth: int = 0):
"""
Get string representation
:param tabsize: Tabulator size in count of spaces
:param depth: Start depth for line
"""
file_path = self.file_name
if self.relative_path is not None:
file_path = "{}/{}".format(self.relative_path, self.file_name)
result = line_offset(tabsize, depth, self.name)
result += " = /incbin/(\"{}\");\n".format(file_path)
return result
########################################################################################################################
# Node Class
########################################################################################################################
[docs]
class Node(BaseItem):
"""Node representation"""
@property
def props(self):
return self._props
@property
def nodes(self):
return self._nodes
@property
def empty(self):
return False if self.nodes or self.props else True
def __init__(self, name, *args):
"""
Node constructor
:param name: Node name
:param args: List of properties and subnodes
"""
super().__init__(name)
self._props = []
self._nodes = []
for item in args:
self.append(item)
def __str__(self):
""" String representation """
return "< {}: {} props, {} nodes >".format(self.name, len(self.props), len(self.nodes))
def __eq__(self, node):
""" Check node equality """
if not isinstance(node, Node):
return False
if self.name != node.name or \
len(self.props) != len(node.props) or \
len(self.nodes) != len(node.nodes):
return False
for p in self.props:
if p not in node.props:
return False
for n in self.nodes:
if n not in node.nodes:
return False
return True
[docs]
def copy(self):
""" Create a copy of Node object """
node = Node(self.name)
for p in self.props:
node.append(p.copy())
for n in self.nodes:
node.append(n.copy())
return node
[docs]
def get_property(self, name):
"""
Get property object by its name
:param name: Property name
"""
for p in self.props:
if p.name == name:
return p
return None
[docs]
def set_property(self, name, value):
"""
Set property
:param name: Property name
:param value: Property value
"""
if value is None:
new_prop = Property(name)
elif isinstance(value, int):
new_prop = PropWords(name, value)
elif isinstance(value, str):
new_prop = PropStrings(name, value)
elif isinstance(value, list) and isinstance(value[0], int):
new_prop = PropWords(name, *value)
elif isinstance(value, list) and isinstance(value[0], str):
new_prop = PropStrings(name, *value)
elif isinstance(value, (bytes, bytearray)):
new_prop = PropBytes(name, data=value)
else:
raise TypeError('Value type not supported')
new_prop.set_parent(self)
old_prop = self.get_property(name)
if old_prop is None:
self.props.append(new_prop)
else:
index = self.props.index(old_prop)
self.props[index] = new_prop
[docs]
def get_subnode(self, name: str):
"""
Get subnode object by name
:param name: Subnode name
"""
for n in self.nodes:
if n.name == name:
return n
return None
[docs]
def exist_property(self, name: str) -> bool:
"""
Check if property exist and return True if exist else False
:param name: Property name
"""
return False if self.get_property(name) is None else True
[docs]
def exist_subnode(self, name: str) -> bool:
"""
Check if subnode exist and return True if exist else False
:param name: Subnode name
"""
return False if self.get_subnode(name) is None else True
[docs]
def remove_property(self, name: str):
"""
Remove property object by its name.
:param name: Property name
"""
item = self.get_property(name)
if item is not None:
self.props.remove(item)
[docs]
def remove_subnode(self, name: str):
"""
Remove subnode object by its name.
:param name: Subnode name
"""
item = self.get_subnode(name)
if item is not None:
self.nodes.remove(item)
[docs]
def append(self, item):
"""
Append node or property
:param item: The node or property object
"""
assert isinstance(item, (Node, Property)), "Invalid object type, use \"Node\" or \"Property\""
if isinstance(item, Property):
if self.get_property(item.name) is not None:
raise Exception("{}: \"{}\" property already exists".format(self, item.name))
item.set_parent(self)
self.props.append(item)
else:
if self.get_subnode(item.name) is not None:
raise Exception("{}: \"{}\" node already exists".format(self, item.name))
if item is self:
raise Exception("{}: append the same node {}".format(self, item.name))
item.set_parent(self)
self.nodes.append(item)
[docs]
def merge(self, node_obj, replace: bool = True):
"""
Merge two nodes
:param node_obj: Node object
:param replace: If True, replace current properties with the given properties
"""
assert isinstance(node_obj, Node), "Invalid object type"
def get_property_index(name):
for i, p in enumerate(self.props):
if p.name == name:
return i
return None
def get_subnode_index(name):
for i, n in enumerate(self.nodes):
if n.name == name:
return i
return None
for prop in node_obj.props:
index = get_property_index(prop.name)
if index is None:
self.append(prop.copy())
elif prop in self._props:
continue
elif replace:
new_prop = prop.copy()
new_prop.set_parent(self)
self._props[index] = new_prop
else:
pass
for sub_node in node_obj.nodes:
index = get_subnode_index(sub_node.name)
if index is None:
self.append(sub_node.copy())
elif sub_node in self._nodes:
continue
else:
self._nodes[index].merge(sub_node, replace)
[docs]
def to_dts(self, tabsize: int = 4, depth: int = 0) -> str:
"""
Get string representation of NODE object
:param tabsize: Tabulator size in count of spaces
:param depth: Start depth for line
"""
if self._label is not None:
dts = line_offset(tabsize, depth, self._label + ': ' + self.name + ' {\n')
else:
dts = line_offset(tabsize, depth, self.name + ' {\n')
# phantom properties which maintain reference state info
# have names ending with _with_references
# don't write those out to dts file
dts += ''.join(
prop.to_dts(tabsize, depth + 1)
for prop in self._props if prop.name.endswith('_with_references') is False)
dts += ''.join(node.to_dts(tabsize, depth + 1) for node in self._nodes)
dts += line_offset(tabsize, depth, "};\n")
return dts
[docs]
def to_dtb(self, strings: str, pos: int = 0, version: int = Header.MAX_VERSION) -> tuple:
"""
Get NODE in binary blob representation
:param strings:
:param pos:
:param version:
"""
if self.name == '/':
blob = pack('>II', DTB_BEGIN_NODE, 0)
else:
blob = pack('>I', DTB_BEGIN_NODE)
blob += self.name.encode('ascii') + b'\0'
if len(blob) % 4:
blob += pack('b', 0) * (4 - (len(blob) % 4))
pos += len(blob)
for prop in self._props:
# phantom property too maintain reference state should
# not write out to dtb file
if prop.name.endswith('_with_references') is False:
(data, strings, pos) = prop.to_dtb(strings, pos, version)
blob += data
for node in self._nodes:
(data, strings, pos) = node.to_dtb(strings, pos, version)
blob += data
pos += 4
blob += pack('>I', DTB_END_NODE)
return blob, strings, pos