Skip to content

Commit

Permalink
Bit order within a value
Browse files Browse the repository at this point in the history
Added support to specify the bit order within a value. Either MSB or LSB first. Use ">" for MSB first and "<" for LSB first.
  • Loading branch information
eerimoq committed Dec 19, 2015
1 parent 0e4c3c4 commit ba7f2db
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 40 deletions.
104 changes: 65 additions & 39 deletions bitstruct.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@


def _parse_format(fmt):
types = re.findall(r'[a-zA-Z]+', fmt)
sizes = map(lambda size: int(size),
re.findall(r'\d+', fmt))

return zip(types, sizes)
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


def _pack_integer(size, arg):
Expand All @@ -16,7 +23,6 @@ def _pack_integer(size, arg):

return '{{:0{}b}}'.format(size).format(arg)


def _pack_boolean(size, arg):
return _pack_integer(size, int(arg))

Expand All @@ -39,10 +45,10 @@ def _pack_bytearray(size, arg):
return bits[0:size]


def _unpack_integer(type, bits):
def _unpack_integer(_type, bits):
value = int(bits, 2)

if type == 's':
if _type == 's':
if bits[0] == '1':
value -= (1 << len(bits))

Expand Down Expand Up @@ -83,21 +89,29 @@ def pack(fmt, *args):
a multiple of 8, padding will be added at the end of the last
byte.
:param fmt: Bitstruct format string.
:param fmt: Bitstruct format string. See format description below.
:param args: Variable argument list of values to pack.
:returns: Bytearray of packed values.
`fmt` is a string of type-length pairs. There are six types; 'u',
's', 'f', 'b', 'r' and 'p'. Length is the number of bits to pack
the value into.
:returns: A bytearray of the packed values.
`fmt` is a string of bitorder-type-length groups. Bitorder 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.
There are six types; 'u', 's', 'f', 'b', 'r' and 'p'.
- 'u' -- unsigned integer
- 's' -- signed integer
- 'f' -- floating point number of 32 or 64 bits
- 'b' -- boolean
- 'r' -- raw, bytearray
- 'p' -- padding, ignore
Length is the number of bits to pack the value into.
Example format string: 'u1u3p7s16'
"""
Expand All @@ -106,20 +120,26 @@ def pack(fmt, *args):
infos = _parse_format(fmt)
i = 0

for type, size in infos:
if type == 'p':
for _type, size, endianness in infos:
if _type == 'p':
bits += size * '0'
else:
if type in 'us':
bits += _pack_integer(size, args[i])
elif type == 'f':
bits += _pack_float(size, args[i])
elif type == 'b':
bits += _pack_boolean(size, args[i])
elif type == 'r':
bits += _pack_bytearray(size, args[i])
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 == 'r':
value_bits = _pack_bytearray(size, args[i])
else:
raise ValueError("bad type '{}' in format".format(type))
raise ValueError("bad type '{}' in format".format(_type))

# reverse the bit order in little endian values
if endianness == "<":
value_bits = value_bits[::-1]

bits += value_bits
i += 1

# padding of last byte
Expand All @@ -138,7 +158,7 @@ def unpack(fmt, data):
:param fmt: Bitstruct format string. See pack() for details.
:param data: Bytearray of values to unpack.
:returns: Tuple of unpacked values.
:returns: A tuple of the unpacked values.
"""

Expand All @@ -147,20 +167,26 @@ def unpack(fmt, data):
res = []
i = 0

for type, size in infos:
if type == 'p':
for _type, size, endianness in infos:
if _type == 'p':
pass
else:
if type in 'us':
value = _unpack_integer(type, bits[i:i+size])
elif type == 'f':
value = _unpack_float(size, bits[i:i+size])
elif type == 'b':
value = _unpack_boolean(bits[i:i+size])
elif type == 'r':
value = _unpack_bytearray(size, bits[i:i+size])
value_bits = bits[i:i+size]

# 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 == 'r':
value = _unpack_bytearray(size, value_bits)
else:
raise ValueError("bad type '{}' in format".format(type))
raise ValueError("bad type '{}' in format".format(_type))
res.append(value)
i += size

Expand All @@ -176,7 +202,7 @@ def calcsize(fmt):
"""

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


def byteswap(fmt, data, offset = 0):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from setuptools import setup

setup(name='bitstruct',
version='2.0.2',
version='2.1.0',
description=('This module performs conversions between Python values '
'and C bit field structs represented as Python '
'bytearrays.'),
Expand Down
45 changes: 45 additions & 0 deletions tests/test_bitstruct.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class BitStructTest(unittest.TestCase):

def test_pack(self):
"""Pack values.
"""

packed = pack('u1u1s6u7u9', 0, 0, -2, 65, 22)
Expand Down Expand Up @@ -38,6 +39,7 @@ def test_pack(self):

def test_unpack(self):
"""Unpack values.
"""

unpacked = unpack('u1u1s6u7u9', bytearray(b'\x3e\x82\x16'))
Expand Down Expand Up @@ -87,6 +89,7 @@ def test_unpack(self):

def test_pack_unpack(self):
"""Pack and unpack values.
"""

packed = pack('u1u1s6u7u9', 0, 0, -2, 65, 22)
Expand All @@ -99,6 +102,7 @@ def test_pack_unpack(self):

def test_calcsize(self):
"""Calculate size.
"""

size = calcsize('u1u1s6u7u9')
Expand All @@ -118,6 +122,7 @@ def test_calcsize(self):

def test_byteswap(self):
"""Byte swap.
"""

res = bytearray(b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a')
Expand All @@ -128,5 +133,45 @@ def test_byteswap(self):
unpacked = unpack('u1u5u2u16', byteswap('12', packed))
self.assertEqual(unpacked, (1, 2, 3, 1024))

def test_endianness(self):
"""Test pack/unpack with endianness information in the format string.
"""

# big endian
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))

# little endian
ref = b"\x2c\x48\x0c\x00\x00\x07\xf4"
packed = pack("<u19s3f32", 0x1234, -2, -1.0)
self.assertEqual(packed, ref)
unpacked = unpack("<u19s3f32", packed)
self.assertEqual(unpacked, (0x1234, -2, -1.0))

# mixed endianness
ref = b"\x00\x00\x2f\x3f\xf0\x00\x00\x00\x00\x00\x00\x80"
packed = pack(">u19<s5>f64r3p4", 1, -2, 1.0, bytearray(b"\x80"))
self.assertEqual(packed, ref)
unpacked = unpack(">u19<s5>f64r3p4", packed)
self.assertEqual(unpacked, (1, -2, 1.0, bytearray(b"\x80")))

# opposite endianness of the "mixed endianness" test
ref = b"\x80\x00\x1e\x00\x00\x00\x00\x00\x00\x0f\xfc\x20"
packed = pack("<u19>s5<f64r3p4", 1, -2, 1.0, bytearray(b"\x80"))
self.assertEqual(packed, ref)
unpacked = unpack("<u19>s5<f64r3p4", packed)
self.assertEqual(unpacked, (1, -2, 1.0, bytearray(b"\x80")))

# pack as big endian, unpack as little endian
ref = b"\x40"
packed = pack("u2", 1)
self.assertEqual(packed, ref)
unpacked = unpack("<u2", packed)
self.assertEqual(unpacked, (2, ))

if __name__ == '__main__':
unittest.main()

0 comments on commit ba7f2db

Please sign in to comment.