forked from eerimoq/bitstruct
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbitstruct.py
325 lines (231 loc) · 9.05 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
from __future__ import print_function
import re
import struct
__version__ = "4.0.0"
def _parse_format(fmt):
parsed_infos = re.findall(r'([<>]?)([<>]?)([a-zA-Z])(\d+)', fmt)
# Use big endian as default and use the endianness of the previous
# value if none is given for the current value.
infos = []
endianness = ">"
byte_order = ">"
for info in parsed_infos:
if info[0] != "":
endianness = info[0]
if info[1] != "":
byte_order = info[1]
infos.append((info[2], int(info[3]), endianness, byte_order))
return infos
def _pack_integer(size, arg):
value = int(arg)
if value < 0:
value = ((1 << size) + value)
return '{{:0{}b}}'.format(size).format(value)
def _pack_boolean(size, arg):
value = bool(arg)
return _pack_integer(size, int(value))
def _pack_float(size, arg):
value = float(arg)
if size == 32:
value = struct.pack('>f', value)
elif size == 64:
value = struct.pack('>d', value)
else:
raise ValueError('expected float size of 32 of 64 bits (got {})'.format(
size))
return ''.join('{:08b}'.format(b)
for b in bytearray(value))
def _pack_bytearray(size, arg):
value = bytearray(arg)
bits = ''.join('{:08b}'.format(b)
for b in value)
return bits[0:size]
def _pack_text(size, arg):
value = arg.encode('utf-8')
return _pack_bytearray(size, bytearray(value))
def _unpack_integer(type_, bits):
value = int(bits, 2)
if type_ == 's':
if bits[0] == '1':
value -= (1 << len(bits))
return value
def _unpack_boolean(bits):
value = _unpack_integer('u', bits)
return bool(value)
def _unpack_float(size, bits):
packed = _unpack_bytearray(size, bits)
if size == 32:
value = struct.unpack('>f', packed)[0]
elif size == 64:
value = struct.unpack('>d', packed)[0]
else:
raise ValueError('expected float size of 32 of 64 bits (got {})'.format(
size))
return value
def _unpack_bytearray(size, bits):
value = bytearray()
for i in range(size // 8):
value.append(int(bits[8*i:8*i+8], 2))
rest = size % 8
if rest > 0:
value.append(int(bits[size-rest:], 2) << (8-rest))
return value
def _unpack_text(size, bits):
return _unpack_bytearray(size, bits).decode('utf-8')
def pack(fmt, *args):
"""Return a byte string containing the values v1, v2, ... packed
according to the given format. 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-[byteorder-]]type-length
groups. Bitorder and byteorder may be omitted. Bitorder must be
given if byteorder is given.
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, the previous values' byteorder is used for
the current value. By default, 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 32 or 64 bits
- 'b' -- boolean
- 't' -- text (ascii or utf-8)
- 'r' -- raw, bytes
- 'p' -- padding, 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'
"""
bits = ''
infos = _parse_format(fmt)
i = 0
# Sanity check of the number of arguments.
number_of_arguments = 0
for info in infos:
if info[0] != 'p':
number_of_arguments += 1
if number_of_arguments > len(args):
raise ValueError("pack expected {} item(s) for packing "
"(got {})".format(number_of_arguments, len(args)))
for type_, size, endianness, byte_order in infos:
if type_ == 'p':
bits += size * '0'
else:
if type_ in 'us':
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 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(bytearray([int(''.join(bits[i:i+8]), 2)
for i in range(0, len(bits), 8)]))
def unpack(fmt, data):
"""Unpack `data` (byte string, bytearray or list of integers)
according to the given format. The result is a tuple even if it
contains exactly one item.
:param fmt: Bitstruct format string. See pack() for details.
:param data: Byte string of values to unpack.
:returns: A tuple of the unpacked values.
"""
bits = ''.join(['{:08b}'.format(b) for b in bytearray(data)])
infos = _parse_format(fmt)
# Sanity check.
number_of_bits_to_unpack = sum([size for _, size, _, _ in infos])
if number_of_bits_to_unpack > len(bits):
raise ValueError("unpack requires at least {} bits to unpack "
"(got {})".format(number_of_bits_to_unpack,
len(bits)))
res = []
offset = 0
for type_, size, endianness, byte_order in infos:
if type_ == 'p':
pass
else:
# Reverse bytes order for least significant byte first.
if 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_ in 'us':
value = _unpack_integer(type_, 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(fmt):
"""Calculate the number of bits in given format.
:param fmt: Bitstruct format string.
:returns: Number of bits in format string.
"""
return sum([size for _, size, _, _ in _parse_format(fmt)])
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.
"""
i = offset
data_swapped = b''
for f in fmt:
length = int(f)
value = data[i:i+length]
data_swapped += value[::-1]
i += length
return data_swapped