Skip to content

Commit

Permalink
Byte order support in pack and unpack functions.
Browse files Browse the repository at this point in the history
  • Loading branch information
eerimoq committed May 11, 2017
1 parent a8ca4e0 commit 24a5d3a
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 12 deletions.
70 changes: 58 additions & 12 deletions bitstruct.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
from __future__ import print_function

import re
import struct

__version__ = "3.3.1"
__version__ = "3.4.0"


def _parse_format(fmt):
if fmt[-1] in [">", "<"]:
byte_order = fmt[-1]
fmt = fmt[:-1]
else:
byte_order = ">"

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 = ">"

for info in parsed_infos:
if info[0] != "":
endianness = info[0]

infos.append((info[1], int(info[2]), endianness))

return infos
return infos, byte_order


def _pack_integer(size, arg):
Expand Down Expand Up @@ -117,14 +127,19 @@ def pack(fmt, *args):
: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. Bitorder may be
omitted.
`fmt` is a string of bitorder-type-length groups, and optionally a
byteorder identifier afer 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
Expand All @@ -137,12 +152,18 @@ def pack(fmt, *args):
Length is the number of bits to pack the value into.
Example format string: 'u1u3p7s16'
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<'
"""

bits = ''
infos = _parse_format(fmt)
infos, byte_order = _parse_format(fmt)
i = 0

# Sanity check of the number of arguments.
Expand Down Expand Up @@ -177,7 +198,19 @@ def pack(fmt, *args):
if endianness == "<":
value_bits = value_bits[::-1]

bits += value_bits
# 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
Expand All @@ -201,7 +234,7 @@ def unpack(fmt, data):
"""

bits = ''.join(['{:08b}'.format(b) for b in bytearray(data)])
infos = _parse_format(fmt)
infos, byte_order = _parse_format(fmt)

# Sanity check.
number_of_bits_to_unpack = sum([size for _, size, _ in infos])
Expand All @@ -212,13 +245,26 @@ def unpack(fmt, data):
len(bits)))

res = []
i = 0
offset = 0

for type_, size, endianness in infos:
if type_ == 'p':
pass
else:
value_bits = bits[i:i+size]
# 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 == "<":
Expand All @@ -239,7 +285,7 @@ def unpack(fmt, data):

res.append(value)

i += size
offset += size

return tuple(res)

Expand All @@ -252,7 +298,7 @@ def calcsize(fmt):
"""

return sum([size for _, size, _ in _parse_format(fmt)])
return sum([size for _, size, _ in _parse_format(fmt)[0]])


def byteswap(fmt, data, offset = 0):
Expand Down
53 changes: 53 additions & 0 deletions tests/test_bitstruct.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,59 @@ def test_endianness(self):
unpacked = unpack('<u2', packed)
self.assertEqual(unpacked, (2, ))

def test_byte_order(self):
"""Test pack/unpack with byte order information in the format string.
"""

# most significant byte first (default)
ref = b'\x02\x46\x9a\xfe\x00\x00\x00'
packed = pack('u19s3f32>', 0x1234, -2, -1.0)
self.assertEqual(packed, ref)
unpacked = unpack('u19s3f32>', packed)
self.assertEqual(unpacked, (0x1234, -2, -1.0))

# least significant byte first
ref = b'\x34\x12\x18\x00\x00\xe0\xbc'
packed = pack('u19s3f32<', 0x1234, -2, -1.0)
self.assertEqual(packed, ref)
unpacked = unpack('u19s3f32<', packed)
self.assertEqual(unpacked, (0x1234, -2, -1.0))

# least significant byte first
ref = b'\x34\x12'
packed = pack('u8s8<', 0x34, 0x12)
self.assertEqual(packed, ref)
unpacked = unpack('u8s8<', packed)
self.assertEqual(unpacked, (0x34, 0x12))

# least significant byte first
ref = b'\x34\x22'
packed = pack('u3u12<', 1, 0x234)
self.assertEqual(packed, ref)
unpacked = unpack('u3s12<', packed)
self.assertEqual(unpacked, (1, 0x234))

# least significant byte first
ref = b'\x34\x11\x00'
packed = pack('u3u17<', 1, 0x234)
self.assertEqual(packed, ref)
unpacked = unpack('u3s17<', packed)
self.assertEqual(unpacked, (1, 0x234))

# least significant byte first
ref = b'\x80'
packed = pack('u1<', 1)
self.assertEqual(packed, ref)
unpacked = unpack('u1<', packed)
self.assertEqual(unpacked, (1, ))

# least significant byte first
ref = b'\x45\x23\x25\x82'
packed = pack('u19u5u1u7<', 0x12345, 5, 1, 2)
self.assertEqual(packed, ref)
unpacked = unpack('u19u5u1u7<', packed)
self.assertEqual(unpacked, (0x12345, 5, 1, 2))

def test_performance(self):
"""Test pack/unpack performance.
Expand Down

0 comments on commit 24a5d3a

Please sign in to comment.