from collections import deque from enum import Enum, IntEnum, IntFlag import struct from typing import Optional class SizeLimitError(ValueError): """Raised when trying to (de-)serialise data exceeding D-Bus' size limit. This is currently only implemented for arrays, where the maximum size is 64 MiB. """ pass class Endianness(Enum): little = 1 big = 2 def struct_code(self): return '<' if (self is Endianness.little) else '>' def dbus_code(self): return b'l' if (self is Endianness.little) else b'B' endian_map = {b'l': Endianness.little, b'B': Endianness.big} class MessageType(Enum): method_call = 1 method_return = 2 error = 3 signal = 4 class MessageFlag(IntFlag): no_reply_expected = 1 no_auto_start = 2 allow_interactive_authorization = 4 class HeaderFields(IntEnum): path = 1 interface = 2 member = 3 error_name = 4 reply_serial = 5 destination = 6 sender = 7 signature = 8 unix_fds = 9 def padding(pos, step): pad = step - (pos % step) if pad == step: return 0 return pad class FixedType: def __init__(self, size, struct_code): self.size = self.alignment = size self.struct_code = struct_code def parse_data(self, buf, pos, endianness, fds=()): pos += padding(pos, self.alignment) code = endianness.struct_code() + self.struct_code val = struct.unpack(code, buf[pos:pos + self.size])[0] return val, pos + self.size def serialise(self, data, pos, endianness, fds=None): pad = b'\0' * padding(pos, self.alignment) code = endianness.struct_code() + self.struct_code return pad + struct.pack(code, data) def __repr__(self): return 'FixedType({!r}, {!r})'.format(self.size, self.struct_code) def __eq__(self, other): return (type(other) is FixedType) and (self.size == other.size) \ and (self.struct_code == other.struct_code) class Boolean(FixedType): def __init__(self): super().__init__(4, 'I') # D-Bus booleans take 4 bytes def parse_data(self, buf, pos, endianness, fds=()): val, new_pos = super().parse_data(buf, pos, endianness) return bool(val), new_pos def __repr__(self): return 'Boolean()' def __eq__(self, other): return type(other) is Boolean class FileDescriptor(FixedType): def __init__(self): super().__init__(4, 'I') def parse_data(self, buf, pos, endianness, fds=()): idx, new_pos = super().parse_data(buf, pos, endianness) return fds[idx], new_pos def serialise(self, data, pos, endianness, fds=None): if fds is None: raise RuntimeError("Sending FDs is not supported or not enabled") if hasattr(data, 'fileno'): data = data.fileno() if isinstance(data, bool) or not isinstance(data, int): raise TypeError("Cannot use {data!r} as file descriptor. Expected " "an int or an object with fileno() method") if data < 0: raise ValueError(f"File descriptor can't be negative ({data})") fds.append(data) return super().serialise(len(fds) - 1, pos, endianness) def __repr__(self): return 'FileDescriptor()' def __eq__(self, other): return type(other) is FileDescriptor simple_types = { 'y': FixedType(1, 'B'), # unsigned 8 bit 'n': FixedType(2, 'h'), # signed 16 bit 'q': FixedType(2, 'H'), # unsigned 16 bit 'b': Boolean(), # bool (32-bit) 'i': FixedType(4, 'i'), # signed 32-bit 'u': FixedType(4, 'I'), # unsigned 32-bit 'x': FixedType(8, 'q'), # signed 64-bit 't': FixedType(8, 'Q'), # unsigned 64-bit 'd': FixedType(8, 'd'), # double 'h': FileDescriptor(), # file descriptor (uint32 index in a separate list) } class StringType: def __init__(self, length_type): self.length_type = length_type @property def alignment(self): return self.length_type.size def parse_data(self, buf, pos, endianness, fds=()): length, pos = self.length_type.parse_data(buf, pos, endianness) end = pos + length val = buf[pos:end].decode('utf-8') assert buf[end:end + 1] == b'\0' return val, end + 1 def serialise(self, data, pos, endianness, fds=None): if not isinstance(data, str): raise TypeError("Expected str, not {!r}".format(data)) encoded = data.encode('utf-8') len_data = self.length_type.serialise(len(encoded), pos, endianness) return len_data + encoded + b'\0' def __repr__(self): return 'StringType({!r})'.format(self.length_type) def __eq__(self, other): return (type(other) is StringType) \ and (self.length_type == other.length_type) simple_types.update({ 's': StringType(simple_types['u']), # String 'o': StringType(simple_types['u']), # Object path 'g': StringType(simple_types['y']), # Signature }) class Struct: alignment = 8 def __init__(self, fields): if any(isinstance(f, DictEntry) for f in fields): raise TypeError("Found dict entry outside array") self.fields = fields def parse_data(self, buf, pos, endianness, fds=()): pos += padding(pos, 8) res = [] for field in self.fields: v, pos = field.parse_data(buf, pos, endianness, fds=fds) res.append(v) return tuple(res), pos def serialise(self, data, pos, endianness, fds=None): if not isinstance(data, tuple): raise TypeError("Expected tuple, not {!r}".format(data)) if len(data) != len(self.fields): raise ValueError("{} entries for {} fields".format( len(data), len(self.fields) )) pad = b'\0' * padding(pos, self.alignment) pos += len(pad) res_pieces = [] for item, field in zip(data, self.fields): res_pieces.append(field.serialise(item, pos, endianness, fds=fds)) pos += len(res_pieces[-1]) return pad + b''.join(res_pieces) def __repr__(self): return "{}({!r})".format(type(self).__name__, self.fields) def __eq__(self, other): return (type(other) is type(self)) and (self.fields == other.fields) class DictEntry(Struct): def __init__(self, fields): if len(fields) != 2: raise TypeError( "Dict entry must have 2 fields, not %d" % len(fields)) if not isinstance(fields[0], (FixedType, StringType)): raise TypeError( "First field in dict entry must be simple type, not {}" .format(type(fields[0]))) super().__init__(fields) class Array: alignment = 4 length_type = FixedType(4, 'I') def __init__(self, elt_type): self.elt_type = elt_type def parse_data(self, buf, pos, endianness, fds=()): # print('Array start', pos) length, pos = self.length_type.parse_data(buf, pos, endianness) pos += padding(pos, self.elt_type.alignment) end = pos + length if self.elt_type == simple_types['y']: # Array of bytes return buf[pos:end], end res = [] while pos < end: # print('Array elem', pos) v, pos = self.elt_type.parse_data(buf, pos, endianness, fds=fds) res.append(v) if isinstance(self.elt_type, DictEntry): # Convert list of 2-tuples to dict res = dict(res) return res, pos def serialise(self, data, pos, endianness, fds=None): data_is_bytes = False if isinstance(self.elt_type, DictEntry) and isinstance(data, dict): data = data.items() elif (self.elt_type == simple_types['y']) and isinstance(data, bytes): data_is_bytes = True elif not isinstance(data, list): raise TypeError("Not suitable for array: {!r}".format(data)) # Fail fast if we know in advance that the data is too big: if isinstance(self.elt_type, FixedType): if (self.elt_type.size * len(data)) > 2**26: raise SizeLimitError("Array size exceeds 64 MiB limit") pad1 = padding(pos, self.alignment) pos_after_length = pos + pad1 + 4 pad2 = padding(pos_after_length, self.elt_type.alignment) if data_is_bytes: buf = data else: data_pos = pos_after_length + pad2 limit_pos = data_pos + 2 ** 26 chunks = [] for item in data: chunks.append(self.elt_type.serialise( item, data_pos, endianness, fds=fds )) data_pos += len(chunks[-1]) if data_pos > limit_pos: raise SizeLimitError("Array size exceeds 64 MiB limit") buf = b''.join(chunks) len_data = self.length_type.serialise(len(buf), pos+pad1, endianness) # print('Array ser: pad1={!r}, len_data={!r}, pad2={!r}, buf={!r}'.format( # pad1, len_data, pad2, buf)) return (b'\0' * pad1) + len_data + (b'\0' * pad2) + buf def __repr__(self): return 'Array({!r})'.format(self.elt_type) def __eq__(self, other): return (type(other) is Array) and (self.elt_type == other.elt_type) class Variant: alignment = 1 def parse_data(self, buf, pos, endianness, fds=()): # print('variant', pos) sig, pos = simple_types['g'].parse_data(buf, pos, endianness) # print('variant sig:', repr(sig), pos) valtype = parse_signature(list(sig)) val, pos = valtype.parse_data(buf, pos, endianness, fds=fds) # print('variant done', (sig, val), pos) return (sig, val), pos def serialise(self, data, pos, endianness, fds=None): sig, data = data valtype = parse_signature(list(sig)) sig_buf = simple_types['g'].serialise(sig, pos, endianness) return sig_buf + valtype.serialise( data, pos + len(sig_buf), endianness, fds=fds ) def __repr__(self): return 'Variant()' def __eq__(self, other): return type(other) is Variant def parse_signature(sig): """Parse a symbolic signature into objects. """ # Based on http://norvig.com/lispy.html token = sig.pop(0) if token == 'a': return Array(parse_signature(sig)) if token == 'v': return Variant() elif token == '(': fields = [] while sig[0] != ')': fields.append(parse_signature(sig)) sig.pop(0) # ) return Struct(fields) elif token == '{': de = [] while sig[0] != '}': de.append(parse_signature(sig)) sig.pop(0) # } return DictEntry(de) elif token in ')}': raise ValueError('Unexpected end of struct') else: return simple_types[token] def calc_msg_size(buf): endian, = struct.unpack('c', buf[:1]) endian = endian_map[endian] body_length, = struct.unpack(endian.struct_code() + 'I', buf[4:8]) fields_array_len, = struct.unpack(endian.struct_code() + 'I', buf[12:16]) header_len = 16 + fields_array_len return header_len + padding(header_len, 8) + body_length _header_fields_type = Array(Struct([simple_types['y'], Variant()])) def parse_header_fields(buf, endianness): l, pos = _header_fields_type.parse_data(buf, 12, endianness) return {HeaderFields(k): v[1] for (k, v) in l}, pos header_field_codes = { 1: 'o', 2: 's', 3: 's', 4: 's', 5: 'u', 6: 's', 7: 's', 8: 'g', 9: 'u', } def serialise_header_fields(d, endianness): l = [(i.value, (header_field_codes[i], v)) for (i, v) in sorted(d.items())] return _header_fields_type.serialise(l, 12, endianness) class Header: def __init__(self, endianness, message_type, flags, protocol_version, body_length, serial, fields): """A D-Bus message header It's not normally necessary to construct this directly: use higher level functions and methods instead. """ self.endianness = endianness self.message_type = MessageType(message_type) self.flags = MessageFlag(flags) self.protocol_version = protocol_version self.body_length = body_length self.serial = serial self.fields = fields def __repr__(self): return 'Header({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, fields={!r})'.format( self.endianness, self.message_type, self.flags, self.protocol_version, self.body_length, self.serial, self.fields) def serialise(self, serial=None): s = self.endianness.struct_code() + 'cBBBII' if serial is None: serial = self.serial return struct.pack(s, self.endianness.dbus_code(), self.message_type.value, self.flags, self.protocol_version, self.body_length, serial) \ + serialise_header_fields(self.fields, self.endianness) @classmethod def from_buffer(cls, buf): endian, msgtype, flags, pv = struct.unpack('cBBB', buf[:4]) endian = endian_map[endian] bodylen, serial = struct.unpack(endian.struct_code() + 'II', buf[4:12]) fields, pos = parse_header_fields(buf, endian) return cls(endian, msgtype, flags, pv, bodylen, serial, fields), pos class Message: """Object representing a DBus message. It's not normally necessary to construct this directly: use higher level functions and methods instead. """ def __init__(self, header, body): self.header = header self.body = body def __repr__(self): return "{}({!r}, {!r})".format(type(self).__name__, self.header, self.body) @classmethod def from_buffer(cls, buf: bytes, fds=()) -> 'Message': header, pos = Header.from_buffer(buf) n_fds = header.fields.get(HeaderFields.unix_fds, 0) if n_fds > len(fds): raise ValueError( f"Message expects {n_fds} FDs, but only {len(fds)} were received" ) fds = fds[:n_fds] body = () if HeaderFields.signature in header.fields: sig = header.fields[HeaderFields.signature] body_type = parse_signature(list('(%s)' % sig)) body = body_type.parse_data(buf, pos, header.endianness, fds=fds)[0] return Message(header, body) def serialise(self, serial=None, fds=None) -> bytes: """Convert this message to bytes. Specifying *serial* overrides the ``msg.header.serial`` field, so a connection can use its own serial number without modifying the message. If file-descriptor support is in use, *fds* should be a :class:`array.array` object with type ``'i'``. Any file descriptors in the message will be added to the array. If the message contains FDs, it can't be serialised without this array. """ endian = self.header.endianness if HeaderFields.signature in self.header.fields: sig = self.header.fields[HeaderFields.signature] body_type = parse_signature(list('(%s)' % sig)) body_buf = body_type.serialise(self.body, 0, endian, fds=fds) else: body_buf = b'' self.header.body_length = len(body_buf) if fds: self.header.fields[HeaderFields.unix_fds] = len(fds) header_buf = self.header.serialise(serial=serial) pad = b'\0' * padding(len(header_buf), 8) return header_buf + pad + body_buf class Parser: """Parse DBus messages from a stream of incoming data. """ def __init__(self): self.buf = BufferPipe() self.fds = [] self.next_msg_size = None def add_data(self, data: bytes, fds=()): """Provide newly received data to the parser""" self.buf.write(data) self.fds.extend(fds) def feed(self, data): """Feed the parser newly read data. Returns a list of messages completed by the new data. """ self.add_data(data) return list(iter(self.get_next_message, None)) def bytes_desired(self): """How many bytes can be received without going beyond the next message? This is only used with file-descriptor passing, so we don't get too many FDs in a single recvmsg call. """ got = self.buf.bytes_buffered if got < 16: # The first 16 bytes tell us the message size return 16 - got if self.next_msg_size is None: self.next_msg_size = calc_msg_size(self.buf.peek(16)) return self.next_msg_size - got def get_next_message(self) -> Optional[Message]: """Parse one message, if there is enough data. Returns None if it doesn't have a complete message. """ if self.next_msg_size is None: if self.buf.bytes_buffered >= 16: self.next_msg_size = calc_msg_size(self.buf.peek(16)) nms = self.next_msg_size if (nms is not None) and self.buf.bytes_buffered >= nms: raw_msg = self.buf.read(nms) msg = Message.from_buffer(raw_msg, fds=self.fds) self.next_msg_size = None fds_consumed = msg.header.fields.get(HeaderFields.unix_fds, 0) self.fds = self.fds[fds_consumed:] return msg class BufferPipe: """A place to store received data until we can parse a complete message The main difference from io.BytesIO is that read & write operate at opposite ends, like a pipe. """ def __init__(self): self.chunks = deque() self.bytes_buffered = 0 def write(self, b: bytes): self.chunks.append(b) self.bytes_buffered += len(b) def _peek_iter(self, nbytes: int): assert nbytes <= self.bytes_buffered for chunk in self.chunks: chunk = chunk[:nbytes] nbytes -= len(chunk) yield chunk if nbytes <= 0: break def peek(self, nbytes: int) -> bytes: """Get exactly nbytes bytes from the front without removing them""" return b''.join(self._peek_iter(nbytes)) def _read_iter(self, nbytes: int): assert nbytes <= self.bytes_buffered while True: chunk = self.chunks.popleft() self.bytes_buffered -= len(chunk) if nbytes <= len(chunk): break nbytes -= len(chunk) yield chunk # Final chunk chunk, rem = chunk[:nbytes], chunk[nbytes:] if rem: self.chunks.appendleft(rem) self.bytes_buffered += len(rem) yield chunk def read(self, nbytes: int) -> bytes: """Take & return exactly nbytes bytes from the front""" return b''.join(self._read_iter(nbytes))