Skip to content

Commit 8c7d1dd

Browse files
authored
Merge pull request #35 from rcfox/fieldarray-num-elements
Accept a num_elements_provider in FieldArray to specify a size in elements instead of bytes.
2 parents a690e94 + f013d1a commit 8c7d1dd

File tree

3 files changed

+66
-8
lines changed

3 files changed

+66
-8
lines changed

suitcase/fields.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,8 @@ class FieldArray(BaseField):
845845
the entire contents of the array.
846846
:param length_provider: The field providing a length value binding this
847847
message (if any). Set this field to None to leave unconstrained.
848+
:param num_elements_provider: The field providing the number of elements
849+
in the array. Set this field to None to leave unconstrained.
848850
849851
For example, one can imagine a message listing the zipcodes covered by
850852
a telephone area code. Depending on the population density, the number
@@ -863,7 +865,7 @@ class TelephoneZipcodes(Structure):
863865
864866
"""
865867

866-
def __init__(self, substructure, length_provider=None, **kwargs):
868+
def __init__(self, substructure, length_provider=None, num_elements_provider=None, **kwargs):
867869
BaseField.__init__(self, **kwargs)
868870
self.substructure = substructure
869871
self._value = list()
@@ -872,6 +874,12 @@ def __init__(self, substructure, length_provider=None, **kwargs):
872874
self.length_provider.associate_length_consumer(self)
873875
else:
874876
self.length_provider = None
877+
if isinstance(num_elements_provider, FieldPlaceholder):
878+
self.num_elements_provider = self._ph2f(num_elements_provider)
879+
self.num_elements_provider.length_value_provider = lambda: len(self._value)
880+
else:
881+
self.num_elements_provider = None
882+
875883

876884
@property
877885
def bytes_required(self):
@@ -880,15 +888,22 @@ def bytes_required(self):
880888
else:
881889
return self.length_provider.get_adjusted_length()
882890

891+
@property
892+
def num_elements(self):
893+
if self.num_elements_provider is None:
894+
return None
895+
else:
896+
return self.num_elements_provider.get_adjusted_length()
897+
883898
def pack(self, stream):
884899
for structure in self._value:
885900
stream.write(structure.pack())
886901

887902
def unpack(self, data, **kwargs):
888903
length = self.bytes_required
889-
if length == 0 or (data == b"" and length is None):
904+
if length == 0 or (data == b"" and length is None) or self.num_elements == 0:
890905
# Array is empty.
891-
return
906+
return data
892907

893908
kwargs['trailing'] = True
894909
while True:
@@ -897,6 +912,14 @@ def unpack(self, data, **kwargs):
897912
self._value.append(structure)
898913
if data == b"":
899914
break
915+
if len(self._value) == self.num_elements:
916+
break
917+
918+
if self.num_elements is not None and len(self._value) != self.num_elements:
919+
raise SuitcaseParseError("Expected %s elements but received %d." %
920+
(self.num_elements, len(self._value)))
921+
922+
return data
900923

901924

902925
class BaseFixedByteSequence(BaseField):

suitcase/structure.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
#
55
# Copyright (c) 2015 Digi International Inc. All Rights Reserved.
66
import sys
7-
7+
import os
88
import six
99
from suitcase.exceptions import SuitcaseException, \
1010
SuitcasePackException, SuitcaseParseError
11-
from suitcase.fields import FieldPlaceholder, CRCField, SubstructureField, \
11+
from suitcase.fields import FieldArray, FieldPlaceholder, CRCField, SubstructureField, \
1212
ConditionalField
1313
from six import BytesIO
1414

@@ -107,8 +107,12 @@ def unpack_stream(self, stream):
107107
stream.seek(stream.tell() + consumed)
108108
continue
109109
elif length is None:
110-
greedy_field = field
111-
break
110+
if isinstance(field, FieldArray) and field.num_elements is not None:
111+
# Read the data greedily now, and we'll backtrack after enough elements have been read.
112+
data = stream.read()
113+
else:
114+
greedy_field = field
115+
break
112116
else:
113117
data = stream.read(length)
114118
if len(data) != length:
@@ -118,7 +122,8 @@ def unpack_stream(self, stream):
118122
(name, length, len(data)))
119123

120124
try:
121-
field.unpack(data)
125+
unused_data = field.unpack(data)
126+
stream.seek(-len(unused_data or ""), os.SEEK_CUR)
122127
except SuitcaseException:
123128
raise # just re-raise these
124129
except Exception:

suitcase/test/test_fields.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,12 @@ class BasicMessageArrayAfter(Structure):
10431043
after = UBInt8()
10441044

10451045

1046+
# Test a FieldArray that takes a number of elements instead of a size in bytes.
1047+
class BasicMessageArrayNumElements(Structure):
1048+
count = LengthField(UBInt8())
1049+
array = FieldArray(BasicMessage, num_elements_provider=count)
1050+
1051+
10461052
class TestFieldArray(unittest.TestCase):
10471053
def test_pack_valid(self):
10481054
m = BasicMessageArray()
@@ -1112,6 +1118,30 @@ def test_unpack_after_valid(self):
11121118
self.assertEqual(m2.array[0].b2, 0x22)
11131119
self.assertEqual(m2.after, 0)
11141120

1121+
def test_pack_num_elements_valid(self):
1122+
m = BasicMessageArrayNumElements()
1123+
m.array = []
1124+
self.assertEqual(m.pack(), b"\x00")
1125+
1126+
messages = [BasicMessage(), BasicMessage(), BasicMessage()]
1127+
for i, message in enumerate(messages):
1128+
message.b1 = 0x10 + i
1129+
message.b2 = 0x20 + i
1130+
m.array.append(message)
1131+
1132+
self.assertEqual(m.pack(), b"\x03\x10\x20\x11\x21\x12\x22")
1133+
1134+
def test_unpack_num_elements_valid(self):
1135+
m = BasicMessageArrayNumElements.from_data(b"\x02\x22\x33\x44\x55")
1136+
1137+
self.assertEqual(m.count, 2)
1138+
self.assertEqual(len(m.array), 2)
1139+
self.assertIsInstance(m.array[0], BasicMessage)
1140+
self.assertEqual(m.array[0].b1, 0x22)
1141+
self.assertEqual(m.array[0].b2, 0x33)
1142+
self.assertEqual(m.array[1].b1, 0x44)
1143+
self.assertEqual(m.array[1].b2, 0x55)
1144+
11151145

11161146
# Test SubstructureField
11171147
class PascalString16(Structure):

0 commit comments

Comments
 (0)