File Coverage

blib/lib/Data/Bitfield.pm
Criterion Covered Total %
statement 131 144 90.9
branch 37 50 74.0
condition 13 18 72.2
subroutine 22 28 78.5
pod 6 7 85.7
total 209 247 84.6


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-2019 -- leonerd@leonerd.org.uk
5              
6             package Data::Bitfield;
7              
8 5     5   283306 use strict;
  5         46  
  5         139  
9 5     5   25 use warnings;
  5         10  
  5         206  
10              
11             our $VERSION = '0.04';
12              
13 5     5   29 use Exporter 'import';
  5         10  
  5         290  
14             our @EXPORT_OK = qw( bitfield boolfield intfield signed_intfield enumfield constfield );
15              
16 5     5   31 use Carp;
  5         9  
  5         3209  
17              
18             =head1 NAME
19              
20             C - manage data packed into bytes of multiple bit fields
21              
22             =head1 SYNOPSIS
23              
24             use Data::Bitfield qw( bitfield boolfield enumfield );
25              
26             # The stat(2) st_mode field on Linux
27             bitfield MODE =>
28             format => enumfield(12,
29             undef, "fifo", "char", undef, "dir", undef, "block", undef,
30             "regular", undef, "link", undef, "socket", undef, undef, undef ),
31             set_uid => boolfield(11),
32             set_gid => boolfield(10),
33             sticky => boolfield(9),
34             user_read => boolfield(8),
35             user_write => boolfield(7),
36             user_exec => boolfield(6),
37             group_read => boolfield(5),
38             group_write => boolfield(4),
39             group_exec => boolfield(3),
40             other_read => boolfield(2),
41             other_write => boolfield(1),
42             other_exec => boolfield(0);
43              
44             my %modebits = unpack_MODE( stat($path)->mode );
45              
46             Z<>
47              
48             # The flag register of a Z80
49             bitfield FLAGS =>
50             sign => boolfield(7),
51             zero => boolfield(6),
52             halfcarry => boolfield(4),
53             parity => boolfield(2),
54             subtract => boolfield(1),
55             carry => boolfield(0);
56              
57             =head1 DESCRIPTION
58              
59             This module provides a single primary function, C, which creates
60             helper functions in the package that calls it, to assist in managing data that
61             is encoded in sets of bits, called bitfields. This may be useful when
62             interacting with a low-level networking protocol, binary file format, hardware
63             devices, or similar purposes.
64              
65             =head2 bitfield
66              
67             bitfield $name, %fields
68              
69             Creates two new functions in the calling package whose names are derived from
70             the string C<$name> passed here. These functions will be symmetric opposites,
71             which convert between a key/value list of field values, and their packed
72             binary byte-string or integer representation.
73              
74             $packed_value = pack_$name( %field_values )
75              
76             %field_values = unpack_$name( $packed_value )
77              
78             These two functions will work to a set of field names that match those field
79             definitions given to the C function that declared them.
80              
81             Each field has a name and a definition. Its definition comes from one of the
82             following field-declaration functions.
83              
84             Additional options may be passed by giving a C reference as the first
85             argument, before the structure name.
86              
87             bitfield { %options }, $name, %fields
88              
89             Recognised options are:
90              
91             =over 4
92              
93             =item format => "bytes-LE" | "bytes-BE" | "integer"
94              
95             Defines the format that the C function will return and the
96             C function will expect to receive as input. The two C
97             formats describe a packed binary string in either little- or big-endian
98             direction, and C describes an integer numerical value.
99              
100             Note that currently the C format is limited to values 32bits wide or
101             smaller.
102              
103             Optional; will default to C if not supplied. This default may change
104             in a later version - make sure to always specify it for now.
105              
106             =item unrecognised_ok => BOOL
107              
108             If true, the C function will not complain about unrecognised field
109             names; they will simply be ignored.
110              
111             =back
112              
113             =cut
114              
115             sub bitfield
116             {
117 18     18 1 41 my $pkg = caller;
118 18         58 bitfield_into_caller( $pkg, @_ );
119             }
120              
121             my %VALID_FORMATS = map { $_ => 1 } qw( bytes-LE bytes-BE integer );
122              
123             sub bitfield_into_caller
124             {
125 18     18 0 32 my $pkg = shift;
126 18 100       57 my %options = ( ref $_[0] eq "HASH" ) ? %{ +shift } : ();
  13         65  
127 18         57 my ( $name, @args ) = @_;
128              
129 18         41 my $unrecognised_ok = !!$options{unrecognised_ok};
130 18   100     62 my $format = $options{format} // "integer";
131 18 50       46 $VALID_FORMATS{$format} or
132             croak "Invalid 'format' value $format";
133              
134 18         42 my $used_bits = '';
135              
136 18         23 my $constmask = '';
137 18         36 my $constval = '';
138              
139 18         50 my %fields;
140              
141 18         46 while( @args ) {
142 39         67 my $name = shift @args;
143 39 100       80 if( !defined $name ) {
144 1         2 my ( $shift, $width, $value ) = @{ shift @args };
  1         4  
145 1         4 my $mask = pack "L<", ( ( 1 << $width ) - 1 ) << $shift;
146              
147 1 50       12 croak "Constfield collides with other defined bits"
148             if ( $used_bits & $mask ) !~ m/^\0*$/;
149              
150 1         4 $constval |= pack( "L<", $value << $shift );
151 1         2 $used_bits |= $mask;
152              
153 1         3 next;
154             }
155              
156 38         51 my ( $shift, $width, $encoder, $decoder ) = @{ shift @args };
  38         79  
157 38         85 my $offs = int( $shift / 8 ); $shift %= 8;
  38         67  
158              
159 38         140 my $mask = ( "\x00" x $offs ) . pack "L<", ( ( 1 << $width ) - 1 ) << $shift;
160              
161             croak "Field $name is defined twice"
162 38 100       202 if $fields{$name};
163 37 100       312 croak "Field $name collides with other defined fields"
164             if ( $used_bits & $mask ) !~ m/^\0*$/;
165              
166 36 100       83 if( $format eq "bytes-BE" ) {
167 3 100       6 $shift += 8 if $width <= 8;
168 3 50       7 $shift += 8 if $width <= 16;
169 3 50       7 $shift += 8 if $width <= 24;
170             }
171              
172 36         102 $fields{$name} = [ $mask, $offs, $shift, $encoder, $decoder ];
173 36         106 $used_bits |= $mask;
174             }
175              
176 16         68 $used_bits =~ s/\0+$//;
177 16         33 my $datalen = length $used_bits;
178              
179 16 100       40 my $fmt = $format eq "bytes-BE" ? "L>" : "L<";
180              
181             my $packsub = sub {
182 19     19   509 my %args = @_;
183 19         33 my $ret = $constval;
184 19         55 foreach ( keys %args ) {
185 31 50 100     298 defined( my $f = $fields{$_} ) or
      66        
186             $unrecognised_ok and next or
187             croak "Unexpected field '$_'";
188              
189 29         76 my ( $mask, $offs, $shift, $encoder ) = @$f;
190              
191 29         43 my $v = $args{$_};
192 29 100       67 $v = $encoder->($v) if $encoder;
193 29 100       232 defined $v or croak "Unsupported value for '$_'";
194              
195             {
196 5     5   46 no warnings 'numeric';
  5         9  
  5         3172  
  28         45  
197 28 100       298 int $v eq $v or
198             croak "Expected an integer value for '$_'";
199             }
200              
201 27         99 my $bits = ( "\x00" x $offs ) . ( pack $fmt, $v << $shift );
202 27 100 66     276 $v >= 0 and ( $bits & ~$mask ) =~ m/^\0+$/ or
203             croak "Value out of range for '$_'";
204              
205 26         72 $ret |= $mask & $bits;
206             }
207 15         137 return substr( $ret, 0, $datalen );
208 16         130 };
209              
210             my $unpacksub = sub {
211 12     12   30 my ( $val ) = @_;
212             # Bitwise extend so there's always enough bits to unpack
213 12         23 $val .= "\0\0\0";
214             # TODO: check constmask
215 12         20 my @ret;
216 12         40 foreach ( keys %fields ) {
217 26         43 my $f = $fields{$_};
218 26         62 my ( $mask, $offs, $shift, undef, $decoder ) = @$f;
219              
220 26         91 my $v = unpack( $fmt, substr( $val & $mask, $offs, 4 ) ) >> $shift;
221              
222 26 100       74 $v = $decoder->($v) if $decoder;
223 26         60 push @ret, $_ => $v;
224             }
225 12         99 return @ret;
226 16         102 };
227              
228 16 100       46 if( $format eq "integer" ) {
229 4         10 my $orig_packsub = $packsub;
230 4         13 my $orig_unpacksub = $unpacksub;
231              
232 4         10 my $nbits = $datalen * 8;
233              
234 4 50       23 if( $nbits <= 8 ) {
    0          
    0          
    0          
235 4     6   22 $packsub = sub { unpack "C", $orig_packsub->( @_ ) };
  6         574  
236 4     2   13 $unpacksub = sub { $orig_unpacksub->( pack "C", $_[0] ) };
  2         18  
237             }
238             elsif( $nbits <= 16 ) {
239 0     0   0 $packsub = sub { unpack "S<", $orig_packsub->( @_ ) };
  0         0  
240 0     0   0 $unpacksub = sub { $orig_unpacksub->( pack "S<", $_[0] ) };
  0         0  
241             }
242             elsif( $nbits <= 24 ) {
243 0     0   0 $packsub = sub { unpack( "L<", $orig_packsub->( @_ ) . "\0" ) };
  0         0  
244 0     0   0 $unpacksub = sub { $orig_unpacksub->( pack "L<", $_[0] ) };
  0         0  
245             }
246             elsif( $nbits <= 32 ) {
247 0     0   0 $packsub = sub { unpack "L<", $orig_packsub->( @_ ) };
  0         0  
248 0     0   0 $unpacksub = sub { $orig_unpacksub->( pack "L<", $_[0] ) };
  0         0  
249             }
250             else {
251 0         0 croak "Cannot currently handle integer packing of $nbits wide values";
252             }
253             }
254              
255 16         25 my %subs;
256              
257 16         50 $subs{"pack_$name"} = $packsub;
258 16         45 $subs{"unpack_$name"} = $unpacksub;
259              
260 5     5   42 no strict 'refs';
  5         10  
  5         3088  
261 16         60 *{"${pkg}::$_"} = $subs{$_} for keys %subs;
  32         161  
262             }
263              
264             =head1 FIELD TYPES
265              
266             =cut
267              
268             =head2 boolfield
269              
270             boolfield( $bitnum )
271              
272             Declares a single bit-wide field at the given bit index, whose value is a
273             simple boolean truth.
274              
275             =cut
276              
277             sub boolfield
278             {
279 19     19 1 3743 my ( $bitnum ) = @_;
280 19     10   122 return [ $bitnum, 1, sub { 0 + !!shift }, sub { !!shift } ];
  12         33  
  10         21  
281             }
282              
283             =head2 intfield
284              
285             intfield( $bitnum, $width )
286              
287             Declares a field of C<$width> bits wide, starting at the given bit index,
288             whose value is an unsigned integer. It will be shifted appropriately.
289              
290             =cut
291              
292             sub intfield
293             {
294 20     20 1 1733 my ( $bitnum, $width ) = @_;
295 20         70 return [ $bitnum, $width ];
296             }
297              
298             =head2 signed_intfield
299              
300             signed_intfield( $bitnum, $width )
301              
302             I
303              
304             Declares a field of C<$width> bits wide, starting at the given bit index,
305             whose value is a signed integer. It will be shifted appropriately.
306              
307             =cut
308              
309             sub signed_intfield
310             {
311 2     2 1 525 my ( $bitnum, $width ) = @_;
312              
313 2         6 my $def = intfield( $bitnum, $width );
314              
315 2         5 my $excess = 2 ** $width;
316 2         3 my $sign = ( 1 << ($width-1) );
317              
318             $def->[2] = sub {
319 2     2   5 my $v = shift;
320 2 100       17 $v += $excess if $v < 0;
321 2         6 return $v;
322 2         8 };
323             $def->[3] = sub {
324 2     2   3 my $v = shift;
325 2 100       6 $v -= $excess if $v & $sign;
326 2         3 return $v;
327 2         9 };
328              
329 2         15 return $def;
330             }
331              
332             =head2 enumfield
333              
334             enumfield( $bitnum, @values )
335              
336             Declares a field some number of bits wide, sufficient to store an integer big
337             enough to act as an index into the list of values, starting at the given bit
338             index. Its value will be automatically converted to or from one of the values
339             given, which should act sensibly as strings for comparison purposes. Holes can
340             be placed in the range of supported values by using C.
341              
342             =cut
343              
344             sub enumfield
345             {
346 6     6 1 774 my ( $bitnum, @values ) = @_;
347 6         12 my $nvalues = scalar @values;
348              
349             # Need to ceil(log2) it
350 6         12 my $width = 1;
351 6         18 $width++ while ( 1 << $width ) < $nvalues;
352              
353 6         13 my $def = intfield( $bitnum, $width );
354             $def->[2] = sub {
355 3     3   4 my $v = shift;
356             defined $values[$_] and $v eq $values[$_] and return $_
357 3   66     29 for 0 .. $#values;
      100        
358 1         3 return undef;
359 6         22 };
360             $def->[3] = sub {
361 2     2   5 return $values[shift];
362 6         19 };
363              
364 6         27 return $def;
365             }
366              
367             =head2 constfield
368              
369             constfield( $bitnum, $width, $value )
370              
371             Declares a field some number of bits wide that stores a constant value. This
372             value will be packed automatically.
373              
374             Unlike other field definitions, this field is not named. It returns a
375             2-element list directly for use in the C list.
376              
377             =cut
378              
379             sub constfield
380             {
381 1     1 1 3 my ( $bitnum, $width, $value ) = @_;
382              
383 1 50 33     8 $value >= 0 and $value < ( 1 << $width ) or
384             croak "Invalid value for constfield of width $width";
385              
386 1         5 return undef, [ $bitnum, $width, $value ];
387             }
388              
389             =head1 TODO
390              
391             =over 4
392              
393             =item *
394              
395             More flexible error-handling - missing/extra values to C, extra bits to C.
396              
397             =item *
398              
399             Allow truely-custom field handling, including code to support discontiguous fields.
400              
401             =back
402              
403             =cut
404              
405             =head1 AUTHOR
406              
407             Paul Evans
408              
409             =cut
410              
411             0x55AA;