forked from eerimoq/bitstruct
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbitstruct.py
411 lines (289 loc) · 12 KB
/
bitstruct.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
from __future__ import print_function
import re
import struct
from io import BytesIO
import binascii
__version__ = "3.7.0"
def _parse_format(fmt):
if fmt and fmt[-1] in '><':
byte_order = fmt[-1]
fmt = fmt[:-1]
else:
byte_order = ''
parsed_infos = re.findall(r'([<>]?)([a-zA-Z])(\d+)', fmt)
if ''.join([''.join(info) for info in parsed_infos]) != fmt:
raise ValueError("bad format '{}'".format(fmt + byte_order))
# Use big endian as default and use the endianness of the previous
# value if none is given for the current value.
infos = []
endianness = ">"
for info in parsed_infos:
if info[0] != "":
endianness = info[0]
if info[1] not in 'supPfbtr':
raise ValueError("bad char '{}' in format".format(info[1]))
infos.append((info[1], int(info[2]), endianness))
return infos, byte_order or '>'
def _pack_integer(size, arg):
value = int(arg)
if value < 0:
value += (1 << size)
value += (1 << size)
return bin(value)[3:]
def _pack_boolean(size, arg):
value = bool(arg)
return _pack_integer(size, int(value))
def _pack_float(size, arg):
value = float(arg)
if size == 16:
value = struct.pack('>e', value)
elif size == 32:
value = struct.pack('>f', value)
elif size == 64:
value = struct.pack('>d', value)
else:
raise ValueError('expected float size of 16, 32, or 64 bits (got {})'.format(
size))
return bin(int(b'01' + binascii.hexlify(value), 16))[3:]
def _pack_bytearray(size, arg):
return bin(int(b'01' + binascii.hexlify(arg), 16))[3:size + 3]
def _pack_text(size, arg):
value = arg.encode('utf-8')
return _pack_bytearray(size, bytearray(value))
def _unpack_signed_integer(bits):
value = int(bits, 2)
if bits[0] == '1':
value -= (1 << len(bits))
return value
def _unpack_unsigned_integer(bits):
return int(bits, 2)
def _unpack_boolean(bits):
return bool(int(bits, 2))
def _unpack_float(size, bits):
packed = _unpack_bytearray(size, bits)
if size == 16:
value = struct.unpack('>e', packed)[0]
elif size == 32:
value = struct.unpack('>f', packed)[0]
elif size == 64:
value = struct.unpack('>d', packed)[0]
else:
raise ValueError('expected float size of 16, 32, or 64 bits (got {})'.format(
size))
return value
def _unpack_bytearray(size, bits):
rest = size % 8
if rest > 0:
bits += (8 - rest) * '0'
return binascii.unhexlify(hex(int('10000000' + bits, 2))[4:].rstrip('L'))
def _unpack_text(size, bits):
return _unpack_bytearray(size, bits).decode('utf-8')
class CompiledFormat(object):
"""A compiled format string that can be used to pack and/or unpack
data multiple times.
Instances of this class are created by the factory function
:func:`~bitstruct.compile()`.
:param fmt: Bitstruct format string. See :func:`~bitstruct.pack()`
for details.
"""
def __init__(self, fmt):
infos, byte_order = _parse_format(fmt)
self._infos = infos
self._byte_order = byte_order
self._number_of_bits_to_unpack = sum([info[1] for info in infos])
self._number_of_arguments = 0
for info in infos:
if info[0] not in 'pP':
self._number_of_arguments += 1
def pack(self, *args):
"""Return a byte string containing the values v1, v2, ... packed
according to the compiled format string. If the total number
of bits are not a multiple of 8, padding will be added at the
end of the last byte.
:param args: Variable argument list of values to pack.
:returns: A byte string of the packed values.
"""
bits = ''
i = 0
# Sanity check of the number of arguments.
if len(args) < self._number_of_arguments:
raise ValueError("pack expected {} item(s) for packing "
"(got {})".format(self._number_of_arguments,
len(args)))
for type_, size, endianness in self._infos:
if type_ == 'p':
bits += size * '0'
elif type_ == 'P':
bits += size * '1'
else:
if type_ == 's':
value_bits = _pack_integer(size, args[i])
elif type_ == 'u':
value_bits = _pack_integer(size, args[i])
elif type_ == 'f':
value_bits = _pack_float(size, args[i])
elif type_ == 'b':
value_bits = _pack_boolean(size, args[i])
elif type_ == 't':
value_bits = _pack_text(size, args[i])
elif type_ == 'r':
value_bits = _pack_bytearray(size, bytearray(args[i]))
else:
raise ValueError("bad type '{}' in format".format(type_))
# reverse the bit order in little endian values
if endianness == "<":
value_bits = value_bits[::-1]
# reverse bytes order for least significant byte first
if self._byte_order == ">":
bits += value_bits
else:
aligned_offset = len(value_bits) - (8 - (len(bits) % 8))
while aligned_offset > 0:
bits += value_bits[aligned_offset:]
value_bits = value_bits[:aligned_offset]
aligned_offset -= 8
bits += value_bits
i += 1
# padding of last byte
tail = len(bits) % 8
if tail != 0:
bits += (8 - tail) * '0'
return bytes(_unpack_bytearray(len(bits), bits))
def unpack(self, data):
"""Unpack `data` (byte string, bytearray or list of integers)
according to the compiled format string. The result is a tuple
even if it contains exactly one item.
:param data: Byte string of values to unpack.
:returns: A tuple of the unpacked values.
"""
bits = bin(int(b'01' + binascii.hexlify(bytearray(data)), 16))[3:]
# Sanity check.
if self._number_of_bits_to_unpack > len(bits):
raise ValueError("unpack requires at least {} bits to unpack "
"(got {})".format(self._number_of_bits_to_unpack,
len(bits)))
res = []
offset = 0
for type_, size, endianness in self._infos:
if type_ in 'pP':
pass
else:
# reverse bytes order for least significant byte first
if self._byte_order == ">":
value_bits = bits[offset:offset + size]
else:
value_bits_tmp = bits[offset:offset + size]
aligned_offset = (size - ((offset + size) % 8))
value_bits = ''
while aligned_offset > 0:
value_bits += value_bits_tmp[aligned_offset:aligned_offset + 8]
value_bits_tmp = value_bits_tmp[:aligned_offset]
aligned_offset -= 8
value_bits += value_bits_tmp
# reverse the bit order in little endian values
if endianness == "<":
value_bits = value_bits[::-1]
if type_ == 's':
value = _unpack_signed_integer(value_bits)
elif type_ == 'u':
value = _unpack_unsigned_integer(value_bits)
elif type_ == 'f':
value = _unpack_float(size, value_bits)
elif type_ == 'b':
value = _unpack_boolean(value_bits)
elif type_ == 't':
value = _unpack_text(size, value_bits)
elif type_ == 'r':
value = bytes(_unpack_bytearray(size, value_bits))
else:
raise ValueError("bad type '{}' in format".format(type_))
res.append(value)
offset += size
return tuple(res)
def calcsize(self):
"""Calculate the number of bits in the compiled format string.
:returns: Number of bits in the format string.
"""
return sum([size for _, size, _ in self._infos])
def pack(fmt, *args):
"""Return a byte string containing the values v1, v2, ... packed
according to given format string `fmt`. If the total number of
bits are not a multiple of 8, padding will be added at the end of
the last byte.
:param fmt: Bitstruct format string. See format description below.
:param args: Variable argument list of values to pack.
:returns: A byte string of the packed values.
`fmt` is a string of bitorder-type-length groups, and optionally a
byteorder identifier after the groups. Bitorder and byteorder may
be omitted.
Bitorder is either ``>`` or ``<``, where ``>`` means MSB first and
``<`` means LSB first. If bitorder is omitted, the previous
values' bitorder is used for the current value. For example, in
the format string ``'u1<u2u3'``, ``u1`` is MSB first and both
``u2`` and ``u3`` are LSB first.
Byteorder is either ``>`` or ``<``, where ``>`` means most
significant byte first and ``<`` means least significant byte
first. If byteorder is omitted, most significant byte first is
used.
There are seven types; ``u``, ``s``, ``f``, ``b``, ``t``, ``r``
and ``p``.
- ``u`` -- unsigned integer
- ``s`` -- signed integer
- ``f`` -- floating point number of 16, 32, or 64 bits
- ``b`` -- boolean
- ``t`` -- text (ascii or utf-8)
- ``r`` -- raw, bytes
- ``p`` -- padding with zeros, ignore
- ``P`` -- padding with ones, ignore
Length is the number of bits to pack the value into.
Example format string with default bit and byte ordering:
``'u1u3p7s16'``
Same format string, but with least significant byte first:
``'u1u3p7s16<'``
Same format string, but with LSB first (``<`` prefix) and least
significant byte first (``<`` suffix): ``'<u1u3p7s16<'``
"""
return CompiledFormat(fmt).pack(*args)
def unpack(fmt, data):
"""Unpack `data` (byte string, bytearray or list of integers)
according to given format string `fmt`. The result is a tuple even
if it contains exactly one item.
:param fmt: Bitstruct format string. See :func:`~bitstruct.pack()`
for details.
:param data: Byte string of values to unpack.
:returns: A tuple of the unpacked values.
"""
return CompiledFormat(fmt).unpack(data)
def calcsize(fmt):
"""Calculate the number of bits in given format string `fmt`.
:param fmt: Bitstruct format string. See :func:`~bitstruct.pack()`
for details.
:returns: Number of bits in format string.
"""
return CompiledFormat(fmt).calcsize()
def byteswap(fmt, data, offset=0):
"""Swap bytes in `data` according to `fmt`, starting at byte
`offset`. `fmt` must be an iterable, iterating over number of
bytes to swap. For example, the format string ``'24'`` applied to
the byte string ``b'\\x00\\x11\\x22\\x33\\x44\\x55'`` will produce
the result ``b'\\x11\\x00\\x55\\x44\\x33\\x22'``.
:param fmt: Swap format string.
:param data: Byte string of data to swap.
:param offset: Start offset into `data`.
:returns: Byte string of swapped bytes.
"""
data = BytesIO(data)
data.seek(offset)
data_swapped = BytesIO()
for f in fmt:
swapped = data.read(int(f))[::-1]
data_swapped.write(swapped)
return data_swapped.getvalue()
def compile(fmt):
"""Compile given format string `fmt` and return a
:class:`~bitstruct.CompiledFormat` object that can be used to pack
and/or unpack data multiple times.
:param fmt: Bitstruct format string. See :func:`~bitstruct.pack()`
for details.
"""
return CompiledFormat(fmt)