]> jfr.im git - irc/weechat/qweechat.git/blob - src/qweechat/weechat/protocol.py
02b06367b66d9c130b2b43df75dab581b6e254ea
[irc/weechat/qweechat.git] / src / qweechat / weechat / protocol.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 #
4 # protocol.py - decode binary messages received from WeeChat/relay
5 #
6 # Copyright (C) 2011-2014 Sébastien Helleu <flashcode@flashtux.org>
7 #
8 # This file is part of QWeeChat, a Qt remote GUI for WeeChat.
9 #
10 # QWeeChat is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # QWeeChat is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with QWeeChat. If not, see <http://www.gnu.org/licenses/>.
22 #
23
24 #
25 # For info about protocol and format of messages, please read document
26 # "WeeChat Relay Protocol", available at: http://weechat.org/doc/
27 #
28 # History:
29 #
30 # 2011-11-23, Sébastien Helleu <flashcode@flashtux.org>:
31 # start dev
32 #
33
34 import collections, struct, zlib
35
36 if hasattr(collections, 'OrderedDict'):
37 # python >= 2.7
38 class WeechatDict(collections.OrderedDict):
39 def __str__(self):
40 return '{%s}' % ', '.join(['%s: %s' % (repr(key), repr(self[key])) for key in self])
41 else:
42 # python <= 2.6
43 WeechatDict = dict
44
45 class WeechatObject:
46 def __init__(self, objtype, value, separator='\n'):
47 self.objtype = objtype;
48 self.value = value
49 self.separator = separator
50 self.indent = ' ' if separator == '\n' else ''
51 self.separator1 = '\n%s' % self.indent if separator == '\n' else ''
52
53 def _str_value(self, v):
54 if type(v) is str and not v is None:
55 return '\'%s\'' % v
56 return str(v)
57
58 def _str_value_hdata(self):
59 lines = ['%skeys: %s%s%spath: %s' % (self.separator1, str(self.value['keys']), self.separator, self.indent, str(self.value['path']))]
60 for i, item in enumerate(self.value['items']):
61 lines.append(' item %d:%s%s' % ((i + 1), self.separator,
62 self.separator.join(['%s%s: %s' % (self.indent * 2, key, self._str_value(value)) for key, value in item.items()])))
63 return '\n'.join(lines)
64
65 def _str_value_infolist(self):
66 lines = ['%sname: %s' % (self.separator1, self.value['name'])]
67 for i, item in enumerate(self.value['items']):
68 lines.append(' item %d:%s%s' % ((i + 1), self.separator,
69 self.separator.join(['%s%s: %s' % (self.indent * 2, key, self._str_value(value)) for key, value in item.items()])))
70 return '\n'.join(lines)
71
72 def _str_value_other(self):
73 return self._str_value(self.value)
74
75 def __str__(self):
76 self._obj_cb = {'hda': self._str_value_hdata,
77 'inl': self._str_value_infolist,
78 }
79 return '%s: %s' % (self.objtype, self._obj_cb.get(self.objtype, self._str_value_other)())
80
81
82 class WeechatObjects(list):
83 def __init__(self, separator='\n'):
84 self.separator = separator
85
86 def __str__(self):
87 return self.separator.join([str(obj) for obj in self])
88
89
90 class WeechatMessage:
91 def __init__(self, size, size_uncompressed, compression, uncompressed, msgid, objects):
92 self.size = size
93 self.size_uncompressed = size_uncompressed
94 self.compression = compression
95 self.uncompressed = uncompressed
96 self.msgid = msgid
97 self.objects = objects
98
99 def __str__(self):
100 if self.compression != 0:
101 return 'size: %d/%d (%d%%), id=\'%s\', objects:\n%s' % (
102 self.size, self.size_uncompressed,
103 100 - ((self.size * 100) // self.size_uncompressed),
104 self.msgid, self.objects)
105 else:
106 return 'size: %d, id=\'%s\', objects:\n%s' % (self.size, self.msgid, self.objects)
107
108
109 class Protocol:
110 """Decode binary message received from WeeChat/relay."""
111
112 def __init__(self):
113 self._obj_cb = {'chr': self._obj_char,
114 'int': self._obj_int,
115 'lon': self._obj_long,
116 'str': self._obj_str,
117 'buf': self._obj_buffer,
118 'ptr': self._obj_ptr,
119 'tim': self._obj_time,
120 'htb': self._obj_hashtable,
121 'hda': self._obj_hdata,
122 'inf': self._obj_info,
123 'inl': self._obj_infolist,
124 'arr': self._obj_array,
125 }
126
127 def _obj_type(self):
128 """Read type in data (3 chars)."""
129 if len(self.data) < 3:
130 self.data = ''
131 return ''
132 objtype = str(self.data[0:3])
133 self.data = self.data[3:]
134 return objtype
135
136 def _obj_len_data(self, length_size):
137 """Read length (1 or 4 bytes), then value with this length."""
138 if len(self.data) < length_size:
139 self.data = ''
140 return None
141 if length_size == 1:
142 length = struct.unpack('B', self.data[0:1])[0]
143 self.data = self.data[1:]
144 else:
145 length = self._obj_int()
146 if length < 0:
147 return None
148 if length > 0:
149 value = self.data[0:length]
150 self.data = self.data[length:]
151 else:
152 value = ''
153 return value
154
155 def _obj_char(self):
156 """Read a char in data."""
157 if len(self.data) < 1:
158 return 0
159 value = struct.unpack('b', self.data[0:1])[0]
160 self.data = self.data[1:]
161 return value
162
163 def _obj_int(self):
164 """Read an integer in data (4 bytes)."""
165 if len(self.data) < 4:
166 self.data = ''
167 return 0
168 value = struct.unpack('>i', self.data[0:4])[0]
169 self.data = self.data[4:]
170 return value
171
172 def _obj_long(self):
173 """Read a long integer in data (length on 1 byte + value as string)."""
174 value = self._obj_len_data(1)
175 if value is None:
176 return None
177 return int(str(value))
178
179 def _obj_str(self):
180 """Read a string in data (length on 4 bytes + content)."""
181 value = self._obj_len_data(4)
182 if value is None:
183 return None
184 return str(value)
185
186 def _obj_buffer(self):
187 """Read a buffer in data (length on 4 bytes + data)."""
188 return self._obj_len_data(4)
189
190 def _obj_ptr(self):
191 """Read a pointer in data (length on 1 byte + value as string)."""
192 value = self._obj_len_data(1)
193 if value is None:
194 return None
195 return '0x%s' % str(value)
196
197 def _obj_time(self):
198 """Read a time in data (length on 1 byte + value as string)."""
199 value = self._obj_len_data(1)
200 if value is None:
201 return None
202 return int(str(value))
203
204 def _obj_hashtable(self):
205 """Read a hashtable in data (type for keys + type for values + count + items)."""
206 type_keys = self._obj_type()
207 type_values = self._obj_type()
208 count = self._obj_int()
209 hashtable = WeechatDict()
210 for i in range(0, count):
211 key = self._obj_cb[type_keys]()
212 value = self._obj_cb[type_values]()
213 hashtable[key] = value
214 return hashtable
215
216 def _obj_hdata(self):
217 """Read a hdata in data."""
218 path = self._obj_str()
219 keys = self._obj_str()
220 count = self._obj_int()
221 list_path = path.split('/')
222 list_keys = keys.split(',')
223 keys_types = []
224 dict_keys = WeechatDict()
225 for key in list_keys:
226 items = key.split(':')
227 keys_types.append(items)
228 dict_keys[items[0]] = items[1]
229 items = []
230 for i in range(0, count):
231 item = WeechatDict()
232 item['__path'] = []
233 pointers = []
234 for p in range(0, len(list_path)):
235 pointers.append(self._obj_ptr())
236 for key, objtype in keys_types:
237 item[key] = self._obj_cb[objtype]()
238 item['__path'] = pointers
239 items.append(item)
240 return {'path': list_path,
241 'keys': dict_keys,
242 'count': count,
243 'items': items,
244 }
245
246 def _obj_info(self):
247 """Read an info in data."""
248 name = self._obj_str()
249 value = self._obj_str()
250 return (name, value)
251
252 def _obj_infolist(self):
253 """Read an infolist in data."""
254 name = self._obj_str()
255 count_items = self._obj_int()
256 items = []
257 for i in range(0, count_items):
258 count_vars = self._obj_int()
259 variables = WeechatDict()
260 for v in range(0, count_vars):
261 var_name = self._obj_str()
262 var_type = self._obj_type()
263 var_value = self._obj_cb[var_type]()
264 variables[var_name] = var_value
265 items.append(variables)
266 return {'name': name, 'items': items}
267
268 def _obj_array(self):
269 """Read an array of values in data."""
270 type_values = self._obj_type()
271 count_values = self._obj_int()
272 values = []
273 for i in range(0, count_values):
274 values.append(self._obj_cb[type_values]())
275 return values
276
277 def decode(self, data, separator='\n'):
278 """Decode binary data and return list of objects."""
279 self.data = data
280 size = len(self.data)
281 size_uncompressed = size
282 uncompressed = None
283 # uncompress data (if it is compressed)
284 compression = struct.unpack('b', self.data[4:5])[0]
285 if compression:
286 uncompressed = zlib.decompress(self.data[5:])
287 size_uncompressed = len(uncompressed) + 5
288 uncompressed = '%s%s%s' % (struct.pack('>i', size_uncompressed), struct.pack('b', 0), uncompressed)
289 self.data = uncompressed
290 else:
291 uncompressed = self.data[:]
292 # skip length and compression flag
293 self.data = self.data[5:]
294 # read id
295 msgid = self._obj_str()
296 if msgid is None:
297 msgid = ''
298 # read objects
299 objects = WeechatObjects(separator=separator)
300 while len(self.data) > 0:
301 objtype = self._obj_type()
302 value = self._obj_cb[objtype]()
303 objects.append(WeechatObject(objtype, value, separator=separator))
304 return WeechatMessage(size, size_uncompressed, compression, uncompressed, msgid, objects)
305
306
307 def hex_and_ascii(data, bytes_per_line=10):
308 """Convert a QByteArray to hex + ascii output."""
309 num_lines = ((len(data) - 1) // bytes_per_line) + 1
310 if num_lines == 0:
311 return ''
312 lines = []
313 for i in range(0, num_lines):
314 str_hex = []
315 str_ascii = []
316 for char in data[i*bytes_per_line:(i*bytes_per_line)+bytes_per_line]:
317 byte = struct.unpack('B', char)[0]
318 str_hex.append('%02X' % int(byte))
319 if byte >= 32 and byte <= 127:
320 str_ascii.append(char)
321 else:
322 str_ascii.append('.')
323 fmt = '%%-%ds %%s' % ((bytes_per_line * 3) - 1)
324 lines.append(fmt % (' '.join(str_hex), ''.join(str_ascii)))
325 return '\n'.join(lines)