diff options
Diffstat (limited to 'libs/socketio/packet.py')
-rw-r--r-- | libs/socketio/packet.py | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/libs/socketio/packet.py b/libs/socketio/packet.py new file mode 100644 index 000000000..73b469d6d --- /dev/null +++ b/libs/socketio/packet.py @@ -0,0 +1,179 @@ +import functools +import json as _json + +import six + +(CONNECT, DISCONNECT, EVENT, ACK, ERROR, BINARY_EVENT, BINARY_ACK) = \ + (0, 1, 2, 3, 4, 5, 6) +packet_names = ['CONNECT', 'DISCONNECT', 'EVENT', 'ACK', 'ERROR', + 'BINARY_EVENT', 'BINARY_ACK'] + + +class Packet(object): + """Socket.IO packet.""" + + # the format of the Socket.IO packet is as follows: + # + # packet type: 1 byte, values 0-6 + # num_attachments: ASCII encoded, only if num_attachments != 0 + # '-': only if num_attachments != 0 + # namespace: only if namespace != '/' + # ',': only if namespace and one of id and data are defined in this packet + # id: ASCII encoded, only if id is not None + # data: JSON dump of data payload + + json = _json + + def __init__(self, packet_type=EVENT, data=None, namespace=None, id=None, + binary=None, encoded_packet=None): + self.packet_type = packet_type + self.data = data + self.namespace = namespace + self.id = id + if binary or (binary is None and self._data_is_binary(self.data)): + if self.packet_type == EVENT: + self.packet_type = BINARY_EVENT + elif self.packet_type == ACK: + self.packet_type = BINARY_ACK + else: + raise ValueError('Packet does not support binary payload.') + self.attachment_count = 0 + self.attachments = [] + if encoded_packet: + self.attachment_count = self.decode(encoded_packet) + + def encode(self): + """Encode the packet for transmission. + + If the packet contains binary elements, this function returns a list + of packets where the first is the original packet with placeholders for + the binary components and the remaining ones the binary attachments. + """ + encoded_packet = six.text_type(self.packet_type) + if self.packet_type == BINARY_EVENT or self.packet_type == BINARY_ACK: + data, attachments = self._deconstruct_binary(self.data) + encoded_packet += six.text_type(len(attachments)) + '-' + else: + data = self.data + attachments = None + needs_comma = False + if self.namespace is not None and self.namespace != '/': + encoded_packet += self.namespace + needs_comma = True + if self.id is not None: + if needs_comma: + encoded_packet += ',' + needs_comma = False + encoded_packet += six.text_type(self.id) + if data is not None: + if needs_comma: + encoded_packet += ',' + encoded_packet += self.json.dumps(data, separators=(',', ':')) + if attachments is not None: + encoded_packet = [encoded_packet] + attachments + return encoded_packet + + def decode(self, encoded_packet): + """Decode a transmitted package. + + The return value indicates how many binary attachment packets are + necessary to fully decode the packet. + """ + ep = encoded_packet + try: + self.packet_type = int(ep[0:1]) + except TypeError: + self.packet_type = ep + ep = '' + self.namespace = None + self.data = None + ep = ep[1:] + dash = ep.find('-') + attachment_count = 0 + if dash > 0 and ep[0:dash].isdigit(): + attachment_count = int(ep[0:dash]) + ep = ep[dash + 1:] + if ep and ep[0:1] == '/': + sep = ep.find(',') + if sep == -1: + self.namespace = ep + ep = '' + else: + self.namespace = ep[0:sep] + ep = ep[sep + 1:] + q = self.namespace.find('?') + if q != -1: + self.namespace = self.namespace[0:q] + if ep and ep[0].isdigit(): + self.id = 0 + while ep and ep[0].isdigit(): + self.id = self.id * 10 + int(ep[0]) + ep = ep[1:] + if ep: + self.data = self.json.loads(ep) + return attachment_count + + def add_attachment(self, attachment): + if self.attachment_count <= len(self.attachments): + raise ValueError('Unexpected binary attachment') + self.attachments.append(attachment) + if self.attachment_count == len(self.attachments): + self.reconstruct_binary(self.attachments) + return True + return False + + def reconstruct_binary(self, attachments): + """Reconstruct a decoded packet using the given list of binary + attachments. + """ + self.data = self._reconstruct_binary_internal(self.data, + self.attachments) + + def _reconstruct_binary_internal(self, data, attachments): + if isinstance(data, list): + return [self._reconstruct_binary_internal(item, attachments) + for item in data] + elif isinstance(data, dict): + if data.get('_placeholder') and 'num' in data: + return attachments[data['num']] + else: + return {key: self._reconstruct_binary_internal(value, + attachments) + for key, value in six.iteritems(data)} + else: + return data + + def _deconstruct_binary(self, data): + """Extract binary components in the packet.""" + attachments = [] + data = self._deconstruct_binary_internal(data, attachments) + return data, attachments + + def _deconstruct_binary_internal(self, data, attachments): + if isinstance(data, six.binary_type): + attachments.append(data) + return {'_placeholder': True, 'num': len(attachments) - 1} + elif isinstance(data, list): + return [self._deconstruct_binary_internal(item, attachments) + for item in data] + elif isinstance(data, dict): + return {key: self._deconstruct_binary_internal(value, attachments) + for key, value in six.iteritems(data)} + else: + return data + + def _data_is_binary(self, data): + """Check if the data contains binary components.""" + if isinstance(data, six.binary_type): + return True + elif isinstance(data, list): + return functools.reduce( + lambda a, b: a or b, [self._data_is_binary(item) + for item in data], False) + elif isinstance(data, dict): + return functools.reduce( + lambda a, b: a or b, [self._data_is_binary(item) + for item in six.itervalues(data)], + False) + else: + return False |