1 | 1 | import re |
|
2 | 1 | import collections |
|
3 | 1 | import functools |
|
4 | 1 | import starstruct.bitfield |
|
5 | |||
6 | |||
7 | 1 | class PackedBitField(object): |
|
8 | """ |
||
9 | A class that is used to bitwise pack/unpack one or more enumerations or |
||
10 | bitfields to/from an integer value |
||
11 | """ |
||
12 | 1 | def __init__(self, *args): |
|
13 | # Ensure that there are no duplicate enum or bitfield types in the list |
||
14 | 1 | member_bitfields = (k for k in args if isinstance(k, starstruct.bitfield.BitField)) |
|
15 | 1 | all_enums = args + tuple(b.enum for b in member_bitfields) |
|
16 | 1 | if len(all_enums) != len(set(all_enums)): |
|
17 | 1 | msg = 'Duplicate fields not allowed: {}'.format(args) |
|
18 | 1 | raise TypeError(msg) |
|
19 | |||
20 | # Ensure that all fields are either bitfields, or enums with all |
||
21 | # members of each enumeration type are integers |
||
22 | 1 | member_enums = (k for k in args if not isinstance(k, starstruct.bitfield.BitField)) |
|
23 | 1 | for key in member_enums: |
|
24 | 1 | if not all(isinstance(member.value, int) for member in key): |
|
25 | 1 | msg = 'Enum {} members must have integer values'.format(repr(key)) |
|
26 | 1 | raise TypeError(msg) |
|
27 | |||
28 | # Allow enum to be a list of enumerations that need bitpacked sequentially |
||
29 | 1 | self._fields = collections.OrderedDict(zip(args, [{}] * len(args))) |
|
30 | |||
31 | # Determine the bits required for the enumeration so they can all be |
||
32 | # packed correctly. Assume that the furthest right enumeration should |
||
33 | # have a bit offset of 0. |
||
34 | 1 | total_width = 0 |
|
35 | 1 | for key in reversed(self._fields): |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
36 | 1 | if isinstance(key, starstruct.bitfield.BitField): |
|
37 | 1 | all_bits = key.bit_mask |
|
38 | else: |
||
39 | 1 | all_bits = functools.reduce(lambda x, y: x | y, [k.value for k in key]) |
|
40 | |||
41 | 1 | self._fields[key] = { |
|
42 | 'offset': total_width, |
||
43 | 'mask': all_bits << total_width, |
||
44 | 'width': all_bits.bit_length(), |
||
45 | } |
||
46 | |||
47 | 1 | total_width += self._fields[key]['width'] |
|
48 | |||
49 | # Track the bit mask and bit length attributes just like BitField |
||
50 | 1 | self.bit_mask = functools.reduce(lambda x, y: x | y, [v['mask'] for v in self._fields.values()]) |
|
51 | 1 | self.bit_length = total_width |
|
52 | |||
53 | 1 | def __repr__(self): |
|
54 | return 'PackedBitField({})'.format(list(self._fields)) |
||
55 | |||
56 | 1 | def __str__(self): |
|
57 | 1 | return 'PackedBitField({})'.format(list(self._fields)) |
|
58 | |||
59 | 1 | def find_value(self, item): |
|
60 | """ |
||
61 | Take a value, determine if it matches one, and only one, of the member fields |
||
62 | """ |
||
63 | # pylint: disable=too-many-branches |
||
64 | |||
65 | # Split the member fields into bitfields and enums |
||
66 | 1 | member_enums = [k for k in self._fields if not isinstance(k, starstruct.bitfield.BitField)] |
|
67 | 1 | member_bitfields = [k for k in self._fields if isinstance(k, starstruct.bitfield.BitField)] |
|
68 | |||
69 | # See if the supplied value is an enum or bitfield value |
||
70 | 1 | matches = [] |
|
71 | 1 | for key in member_bitfields: |
|
72 | 1 | try: |
|
73 | 1 | matches.append((key.find_value(item), key)) |
|
74 | 1 | except ValueError: |
|
75 | # This just means it isn't a member of this bitfield |
||
76 | 1 | pass |
|
77 | |||
78 | # Also check for matches in the enums. This helps guard against |
||
79 | # ambiguous inputs where the bitfield and enum types overlap. |
||
80 | 1 | if isinstance(item, tuple(member_enums)): |
|
81 | 1 | for key in member_enums: |
|
82 | 1 | if isinstance(item, key): |
|
83 | # This is guaranteed a unique match, so return now |
||
84 | 1 | return (item, key) |
|
85 | 1 | elif isinstance(item, str): |
|
86 | # If it's a string, then check it against the enum fields |
||
87 | # (bitfields should already have been validated) |
||
88 | 1 | for key in member_enums: |
|
89 | 1 | try: |
|
90 | 1 | matches.append((getattr(key, item), key)) |
|
91 | 1 | except AttributeError: |
|
92 | # This is the normal error to throw if the enum name is |
||
93 | # not valid for this enumeration type. Check the next enum. |
||
94 | 1 | pass |
|
95 | else: |
||
96 | # Lastly, assume that the item is an integer value, attempt to |
||
97 | # convert it to one of the enum values to ensure it is a valid |
||
98 | # value. But if it matches more than one member field, we are |
||
99 | # unable to pack this properly. |
||
100 | 1 | for key in member_enums: |
|
101 | 1 | try: |
|
102 | 1 | matches.append((key(item), key)) |
|
103 | 1 | except ValueError: |
|
104 | # This just means that the value is not valid for a |
||
105 | # specific enum type, check all enums for a match before |
||
106 | # raising a ValueError |
||
107 | 1 | pass |
|
108 | |||
109 | 1 | if len(matches) == 1: |
|
110 | 1 | return matches[0] |
|
111 | 1 | elif len(matches) < 1: |
|
112 | 1 | msg = '{} is not a valid {}'.format(item, list(self._fields)) |
|
113 | 1 | raise ValueError(msg) |
|
114 | 1 | elif len(matches) > 1: |
|
115 | 1 | msg = '{} is not a unique {}'.format(item, list(self._fields)) |
|
116 | 1 | raise ValueError(msg) |
|
117 | |||
118 | 1 | def pack(self, arg): |
|
119 | """ |
||
120 | Take a list (or single value) and bitwise-or all the values together |
||
121 | """ |
||
122 | 1 | value = 0 |
|
123 | 1 | if arg is not None: |
|
124 | # Handle a variety of inputs: list or single, enum or raw |
||
125 | 1 | if hasattr(arg, '__iter__'): |
|
126 | 1 | arg_list = arg |
|
127 | else: |
||
128 | 1 | arg_list = [arg] |
|
129 | |||
130 | 1 | for item in arg_list: |
|
131 | 1 | (enum_val, key) = self.find_value(item) |
|
132 | 1 | value |= (enum_val.value << self._fields[key]['offset']) |
|
133 | |||
134 | 1 | return value |
|
135 | |||
136 | 1 | def unpack(self, val): |
|
137 | """ |
||
138 | Take a single number and split it out into all values that are present |
||
139 | """ |
||
140 | 1 | values = [] |
|
141 | 1 | for key in self._fields: |
|
142 | 1 | enum_specific_bits = (val & self._fields[key]['mask']) >> self._fields[key]['offset'] |
|
143 | 1 | if isinstance(key, starstruct.bitfield.BitField): |
|
144 | 1 | values.extend(key.unpack(enum_specific_bits)) |
|
145 | else: |
||
146 | 1 | try: |
|
147 | 1 | values.append(key(enum_specific_bits)) |
|
148 | 1 | except ValueError: |
|
149 | 1 | enum_name = re.match(r"<enum '(\S+)'>", str(key)).group(1) |
|
150 | 1 | msg = '{} is not a valid {}'.format(enum_specific_bits, enum_name) |
|
151 | 1 | raise ValueError(msg) |
|
152 | 1 | return frozenset(values) |
|
153 | |||
154 | 1 | def make(self, arg): |
|
155 | """ |
||
156 | Take an input list and return a frozenset |
||
157 | |||
158 | useful for testing |
||
159 | """ |
||
160 | values = [] |
||
161 | if arg is not None: |
||
162 | # Handle a variety of inputs: list or single, enum or raw |
||
163 | if hasattr(arg, '__iter__'): |
||
164 | arg_list = arg |
||
165 | else: |
||
166 | arg_list = [arg] |
||
167 | |||
168 | for item in arg_list: |
||
169 | values.append(self.find_value(item)[0]) |
||
170 | |||
171 | # return this list as a frozenset |
||
172 | return frozenset(values) |
||
173 |