import copy import struct from typing import Any, Dict, List, Optional, Union from typing_extensions import Final # Hack to get around mypy's lack of scoping on types. _renamed_float = float _renamed_bool = bool class NodeException(Exception): """ An exception thrown when we encounter an issue with a property node. """ class Node: """ An object representing one node in the tree structure of a packet. Nodes can have a number of string attributes, and either a value or zero or more children. Note that it is possible and supported for a node to not have a value or children. This also includes a decent amount of constructor helper classmethods to make constructing a tree from source code easier. """ NODE_NAME_CHARS: Final[ str ] = "0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" NODE_TYPE_VOID: Final[int] = 1 NODE_TYPE_S8: Final[int] = 2 NODE_TYPE_U8: Final[int] = 3 NODE_TYPE_S16: Final[int] = 4 NODE_TYPE_U16: Final[int] = 5 NODE_TYPE_S32: Final[int] = 6 NODE_TYPE_U32: Final[int] = 7 NODE_TYPE_S64: Final[int] = 8 NODE_TYPE_U64: Final[int] = 9 NODE_TYPE_BIN: Final[int] = 10 NODE_TYPE_STR: Final[int] = 11 NODE_TYPE_IP4: Final[int] = 12 NODE_TYPE_TIME: Final[int] = 13 NODE_TYPE_FLOAT: Final[int] = 14 NODE_TYPE_DOUBLE: Final[int] = 15 NODE_TYPE_2S8: Final[int] = 16 NODE_TYPE_2U8: Final[int] = 17 NODE_TYPE_2S16: Final[int] = 18 NODE_TYPE_2U16: Final[int] = 19 NODE_TYPE_2S32: Final[int] = 20 NODE_TYPE_2U32: Final[int] = 21 NODE_TYPE_2S64: Final[int] = 22 NODE_TYPE_2U64: Final[int] = 23 NODE_TYPE_2FLOAT: Final[int] = 24 NODE_TYPE_2DOUBLE: Final[int] = 25 NODE_TYPE_3S8: Final[int] = 26 NODE_TYPE_3U8: Final[int] = 27 NODE_TYPE_3S16: Final[int] = 28 NODE_TYPE_3U16: Final[int] = 29 NODE_TYPE_3S32: Final[int] = 30 NODE_TYPE_3U32: Final[int] = 31 NODE_TYPE_3S64: Final[int] = 32 NODE_TYPE_3U64: Final[int] = 33 NODE_TYPE_3FLOAT: Final[int] = 34 NODE_TYPE_3DOUBLE: Final[int] = 35 NODE_TYPE_4S8: Final[int] = 36 NODE_TYPE_4U8: Final[int] = 37 NODE_TYPE_4S16: Final[int] = 38 NODE_TYPE_4U16: Final[int] = 39 NODE_TYPE_4S32: Final[int] = 40 NODE_TYPE_4U32: Final[int] = 41 NODE_TYPE_4S64: Final[int] = 42 NODE_TYPE_4U64: Final[int] = 43 NODE_TYPE_4FLOAT: Final[int] = 44 NODE_TYPE_4DOUBLE: Final[int] = 45 NODE_TYPE_BOOL: Final[int] = 52 NODE_TYPES: Final[Dict[int, Dict[str, Any]]] = { NODE_TYPE_VOID: { "name": "void", "enc": "", "int": False, "composite": False, }, NODE_TYPE_S8: { "name": "s8", "enc": "b", "int": True, "composite": False, }, NODE_TYPE_U8: { "name": "u8", "enc": "B", "int": True, "composite": False, }, NODE_TYPE_S16: { "name": "s16", "enc": "h", "int": True, "composite": False, }, NODE_TYPE_U16: { "name": "u16", "enc": "H", "int": True, "composite": False, }, NODE_TYPE_S32: { "name": "s32", "enc": "i", "int": True, "composite": False, }, NODE_TYPE_U32: { "name": "u32", "enc": "I", "int": True, "composite": False, }, NODE_TYPE_S64: { "name": "s64", "enc": "q", "int": True, "composite": False, }, NODE_TYPE_U64: { "name": "u64", "enc": "Q", "int": True, "composite": False, }, NODE_TYPE_BIN: { "name": "bin", "enc": "s", "int": False, "composite": False, }, NODE_TYPE_STR: { "name": "str", "enc": "s", "int": False, "composite": False, }, NODE_TYPE_IP4: { "name": "ip4", "enc": "4s", "int": False, "composite": False, }, NODE_TYPE_TIME: { "name": "time", "enc": "I", "int": True, "composite": False, }, NODE_TYPE_FLOAT: { "name": "float", "enc": "f", "int": False, "composite": False, }, NODE_TYPE_DOUBLE: { "name": "double", "enc": "d", "int": False, "composite": False, }, NODE_TYPE_2S8: { "name": "2s8", "enc": "bb", "int": True, "composite": True, }, NODE_TYPE_2U8: { "name": "2u8", "enc": "BB", "int": True, "composite": True, }, NODE_TYPE_2S16: { "name": "2s16", "enc": "hh", "int": True, "composite": True, }, NODE_TYPE_2U16: { "name": "2u16", "enc": "HH", "int": True, "composite": True, }, NODE_TYPE_2S32: { "name": "2s32", "enc": "ii", "int": True, "composite": True, }, NODE_TYPE_2U32: { "name": "2u32", "enc": "II", "int": True, "composite": True, }, NODE_TYPE_2S64: { "name": "2s64", "enc": "qq", "int": True, "composite": True, }, NODE_TYPE_2U64: { "name": "2u64", "enc": "QQ", "int": True, "composite": True, }, NODE_TYPE_2FLOAT: { "name": "2float", "enc": "ff", "int": False, "composite": True, }, NODE_TYPE_2DOUBLE: { "name": "2double", "enc": "dd", "int": False, "composite": True, }, NODE_TYPE_3S8: { "name": "3s8", "enc": "bbb", "int": True, "composite": True, }, NODE_TYPE_3U8: { "name": "3u8", "enc": "BBB", "int": True, "composite": True, }, NODE_TYPE_3S16: { "name": "3s16", "enc": "hhh", "int": True, "composite": True, }, NODE_TYPE_3U16: { "name": "3u16", "enc": "HHH", "int": True, "composite": True, }, NODE_TYPE_3S32: { "name": "3s32", "enc": "iii", "int": True, "composite": True, }, NODE_TYPE_3U32: { "name": "3u32", "enc": "III", "int": True, "composite": True, }, NODE_TYPE_3S64: { "name": "3s64", "enc": "qqq", "int": True, "composite": True, }, NODE_TYPE_3U64: { "name": "3u64", "enc": "QQQ", "int": True, "composite": True, }, NODE_TYPE_3FLOAT: { "name": "3float", "enc": "fff", "int": False, "composite": True, }, NODE_TYPE_3DOUBLE: { "name": "3double", "enc": "ddd", "int": False, "composite": True, }, NODE_TYPE_4U8: { "name": "4u8", "enc": "BBBB", "int": True, "composite": True, }, NODE_TYPE_4S8: { "name": "4s8", "enc": "bbbb", "int": True, "composite": True, }, NODE_TYPE_4U16: { "name": "4u16", "enc": "HHHH", "int": True, "composite": True, }, NODE_TYPE_4S16: { "name": "4s16", "enc": "hhhh", "int": True, "composite": True, }, NODE_TYPE_4S32: { "name": "4s32", "enc": "iiii", "int": True, "composite": True, }, NODE_TYPE_4U32: { "name": "4u32", "enc": "IIII", "int": True, "composite": True, }, NODE_TYPE_4S64: { "name": "4s64", "enc": "qqqq", "int": True, "composite": True, }, NODE_TYPE_4U64: { "name": "4u64", "enc": "QQQQ", "int": True, "composite": True, }, NODE_TYPE_4FLOAT: { "name": "4float", "enc": "ffff", "int": False, "composite": True, }, NODE_TYPE_4DOUBLE: { "name": "4double", "enc": "dddd", "int": False, "composite": True, }, NODE_TYPE_BOOL: { "name": "bool", "enc": "b", "int": False, "composite": False, }, } ARRAY_BIT: Final[int] = 0x40 ATTR_TYPE: Final[int] = 0x2E END_OF_NODE: Final[int] = 0xFE END_OF_DOCUMENT: Final[int] = 0xFF @staticmethod def void(name: str) -> "Node": return Node(name=name, type=Node.NODE_TYPE_VOID) @staticmethod def string(name: str, value: str) -> "Node": return Node(name=name, type=Node.NODE_TYPE_STR, value=value) @staticmethod def binary(name: str, value: bytes) -> "Node": return Node(name=name, type=Node.NODE_TYPE_BIN, value=value) @staticmethod def float(name: str, value: _renamed_float) -> "Node": return Node(name=name, type=Node.NODE_TYPE_FLOAT, value=value) @staticmethod def bool(name: str, value: _renamed_bool) -> "Node": return Node(name=name, type=Node.NODE_TYPE_BOOL, value=value) @staticmethod def ipv4(name: str, value: str) -> "Node": return Node(name=name, type=Node.NODE_TYPE_IP4, value=value) @staticmethod def time(name: str, value: int) -> "Node": return Node(name=name, type=Node.NODE_TYPE_TIME, value=value) @staticmethod def u8(name: str, value: int) -> "Node": Node.__validate(Node.NODE_TYPE_U8, name, value) return Node(name=name, type=Node.NODE_TYPE_U8, value=value) @staticmethod def s8(name: str, value: int) -> "Node": Node.__validate(Node.NODE_TYPE_S8, name, value) return Node(name=name, type=Node.NODE_TYPE_S8, value=value) @staticmethod def u16(name: str, value: int) -> "Node": Node.__validate(Node.NODE_TYPE_U16, name, value) return Node(name=name, type=Node.NODE_TYPE_U16, value=value) @staticmethod def s16(name: str, value: int) -> "Node": Node.__validate(Node.NODE_TYPE_S16, name, value) return Node(name=name, type=Node.NODE_TYPE_S16, value=value) @staticmethod def u32(name: str, value: int) -> "Node": Node.__validate(Node.NODE_TYPE_U32, name, value) return Node(name=name, type=Node.NODE_TYPE_U32, value=value) @staticmethod def s32(name: str, value: int) -> "Node": Node.__validate(Node.NODE_TYPE_S32, name, value) return Node(name=name, type=Node.NODE_TYPE_S32, value=value) @staticmethod def u64(name: str, value: int) -> "Node": Node.__validate(Node.NODE_TYPE_U64, name, value) return Node(name=name, type=Node.NODE_TYPE_U64, value=value) @staticmethod def s64(name: str, value: int) -> "Node": Node.__validate(Node.NODE_TYPE_S64, name, value) return Node(name=name, type=Node.NODE_TYPE_S64, value=value) @staticmethod def time_array(name: str, values: List[int]) -> "Node": return Node(name=name, type=Node.NODE_TYPE_TIME, array=True, value=values) @staticmethod def float_array(name: str, values: List[_renamed_float]) -> "Node": return Node(name=name, type=Node.NODE_TYPE_FLOAT, array=True, value=values) @staticmethod def bool_array(name: str, values: List[_renamed_bool]) -> "Node": return Node(name=name, type=Node.NODE_TYPE_BOOL, array=True, value=values) @staticmethod def u8_array(name: str, values: List[int]) -> "Node": for value in values: Node.__validate(Node.NODE_TYPE_U8, name, value) return Node(name=name, type=Node.NODE_TYPE_U8, array=True, value=values) @staticmethod def s8_array(name: str, values: List[int]) -> "Node": for value in values: Node.__validate(Node.NODE_TYPE_S8, name, value) return Node(name=name, type=Node.NODE_TYPE_S8, array=True, value=values) @staticmethod def u16_array(name: str, values: List[int]) -> "Node": for value in values: Node.__validate(Node.NODE_TYPE_U16, name, value) return Node(name=name, type=Node.NODE_TYPE_U16, array=True, value=values) @staticmethod def s16_array(name: str, values: List[int]) -> "Node": for value in values: Node.__validate(Node.NODE_TYPE_S16, name, value) return Node(name=name, type=Node.NODE_TYPE_S16, array=True, value=values) @staticmethod def u32_array(name: str, values: List[int]) -> "Node": for value in values: Node.__validate(Node.NODE_TYPE_U32, name, value) return Node(name=name, type=Node.NODE_TYPE_U32, array=True, value=values) @staticmethod def s32_array(name: str, values: List[int]) -> "Node": for value in values: Node.__validate(Node.NODE_TYPE_S32, name, value) return Node(name=name, type=Node.NODE_TYPE_S32, array=True, value=values) @staticmethod def u64_array(name: str, values: List[int]) -> "Node": for value in values: Node.__validate(Node.NODE_TYPE_U64, name, value) return Node(name=name, type=Node.NODE_TYPE_U64, array=True, value=values) @staticmethod def s64_array(name: str, values: List[int]) -> "Node": for value in values: Node.__validate(Node.NODE_TYPE_S64, name, value) return Node(name=name, type=Node.NODE_TYPE_S64, array=True, value=values) @staticmethod def fouru8(name: str, values: List[int]) -> "Node": for value in values: Node.__validate(Node.NODE_TYPE_U8, name, value) return Node(name=name, type=Node.NODE_TYPE_4U8, value=values) @staticmethod def typename_to_type(typename: str) -> Optional[int]: """ Given a string typename as would be output in an XML conversion or found in the above NODE_TYPES table, return an integer node type that would be valid for a binary node. Parameters: typename - String corresponding to a node type. Returns: An integer specifying the node type or None if not found. """ for nodetype in Node.NODE_TYPES: if typename.lower() == Node.NODE_TYPES[nodetype]["name"]: return nodetype return None @staticmethod def __validate(nodetype: int, name: str, value: int) -> None: if nodetype == Node.NODE_TYPE_U8: if value < 0 or value > 255: raise NodeException(f"Invalid value {value} for u8 {name}") elif nodetype == Node.NODE_TYPE_S8: if value < -128 or value > 127: raise NodeException(f"Invalid value {value} for s8 {name}") elif nodetype == Node.NODE_TYPE_U16: if value < 0 or value > 65535: raise NodeException(f"Invalid value {value} for u16 {name}") elif nodetype == Node.NODE_TYPE_S16: if value < -32768 or value > 32767: raise NodeException(f"Invalid value {value} for s16 {name}") elif nodetype == Node.NODE_TYPE_U32: if value < 0 or value > 4294967295: raise NodeException(f"Invalid value {value} for u32 {name}") elif nodetype == Node.NODE_TYPE_S32: if value < -2147483648 or value > 2147483647: raise NodeException(f"Invalid value {value} for s32 {name}") elif nodetype == Node.NODE_TYPE_U64: if value < 0 or value > 18446744073709551615: raise NodeException(f"Invalid value {value} for u64 {name}") elif nodetype == Node.NODE_TYPE_S64: if value < -9223372036854775808 or value > 9223372036854775807: raise NodeException(f"Invalid value {value} for s32 {name}") def __init__( self, name: Optional[str] = None, type: Optional[int] = None, array: Optional[_renamed_bool] = None, value: Optional[Any] = None, ) -> None: """ Initialize a node, with an optional name and type. Parameters: name - A string specifying the name of the node type - An integer specifying the type of the node. Should be a valid type as found in Node.NODE_TYPES with an optional Node.ARRAY_BIT set. array - A boolean specifying whether or not this node is an array. If not provided, will extract the array bit flag from the type. value - A mixed value corresponding to the type that this node should be initialized with. """ self.__name: Optional[str] = None self.__array = False self.__translated_type: Optional[Dict[str, Any]] = None self.__type: Optional[int] = None self.__attrs: Dict[str, str] = {} self.__value: Any = None self.__children: List[Node] = [] if name is not None: self.set_name(name) if type is not None: self.set_type(type, array=array) if value is not None: self.set_value(value) def set_name(self, name: str) -> None: """ Set the name of the node to a new string. Parameters: name - A string specifying the node name. Should be made up of only NODE_NAME_CHARS characters. """ # Ensure it isn't a violation for char in name: if char not in Node.NODE_NAME_CHARS: raise NodeException(f"Invalid node name {name}") self.__name = name @property def name(self) -> str: """ Get the name of the node as a string. Returns: A string node name. """ if self.__name is None: raise Exception("Logic error, tried to fetch name before setting!") return self.__name def set_type(self, type: int, array: Optional[_renamed_bool] = None) -> None: """ Set the type of the node to a new integer type, as specified in Node.NODE_TYPES. Parameters: type - An integer type to set the node type as. array - A boolean specifying whether this node is an array or not. If not provided this function will extract the array bit from the provided type integer. """ if array is not None: if array: type = type | Node.ARRAY_BIT else: type = type & (~Node.ARRAY_BIT) if (type & Node.ARRAY_BIT) != 0: self.__array = True try: self.__translated_type = Node.NODE_TYPES[type & (~Node.ARRAY_BIT)] self.__type = type except KeyError: raise NodeException(f"Unknown node type {type} on node name {self.__name}") @property def type(self) -> int: """ Returns the underlying data type for this node. Returns: An integer node type. Should correspond with node types, but note that the array bit ARRAY_BIT might be set. """ if self.__type is None: raise Exception("Logic error, tried to fetch type before setting!") return self.__type @property def data_type(self) -> str: """ Returns the data type name based on the node's type. Returns: A string data type name. This string can be fed to typename_to_type to get the original type back. """ if self.__translated_type is None: raise Exception( "Logic error, tried to fetch data type before setting type!" ) return self.__translated_type["name"] @property def data_length(self) -> Optional[int]: """ Returns the number of bytes used by the encoding, based on the node's type. If this is a binary blob or a string, returns None. For array types, this represents the size of one element in bytes. Returns: An integer data length, or None if this node's element has variable length. """ if self.__translated_type is None: raise Exception( "Logic error, tried to fetch data length before setting type!" ) if self.__translated_type["name"] in {"bin", "str"}: return None return struct.calcsize(self.__translated_type["enc"]) @property def data_encoding(self) -> str: """ Returns the python struct encoding character used to encode/decode this type. Returns: A character that can be passed to struct.pack or struct.unpack. """ if self.__translated_type is None: raise Exception( "Logic error, tried to fetch data encoding before setting type!" ) return self.__translated_type["enc"] def set_attribute(self, attr: str, val: str = "") -> None: """ Set an attribute to a particular string value on this node. Parameters: attr - A string attribute to set on the node. val - The string value to set the attribute value to. Defaults to empty string if not provided. """ self.__attrs[attr] = val def attribute(self, attr: str, default: Optional[str] = None) -> Optional[str]: """ Get an attribute based on a string, or None if nonexistent. Parameters: attr - A string attribute to look up. Returns: The attribute value as a string. """ return self.__attrs.get(attr, default) def add_child(self, child: "Node") -> None: """ Add a child Node to this node. Parameters: child - A Node to set as a child to this node. """ if not isinstance(child, Node): raise NodeException("Invalid child") self.__children.append(child) def child(self, name: str) -> Optional["Node"]: """ Find a child by name. Parameters: name - String name of the child to find. If one or more slashes is included, traverses each name, looking up that child. Returns: A Node if a child was found by name, or None if not. """ tree = name.split("/", 1) for child in self.__children: if child.name == tree[0]: if len(tree) == 1: # We don't have any more nodes to traverse. return child else: # We have more nodes, try to get the next. return child.child(tree[1]) # There was no child by this name, return None. return None def child_value(self, name: str) -> Optional[Any]: """ Find a child by name, and look up its value. Parameters: name - String name of child to find. Supports slashes similarly to the above child() method. Returns: A value of the child node if the child was found, or None if not. Also returns None if the child is a void node. """ child = self.child(name) if child is None: return None return child.value @property def children(self) -> List["Node"]: """ Wrapper for accessing children. Returns: A list of Node instances which are children of this Node. """ return self.__children @property def attributes(self) -> Dict[str, str]: """ Wrapper for accessing attributes. Returns: A dictionary keyed by attribute name whose values are strings. """ return self.__attrs @property def is_array(self) -> _renamed_bool: """ Wrapper for accessing array type. Returns: True if this Node is an array, False otherwise. """ return self.__array @property def is_composite(self) -> _renamed_bool: """ Returns whether or not this element is a composite type (basically an array, but packed differently). Returns: True if this Node is a composite type, False otherwise. """ if self.__translated_type is None: raise Exception( "Logic error, tried to fetch composite determination before setting type!" ) return self.__translated_type["composite"] def set_value(self, val: Any) -> None: """ Sets the value of this node. If this node is an array type (see Node.array boolean), expects an array. If not, expects a scalar value. Paramters: val - A mixed value to set the node to. """ is_array = isinstance(val, (list, tuple)) if self.__translated_type is None: raise Exception("Logic error, tried to set value before setting type!") translated_type: Dict[str, Any] = self.__translated_type # Handle composite types if translated_type["composite"]: if not is_array: raise NodeException("Input is not array, expected array") if len(val) != len(translated_type["enc"]): raise NodeException( f'Input array for {translated_type["name"]} expected to be {len(translated_type["enc"])} elements!' ) is_array = False if is_array != self.__array: raise NodeException( f'Input {"is" if is_array else "is not"} array, expected {"array" if self.__array else "scalar"}' ) def val_to_str(val: Any) -> Union[str, bytes]: if translated_type["name"] == "bool": # Support user-built boolean types if val is True: return "true" if val is False: return "false" # Support construction from binary return "true" if val != 0 else "false" elif translated_type["name"] == "float": return str(val) elif translated_type["name"] == "ip4": try: # Support construction from binary ip = struct.unpack("BBBB", val) return f"{ip[0]}.{ip[1]}.{ip[2]}.{ip[3]}" except (struct.error, TypeError): # Assume that its user-built string? if isinstance(val, str): if len(val.split(".")) == 4: return val raise NodeException(f"Invalid value {val} for IP4 type") elif translated_type["int"]: return str(val) else: # This could return either a string or bytes. return val if is_array or translated_type["composite"]: self.__value = [val_to_str(v) for v in val] else: self.__value = val_to_str(val) @property def value(self) -> Any: """ Gets the value of this node. If this node is an array type, returns an array. If no, returns a scalar. Returns: A mixed value corresponding to this node's value. The returned value will be of the correct data type. """ if self.__translated_type is None: raise Exception("Logic error, tried to get value before setting type!") translated_type: Dict[str, Any] = self.__translated_type def str_to_val(string: Union[str, bytes]) -> Any: if translated_type["name"] == "bool": return string == "true" elif translated_type["name"] == "float": return float(string) elif translated_type["name"] == "ip4": if not isinstance(string, str): raise Exception("Logic error, expected a string!") ip = [int(tup) for tup in string.split(".")] return struct.pack("BBBB", ip[0], ip[1], ip[2], ip[3]) elif translated_type["int"]: return int(string) else: # At this point, we could be a string or bytes. return string if self.__array or translated_type["composite"]: return [str_to_val(v) for v in self.__value] else: return str_to_val(self.__value) def __to_xml(self, depth: int) -> str: """ Convert this node, attributes and all children to an XML-like representation of the tree. Parameters: depth - Number of levels deep into the tree we currently are. If we shouldn't output any depth, this should be set to None. Returns: A string representing the XML-like data for this node and all children. """ if self.__translated_type is None: raise Exception( "Logic error, tried to get XML representation before setting type!" ) translated_type: Dict[str, Any] = self.__translated_type attrs_dict = copy.deepcopy(self.__attrs) order = sorted(attrs_dict.keys()) if self.data_length != 0: # Represent type and length if self.__array: if self.__value is None: attrs_dict["__count"] = "0" else: attrs_dict["__count"] = str(len(self.__value)) order.insert(0, "__count") attrs_dict["__type"] = translated_type["name"] order.insert(0, "__type") def escape(val: Any, attr: _renamed_bool = False) -> str: if isinstance(val, str): val = val.replace("&", "&") val = val.replace("<", "<") val = val.replace(">", ">") val = val.replace("'", "'") val = val.replace('"', """) if attr: val = val.replace("\r", " ") val = val.replace("\n", " ") return val else: return str(val) if attrs_dict: attrs = " " + " ".join( [f'{attr}="{escape(attrs_dict[attr], attr=True)}"' for attr in order] ) else: attrs = "" def get_val() -> str: if self.__array or translated_type["composite"]: if self.__value is None: vals = "" else: vals = " ".join([val for val in self.__value]) elif translated_type["name"] == "str": vals = escape(self.__value) elif translated_type["name"] == "bin": # Convert to a hex string def bin_to_hex(binary: int) -> str: val = hex(binary)[2:] while len(val) < 2: val = "0" + val return val vals = "".join([bin_to_hex(v) for v in self.__value]) else: vals = str(self.__value) return vals if self.__children: # Has children nodes children = [child.__to_xml(depth=depth + 1) for child in self.__children] if self.data_length != 0: # Has children and a value children = [ f'{" " * ((depth + 1) * 4)}{get_val()}\n', ] + children string = f'{" " * (depth * 4)}<{self.__name}{attrs}>\n{"".join(children)}{" " * (depth * 4)}\n' else: # Doesn't have children nodes if self.data_length == 0: # Void node string = f'{" " * (depth * 4)}<{self.__name}{attrs} />\n' else: # Node with values string = f'{" " * (depth * 4)}<{self.__name}{attrs}>{get_val()}\n' return string def __str__(self) -> str: """ Convenience function to auto-convert this node and children to XML if printed. Returns: A string that is parseable as valid XML, pretty printed. """ return self.__to_xml(0) def __eq__(self, other: object) -> _renamed_bool: """ Convenience function for comparing two nodes. Parameters: other - Another property node to compare this to. Returns: True if the name, value, all attributes and children match this node, False otherwise. """ if not isinstance(other, Node): return False try: if self.__name != other.__name: return False if self.__array != other.__array: return False if self.__type != other.__type: return False if not self.__array: if self.__value != other.__value: return False else: if len(self.__value) != len(other.__value): return False for i in range(len(self.__value)): if self.__value[i] != other.__value[i]: return False for attr in self.__attrs: if other.attribute(attr) != self.attribute(attr): return False for attr in other.__attrs: if self.attribute(attr) != other.attribute(attr): return False if len(self.__children) != len(other.__children): return False for i in range(len(self.__children)): if self.__children[i] != other.__children[i]: return False return True except Exception: return False def __ne__(self, other: object) -> _renamed_bool: """ Convenience function for comparing two nodes. Parameters: other - Another Node to compare to. Returns: True if this node doesn't equal the other node, False if it does equal. """ return not self.__eq__(other)