457 lines
16 KiB
Python
457 lines
16 KiB
Python
|
from functools import lru_cache
|
||
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||
|
|
||
|
from .errors import InvalidSignatureError, SignatureBodyMismatchError
|
||
|
from .validators import is_object_path_valid
|
||
|
|
||
|
|
||
|
class SignatureType:
|
||
|
"""A class that represents a single complete type within a signature.
|
||
|
|
||
|
This class is not meant to be constructed directly. Use the :class:`SignatureTree`
|
||
|
class to parse signatures.
|
||
|
|
||
|
:ivar ~.signature: The signature of this complete type.
|
||
|
:vartype ~.signature: str
|
||
|
|
||
|
:ivar children: A list of child types if this is a container type. Arrays \
|
||
|
have one child type, dict entries have two child types (key and value), and \
|
||
|
structs have child types equal to the number of struct members.
|
||
|
:vartype children: list(:class:`SignatureType`)
|
||
|
"""
|
||
|
|
||
|
_tokens = "ybnqiuxtdsogavh({"
|
||
|
__slots__ = ("token", "children", "_signature")
|
||
|
|
||
|
def __init__(self, token: str) -> None:
|
||
|
"""Init a new SignatureType."""
|
||
|
self.token: str = token
|
||
|
self.children: List[SignatureType] = []
|
||
|
self._signature: Optional[str] = None
|
||
|
|
||
|
def __eq__(self, other: Any) -> bool:
|
||
|
"""Compare this type to another type or signature string."""
|
||
|
if type(other) is SignatureType:
|
||
|
return self.signature == other.signature
|
||
|
return super().__eq__(other)
|
||
|
|
||
|
def _collapse(self) -> str:
|
||
|
"""Collapse this type into a signature string."""
|
||
|
if self.token not in "a({":
|
||
|
return self.token
|
||
|
|
||
|
signature = [self.token]
|
||
|
|
||
|
for child in self.children:
|
||
|
signature.append(child._collapse())
|
||
|
|
||
|
if self.token == "(":
|
||
|
signature.append(")")
|
||
|
elif self.token == "{":
|
||
|
signature.append("}")
|
||
|
|
||
|
return "".join(signature)
|
||
|
|
||
|
@property
|
||
|
def signature(self) -> str:
|
||
|
if self._signature is not None:
|
||
|
return self._signature
|
||
|
self._signature = self._collapse()
|
||
|
return self._signature
|
||
|
|
||
|
@staticmethod
|
||
|
def _parse_next(signature: str) -> Tuple["SignatureType", str]:
|
||
|
if not signature:
|
||
|
raise InvalidSignatureError("Cannot parse an empty signature")
|
||
|
|
||
|
token = signature[0]
|
||
|
|
||
|
if token not in SignatureType._tokens:
|
||
|
raise InvalidSignatureError(f'got unexpected token: "{token}"')
|
||
|
|
||
|
# container types
|
||
|
if token == "a":
|
||
|
self = SignatureType("a")
|
||
|
(child, signature) = SignatureType._parse_next(signature[1:])
|
||
|
if not child:
|
||
|
raise InvalidSignatureError("missing type for array")
|
||
|
self.children.append(child)
|
||
|
return (self, signature)
|
||
|
elif token == "(":
|
||
|
self = SignatureType("(")
|
||
|
signature = signature[1:]
|
||
|
while True:
|
||
|
(child, signature) = SignatureType._parse_next(signature)
|
||
|
if not signature:
|
||
|
raise InvalidSignatureError('missing closing ")" for struct')
|
||
|
self.children.append(child)
|
||
|
if signature[0] == ")":
|
||
|
return (self, signature[1:])
|
||
|
elif token == "{":
|
||
|
self = SignatureType("{")
|
||
|
signature = signature[1:]
|
||
|
(key_child, signature) = SignatureType._parse_next(signature)
|
||
|
if not key_child or len(key_child.children):
|
||
|
raise InvalidSignatureError("expected a simple type for dict entry key")
|
||
|
self.children.append(key_child)
|
||
|
(value_child, signature) = SignatureType._parse_next(signature)
|
||
|
if not value_child:
|
||
|
raise InvalidSignatureError("expected a value for dict entry")
|
||
|
if not signature or signature[0] != "}":
|
||
|
raise InvalidSignatureError('missing closing "}" for dict entry')
|
||
|
self.children.append(value_child)
|
||
|
return (self, signature[1:])
|
||
|
|
||
|
# basic type
|
||
|
return (SignatureType(token), signature[1:])
|
||
|
|
||
|
def _verify_byte(self, body: Any) -> None:
|
||
|
BYTE_MIN = 0x00
|
||
|
BYTE_MAX = 0xFF
|
||
|
if not isinstance(body, int):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus BYTE type "y" must be Python type "int", got {type(body)}'
|
||
|
)
|
||
|
if body < BYTE_MIN or body > BYTE_MAX:
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f"DBus BYTE type must be between {BYTE_MIN} and {BYTE_MAX}"
|
||
|
)
|
||
|
|
||
|
def _verify_boolean(self, body: Any) -> None:
|
||
|
if not isinstance(body, bool):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus BOOLEAN type "b" must be Python type "bool", got {type(body)}'
|
||
|
)
|
||
|
|
||
|
def _verify_int16(self, body: Any) -> None:
|
||
|
INT16_MIN = -0x7FFF - 1
|
||
|
INT16_MAX = 0x7FFF
|
||
|
if not isinstance(body, int):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus INT16 type "n" must be Python type "int", got {type(body)}'
|
||
|
)
|
||
|
elif body > INT16_MAX or body < INT16_MIN:
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus INT16 type "n" must be between {INT16_MIN} and {INT16_MAX}'
|
||
|
)
|
||
|
|
||
|
def _verify_uint16(self, body: Any) -> None:
|
||
|
UINT16_MIN = 0
|
||
|
UINT16_MAX = 0xFFFF
|
||
|
if not isinstance(body, int):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus UINT16 type "q" must be Python type "int", got {type(body)}'
|
||
|
)
|
||
|
elif body > UINT16_MAX or body < UINT16_MIN:
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus UINT16 type "q" must be between {UINT16_MIN} and {UINT16_MAX}'
|
||
|
)
|
||
|
|
||
|
def _verify_int32(self, body: int) -> None:
|
||
|
INT32_MIN = -0x7FFFFFFF - 1
|
||
|
INT32_MAX = 0x7FFFFFFF
|
||
|
if not isinstance(body, int):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus INT32 type "i" must be Python type "int", got {type(body)}'
|
||
|
)
|
||
|
elif body > INT32_MAX or body < INT32_MIN:
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus INT32 type "i" must be between {INT32_MIN} and {INT32_MAX}'
|
||
|
)
|
||
|
|
||
|
def _verify_uint32(self, body: Any) -> None:
|
||
|
UINT32_MIN = 0
|
||
|
UINT32_MAX = 0xFFFFFFFF
|
||
|
if not isinstance(body, int):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus UINT32 type "u" must be Python type "int", got {type(body)}'
|
||
|
)
|
||
|
elif body > UINT32_MAX or body < UINT32_MIN:
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus UINT32 type "u" must be between {UINT32_MIN} and {UINT32_MAX}'
|
||
|
)
|
||
|
|
||
|
def _verify_int64(self, body: Any) -> None:
|
||
|
INT64_MAX = 9223372036854775807
|
||
|
INT64_MIN = -INT64_MAX - 1
|
||
|
if not isinstance(body, int):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus INT64 type "x" must be Python type "int", got {type(body)}'
|
||
|
)
|
||
|
elif body > INT64_MAX or body < INT64_MIN:
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus INT64 type "x" must be between {INT64_MIN} and {INT64_MAX}'
|
||
|
)
|
||
|
|
||
|
def _verify_uint64(self, body: Any) -> None:
|
||
|
UINT64_MIN = 0
|
||
|
UINT64_MAX = 18446744073709551615
|
||
|
if not isinstance(body, int):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus UINT64 type "t" must be Python type "int", got {type(body)}'
|
||
|
)
|
||
|
elif body > UINT64_MAX or body < UINT64_MIN:
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus UINT64 type "t" must be between {UINT64_MIN} and {UINT64_MAX}'
|
||
|
)
|
||
|
|
||
|
def _verify_double(self, body: Any) -> None:
|
||
|
if not isinstance(body, (float, int)):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus DOUBLE type "d" must be Python type "float" or "int", got {type(body)}'
|
||
|
)
|
||
|
|
||
|
def _verify_unix_fd(self, body: Any) -> None:
|
||
|
try:
|
||
|
self._verify_uint32(body)
|
||
|
except SignatureBodyMismatchError:
|
||
|
raise SignatureBodyMismatchError(
|
||
|
'DBus UNIX_FD type "h" must be a valid UINT32'
|
||
|
)
|
||
|
|
||
|
def _verify_object_path(self, body: Any) -> None:
|
||
|
if not is_object_path_valid(body):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
'DBus OBJECT_PATH type "o" must be a valid object path'
|
||
|
)
|
||
|
|
||
|
def _verify_string(self, body: Any) -> None:
|
||
|
if not isinstance(body, str):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus STRING type "s" must be Python type "str", got {type(body)}'
|
||
|
)
|
||
|
|
||
|
def _verify_signature(self, body: Any) -> None:
|
||
|
# I guess we could run it through the SignatureTree parser instead
|
||
|
if not isinstance(body, str):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus SIGNATURE type "g" must be Python type "str", got {type(body)}'
|
||
|
)
|
||
|
if len(body.encode()) > 0xFF:
|
||
|
raise SignatureBodyMismatchError(
|
||
|
'DBus SIGNATURE type "g" must be less than 256 bytes'
|
||
|
)
|
||
|
|
||
|
def _verify_array(self, body: Any) -> None:
|
||
|
child_type = self.children[0]
|
||
|
|
||
|
if child_type.token == "{":
|
||
|
if not isinstance(body, dict):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus ARRAY type "a" with DICT_ENTRY child must be Python type "dict", got {type(body)}'
|
||
|
)
|
||
|
for key, value in body.items():
|
||
|
child_type.children[0].verify(key)
|
||
|
child_type.children[1].verify(value)
|
||
|
elif child_type.token == "y":
|
||
|
if not isinstance(body, (bytearray, bytes)):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus ARRAY type "a" with BYTE child must be Python type "bytes", got {type(body)}'
|
||
|
)
|
||
|
# no need to verify children
|
||
|
else:
|
||
|
if not isinstance(body, list):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus ARRAY type "a" must be Python type "list", got {type(body)}'
|
||
|
)
|
||
|
for member in body:
|
||
|
child_type.verify(member)
|
||
|
|
||
|
def _verify_struct(self, body: Any) -> None:
|
||
|
if not isinstance(body, (list, tuple)):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus STRUCT type "(" must be Python type "list" or "tuple", got {type(body)}'
|
||
|
)
|
||
|
|
||
|
if len(body) != len(self.children):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
'DBus STRUCT type "(" must have Python list members equal to the number of struct type members'
|
||
|
)
|
||
|
|
||
|
for i, member in enumerate(body):
|
||
|
self.children[i].verify(member)
|
||
|
|
||
|
def _verify_variant(self, body: Any) -> None:
|
||
|
# a variant signature and value is valid by construction
|
||
|
if not isinstance(body, Variant):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f'DBus VARIANT type "v" must be Python type "Variant", got {type(body)}'
|
||
|
)
|
||
|
|
||
|
def verify(self, body: Any) -> bool:
|
||
|
"""Verify that the body matches this type.
|
||
|
|
||
|
:returns: True if the body matches this type.
|
||
|
:raises:
|
||
|
:class:`SignatureBodyMismatchError` if the body does not match this type.
|
||
|
"""
|
||
|
if body is None:
|
||
|
raise SignatureBodyMismatchError('Cannot serialize Python type "None"')
|
||
|
validator = self.validators.get(self.token)
|
||
|
if validator:
|
||
|
validator(self, body)
|
||
|
else:
|
||
|
raise Exception(f"cannot verify type with token {self.token}")
|
||
|
|
||
|
return True
|
||
|
|
||
|
validators: Dict[str, Callable[["SignatureType", Any], None]] = {
|
||
|
"y": _verify_byte,
|
||
|
"b": _verify_boolean,
|
||
|
"n": _verify_int16,
|
||
|
"q": _verify_uint16,
|
||
|
"i": _verify_int32,
|
||
|
"u": _verify_uint32,
|
||
|
"x": _verify_int64,
|
||
|
"t": _verify_uint64,
|
||
|
"d": _verify_double,
|
||
|
"h": _verify_uint32,
|
||
|
"o": _verify_string,
|
||
|
"s": _verify_string,
|
||
|
"g": _verify_signature,
|
||
|
"a": _verify_array,
|
||
|
"(": _verify_struct,
|
||
|
"v": _verify_variant,
|
||
|
}
|
||
|
|
||
|
|
||
|
class SignatureTree:
|
||
|
"""A class that represents a signature as a tree structure for conveniently
|
||
|
working with DBus signatures.
|
||
|
|
||
|
This class will not normally be used directly by the user.
|
||
|
|
||
|
:ivar types: A list of parsed complete types.
|
||
|
:vartype types: list(:class:`SignatureType`)
|
||
|
|
||
|
:ivar ~.signature: The signature of this signature tree.
|
||
|
:vartype ~.signature: str
|
||
|
|
||
|
:raises:
|
||
|
:class:`InvalidSignatureError` if the given signature is not valid.
|
||
|
"""
|
||
|
|
||
|
__slots__ = ("signature", "types")
|
||
|
|
||
|
def __init__(self, signature: str = "") -> None:
|
||
|
self.signature = signature
|
||
|
|
||
|
self.types: List[SignatureType] = []
|
||
|
|
||
|
if len(signature) > 0xFF:
|
||
|
raise InvalidSignatureError("A signature must be less than 256 characters")
|
||
|
|
||
|
while signature:
|
||
|
(type_, signature) = SignatureType._parse_next(signature)
|
||
|
self.types.append(type_)
|
||
|
|
||
|
def __eq__(self, other: Any) -> bool:
|
||
|
if type(other) is SignatureTree:
|
||
|
return self.signature == other.signature
|
||
|
return super().__eq__(other)
|
||
|
|
||
|
def verify(self, body: List[Any]) -> bool:
|
||
|
"""Verifies that the give body matches this signature tree
|
||
|
|
||
|
:param body: the body to verify for this tree
|
||
|
:type body: list(Any)
|
||
|
|
||
|
:returns: True if the signature matches the body or an exception if not.
|
||
|
|
||
|
:raises:
|
||
|
:class:`SignatureBodyMismatchError` if the signature does not match the body.
|
||
|
"""
|
||
|
if not isinstance(body, list):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f"The body must be a list (got {type(body)})"
|
||
|
)
|
||
|
if len(body) != len(self.types):
|
||
|
raise SignatureBodyMismatchError(
|
||
|
f"The body has the wrong number of types (got {len(body)}, expected {len(self.types)})"
|
||
|
)
|
||
|
for i, type_ in enumerate(self.types):
|
||
|
type_.verify(body[i])
|
||
|
|
||
|
return True
|
||
|
|
||
|
|
||
|
class Variant:
|
||
|
"""A class to represent a DBus variant (type "v").
|
||
|
|
||
|
This class is used in message bodies to represent variants. The user can
|
||
|
expect a value in the body with type "v" to use this class and can
|
||
|
construct this class directly for use in message bodies sent over the bus.
|
||
|
|
||
|
:ivar signature: The signature for this variant. Must be a single complete type.
|
||
|
:vartype signature: str or SignatureTree or SignatureType
|
||
|
|
||
|
:ivar value: The value of this variant. Must correspond to the signature.
|
||
|
:vartype value: Any
|
||
|
|
||
|
:raises:
|
||
|
:class:`InvalidSignatureError` if the signature is not valid.
|
||
|
:class:`SignatureBodyMismatchError` if the signature does not match the body.
|
||
|
"""
|
||
|
|
||
|
__slots__ = ("type", "signature", "value")
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
signature: Union[str, SignatureTree, SignatureType],
|
||
|
value: Any,
|
||
|
verify: bool = True,
|
||
|
) -> None:
|
||
|
"""Init a new Variant."""
|
||
|
self._init_variant(signature, value, verify)
|
||
|
|
||
|
def _init_variant(
|
||
|
self,
|
||
|
signature: Union[str, SignatureTree, SignatureType],
|
||
|
value: Any,
|
||
|
verify: bool,
|
||
|
) -> None:
|
||
|
if type(signature) is SignatureTree:
|
||
|
signature_tree = signature
|
||
|
self.signature = signature_tree.signature
|
||
|
self.type = signature_tree.types[0]
|
||
|
elif type(signature) is SignatureType:
|
||
|
signature_tree = None
|
||
|
self.signature = signature.signature
|
||
|
self.type = signature
|
||
|
elif type(signature) is str:
|
||
|
signature_tree = get_signature_tree(signature)
|
||
|
self.signature = signature
|
||
|
self.type = signature_tree.types[0]
|
||
|
else:
|
||
|
raise TypeError(
|
||
|
"signature must be a SignatureTree, SignatureType, or a string"
|
||
|
)
|
||
|
self.value = value
|
||
|
if verify:
|
||
|
if signature_tree and len(signature_tree.types) != 1:
|
||
|
raise ValueError(
|
||
|
"variants must have a signature for a single complete type"
|
||
|
)
|
||
|
self.type.verify(value)
|
||
|
|
||
|
def __eq__(self, other: Any) -> bool:
|
||
|
if type(other) is Variant:
|
||
|
return self.signature == other.signature and self.value == other.value
|
||
|
return super().__eq__(other)
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
return "<dbus_fast.signature.Variant ('{}', {})>".format(
|
||
|
self.type.signature, self.value
|
||
|
)
|
||
|
|
||
|
|
||
|
get_signature_tree = lru_cache(maxsize=None)(SignatureTree)
|
||
|
"""Get a signature tree for the given signature.
|
||
|
|
||
|
:param signature: The signature to get a tree for.
|
||
|
:type signature: str
|
||
|
|
||
|
:returns: The signature tree for the given signature.
|
||
|
:rtype: :class:`SignatureTree`
|
||
|
"""
|