File Coverage

blib/lib/Data/Bitfield.pm
Criterion Covered Total %
statement 126 131 96.1
branch 40 46 86.9
condition 15 19 78.9
subroutine 24 26 92.3
pod 5 6 83.3
total 210 228 92.1


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2014-2017 -- leonerd@leonerd.org.uk
5              
6             package Data::Bitfield;
7              
8 6     6   263703 use strict;
  6         60  
  6         157  
9 6     6   28 use warnings;
  6         10  
  6         188  
10              
11             our $VERSION = '0.02';
12              
13 6     6   26 use Exporter 'import';
  6         10  
  6         236  
14             our @EXPORT_OK = qw( bitfield boolfield intfield enumfield constfield );
15              
16 6     6   32 use Carp;
  6         33  
  6         2284  
17              
18             =head1 NAME
19              
20             C - manage integers containing multiple bit fields
21              
22             =head1 DESCRIPTION
23              
24             This module provides a single primary function, C, which creates
25             helper functions in the package that calls it, to assist in managing integers
26             that encode sets of bits, called bitfields. This may be useful when
27             interacting with a low-level networking protocol, binary file format, hardware
28             devices, or similar purposes.
29              
30             =head2 bitfield
31              
32             bitfield $name, %fields
33              
34             Creates two new functions in the calling package whose names are derived from
35             the string C<$name> passed here. These functions will be symmetric opposites,
36             which convert between a key/value list of field values, and their packed
37             integer or binary byte-string representation.
38              
39             $packed_value = pack_$name( %field_values )
40              
41             %field_values = unpack_$name( $packed_value )
42              
43             These two functions will work to a set of field names that match those field
44             definitions given to the C function that declared them.
45              
46             Each field has a name and a definition. Its definition comes from one of the
47             following field-declaration functions.
48              
49             Additional options may be passed by giving a C reference as the first
50             argument, before the structure name.
51              
52             bitfield { %options }, $name, %fields
53              
54             Recognised options are:
55              
56             =over 4
57              
58             =item format => "bytes-LE" | "bytes-BE" | "integer"
59              
60             Defines the format that the C function will return and the
61             C function will expect to receive as input. The two C
62             formats describe a packed binary string in either little- or big-endian
63             direction, and C describes an integer numerical value.
64              
65             Note that currently the C formats are limited to values 32bits wide
66             or smaller.
67              
68             Optional; will default to C if not supplied. This default may change
69             in a later version - make sure to always specify it for now.
70              
71             =item unrecognised_ok => BOOL
72              
73             If true, the C function will not complain about unrecognised field
74             names; they will simply be ignored.
75              
76             =back
77              
78             =cut
79              
80             sub bitfield
81             {
82 13     13 1 26 my $pkg = caller;
83 13         34 bitfield_into_caller( $pkg, @_ );
84             }
85              
86             my %VALID_FORMATS = map { $_ => 1 } qw( bytes-LE bytes-BE integer );
87              
88             sub bitfield_into_caller
89             {
90 13     13 0 22 my $pkg = shift;
91 13 100       47 my %options = ( ref $_[0] eq "HASH" ) ? %{ +shift } : ();
  6         19  
92 13         41 my ( $name, @args ) = @_;
93              
94 13         29 my $unrecognised_ok = !!$options{unrecognised_ok};
95 13   100     44 my $format = $options{format} // "integer";
96 13 50       36 $VALID_FORMATS{$format} or
97             croak "Invalid 'format' value $format";
98              
99 13         27 my $used_bits = 0;
100              
101 13         20 my $constmask = 0;
102 13         15 my $constval = 0;
103              
104 13         65 my %fieldmask;
105             my %fieldshift;
106 13         0 my %fieldencoder;
107 13         0 my %fielddecoder;
108 13         33 while( @args ) {
109 30         50 my $name = shift @args;
110 30 100       55 if( !defined $name ) {
111 1         1 my ( $mask, $value ) = @{ shift @args };
  1         2  
112              
113 1 50       3 croak "Constfield collides with other defined bits"
114             if $used_bits & $mask;
115              
116 1         2 $constmask |= $mask;
117 1         1 $constval |= $value;
118 1         1 $used_bits |= $mask;
119              
120 1         3 next;
121             }
122              
123             ( my $mask, $fieldshift{$name}, $fieldencoder{$name}, $fielddecoder{$name} ) =
124 29         36 @{ shift @args };
  29         76  
125              
126             croak "Field $name is defined twice"
127 29 100       172 if $fieldmask{$name};
128 28 100       134 croak "Field $name collides with other defined fields"
129             if $used_bits & $mask;
130              
131 27         39 $fieldmask{$name} = $mask;
132 27         53 $used_bits |= $mask;
133             }
134              
135 11         16 my $nbits = 0;
136 11         40 $nbits += 8 while( 1 << $nbits < $used_bits );
137              
138             my $packsub = sub {
139 14     14   1039 my %args = @_;
140 14         29 my $ret = $constval;
141 14         38 foreach ( keys %args ) {
142 23         38 my $mask = $fieldmask{$_};
143 23 100 100     66 next if !$mask and $unrecognised_ok;
144 22 100       177 croak "Unexpected field '$_'" unless $mask;
145              
146 21         28 my $v = $args{$_};
147 21 100       50 $v = $fieldencoder{$_}($v) if $fieldencoder{$_};
148 21 100       170 defined $v or croak "Unsupported value for '$_'";
149              
150 20 100       42 if( defined( my $shift = $fieldshift{$_} ) ) {
151 6     6   36 no warnings 'numeric';
  6         15  
  6         2837  
152 6 100 66     261 int $v eq $v and $v >= 0 and $v <= ( $mask >> $shift ) or
      100        
153             croak "Expected an integer value for '$_'";
154 4         9 $ret |= ( $v << $shift ) & $mask;
155             }
156             else {
157 14 50       35 $ret |= $mask if $v;
158             }
159             }
160 10         71 return $ret;
161 11         87 };
162              
163             my $unpacksub = sub {
164 8     8   20 my ( $val ) = @_;
165             # TODO: check constmask
166 8         13 my @ret;
167 8         29 foreach ( keys %fieldmask ) {
168 19         34 my $v = $val & $fieldmask{$_};
169 19 100       44 if( defined( my $shift = $fieldshift{$_} ) ) {
170 5         7 $v >>= $shift;
171             }
172             else {
173 14         21 $v = !!$v;
174             }
175              
176 19 100       42 $v = $fielddecoder{$_}($v) if $fielddecoder{$_};
177 19         41 push @ret, $_ => $v;
178             }
179 8         60 return @ret;
180 11         43 };
181              
182 11 100       30 if( $format ne "integer" ) {
183 5         6 my $orig_packsub = $packsub;
184 5         7 my $orig_unpacksub = $unpacksub;
185              
186 5         7 my $big_endian = ( $format eq "bytes-BE" );
187 5 100       10 my $pointy = $big_endian ? ">" : "<";
188              
189 5 100       13 if( $nbits <= 8 ) {
    50          
    100          
    50          
190 1     1   5 $packsub = sub { pack "C", $orig_packsub->( @_ ) };
  1         3  
191 1     1   3 $unpacksub = sub { $orig_unpacksub->( unpack "C", $_[0] ) };
  1         6  
192             }
193             elsif( $nbits <= 16 ) {
194 0     0   0 $packsub = sub { pack "S$pointy", $orig_packsub->( @_ ) };
  0         0  
195 0     0   0 $unpacksub = sub { $orig_unpacksub->( unpack "S$pointy", $_[0] ) };
  0         0  
196             }
197             elsif( $nbits <= 24 ) {
198 2 100       4 if( $big_endian ) {
199             $packsub = sub {
200 1     1   3 substr( pack( "L>", $orig_packsub->( @_ ) ), 1, 3 )
201 1         4 };
202             $unpacksub = sub {
203 1     1   5 $orig_unpacksub->( unpack "L>", "\0$_[0]" )
204 1         3 };
205             }
206             else {
207             $packsub = sub {
208 1     1   3 substr( pack( "L<", $orig_packsub->( @_ ) ), 0, 3 )
209 1         4 };
210             $unpacksub = sub {
211 1     1   5 $orig_unpacksub->( unpack "L<", "$_[0]\0" )
212 1         5 };
213             }
214             }
215             elsif( $nbits <= 32 ) {
216 2     2   13 $packsub = sub { pack "L$pointy", $orig_packsub->( @_ ) };
  2         5  
217 2     2   8 $unpacksub = sub { $orig_unpacksub->( unpack "L$pointy", $_[0] ) };
  2         9  
218             }
219             else {
220 0         0 croak "Cannot currently handle bytewise packing of $nbits wide values";
221             }
222             }
223              
224 11         15 my %subs;
225              
226 11         27 $subs{"pack_$name"} = $packsub;
227 11         28 $subs{"unpack_$name"} = $unpacksub;
228              
229 6     6   42 no strict 'refs';
  6         13  
  6         1985  
230 11         35 *{"${pkg}::$_"} = $subs{$_} for keys %subs;
  22         133  
231             }
232              
233             =head1 FIELD TYPES
234              
235             =cut
236              
237             =head2 boolfield
238              
239             boolfield( $bitnum )
240              
241             Declares a single bit-wide field at the given bit index, whose value is a
242             simple boolean truth.
243              
244             =cut
245              
246             sub boolfield
247             {
248 21     21 1 2937 my ( $bitnum ) = @_;
249 21         84 return [ 1 << $bitnum, undef ];
250             }
251              
252             =head2 intfield
253              
254             intfield( $bitnum, $width )
255              
256             Declares a field of C<$width> bits wide, starting at the given bit index,
257             whose value is an integer. It will be shifted appropriately.
258              
259             =cut
260              
261             sub intfield
262             {
263 9     9 1 82 my ( $bitnum, $width ) = @_;
264 9         17 my $mask = ( 1 << $width ) - 1;
265 9         21 return [ $mask << $bitnum, $bitnum ];
266             }
267              
268             =head2 enumfield
269              
270             enumfield( $bitnum, @values )
271              
272             Declares a field some number of bits wide, sufficient to store an integer big
273             enough to act as an index into the list of values, starting at the given bit
274             index. Its value will be automatically converted to or from one of the values
275             given, which should act sensibly as strings for comparison purposes. Holes can
276             be placed in the range of supported values by using C.
277              
278             =cut
279              
280             sub enumfield
281             {
282 6     6 1 178 my ( $bitnum, @values ) = @_;
283 6         10 my $nvalues = scalar @values;
284              
285             # Need to ceil(log2) it
286 6         8 my $width = 1;
287 6         16 $width++ while ( 1 << $width ) < $nvalues;
288              
289 6         8 my $def = intfield( $bitnum, $width );
290             $def->[2] = sub {
291 3     3   3 my $v = shift;
292             defined $values[$_] and $v eq $values[$_] and return $_
293 3   66     25 for 0 .. $#values;
      100        
294 1         2 return undef;
295 6         18 };
296             $def->[3] = sub {
297 2     2   5 return $values[shift];
298 6         13 };
299              
300 6         33 return $def;
301             }
302              
303             =head2 constfield
304              
305             constfield( $bitnum, $width, $value )
306              
307             Declares a field some number of bits wide that stores a constant value. This
308             value will be packed automatically.
309              
310             Unlike other field definitions, this field is not named. It returns a
311             2-element list directly for use in the C list.
312              
313             =cut
314              
315             sub constfield
316             {
317 1     1 1 4 my ( $bitnum, $width, $value ) = @_;
318              
319 1 50 33     11 $value >= 0 and $value < ( 1 << $width ) or
320             croak "Invalid value for constfield of width $width";
321              
322 1         4 my $mask = ( 1 << $width ) - 1;
323 1         8 return undef, [ $mask << $bitnum, $value << $bitnum ];
324             }
325              
326             =head1 TODO
327              
328             =over 4
329              
330             =item *
331              
332             More flexible error-handling - missing/extra values to C, extra bits to C.
333              
334             =item *
335              
336             Allow truely-custom field handling, including code to support discontiguous fields.
337              
338             =back
339              
340             =cut
341              
342             =head1 AUTHOR
343              
344             Paul Evans
345              
346             =cut
347              
348             0x55AA;