File Coverage

blib/lib/Data/Plist/BinaryWriter.pm
Criterion Covered Total %
statement 198 199 99.5
branch 38 40 95.0
condition n/a
subroutine 29 29 100.0
pod 22 22 100.0
total 287 290 98.9


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Data::Plist::BinaryWriter - write binary property lists
4             from Perl data structures
5              
6             =head1 SYNOPSIS
7              
8             # Create new
9             my $write = Data::Plist::BinaryWriter->new();
10              
11             # Writing to a string ($ret is binary output)
12             my $ret = $write->write($data);
13              
14             # Writing to a file C<$filename>
15             $write->write($filename, $data);
16              
17             =head1 DESCRIPTION
18              
19             C takes perl data structures,
20             serializes them (see L) and
21             recursively writes to a given filehandle in Apple's binary
22             property list format.
23              
24             =cut
25              
26             package Data::Plist::BinaryWriter;
27              
28 4     4   22315 use strict;
  4         11  
  4         119  
29 4     4   19 use warnings;
  4         8  
  4         99  
30 4     4   2869 use Storable;
  4         10047  
  4         248  
31 4     4   1466 use Math::BigInt;
  4         28100  
  4         31  
32 4     4   15215 use Digest::MD5;
  4         9  
  4         148  
33              
34 4     4   19 use base qw/Data::Plist::Writer/;
  4         6  
  4         3456  
35              
36             =head1 METHODS
37              
38             =head2 write_fh $fh, $data
39              
40             Takes a perl data structure C<$data>, serializes it (see
41             L) and writes it to the given
42             filehandle C<$fh> in Apple's binary property list format.
43              
44             The format starts with "bplist00" and contains a 32-byte
45             trailer. The 32-byte trailer consists of the size of the
46             offset objects in the offset table, the size of the indices
47             of the offset table, the number of objects in the binary
48             file, the index of top object in the binary file and the
49             offset table offset.
50              
51             =cut
52              
53             sub write_fh {
54 29     29 1 36 my $self = shift;
55 29 50       73 $self = $self->new() unless ref $self;
56              
57 29         43 my ( $fh, $object ) = @_;
58 29 100       116 $object = $self->serialize($object) if ( $self->{serialize} );
59 29         77 binmode $fh;
60 29         53 $self->{fh} = $fh;
61 29         47 $self->{index} = [];
62 29         75 $self->{size} = $self->count($object);
63 29         62 $self->{objcache} = {};
64 29 100       69 if ( $self->{size} >= 2**8 ) {
65 1         2 $self->{refsize} = 2;
66             } else {
67 28         45 $self->{refsize} = 1;
68             }
69 29         54 print $fh "bplist00";
70 29         74 my $top_index = $self->dispatch($object);
71 28         88 my $offset_size = $self->bytes( $self->{index}->[-1] );
72 28         35 my $table_offset = tell $fh;
73 28         37 for ( @{ $self->{index} } ) {
  28         60  
74 350         631 my $value = pack $self->pack_in( $offset_size - 1 ), $_;
75 350 100       644 if ( $offset_size == 3 ) {
76 6         8 $value = substr $value, -3;
77             }
78 350         584 print $fh $value;
79             }
80 28         79 print $fh ( pack "x6CC", ($offset_size), $self->{refsize} );
81 28         36 print $fh ( pack "x4N", scalar keys %{ $self->{objcache} } );
  28         84  
82 28         51 print $fh ( pack "x4N", $top_index );
83 28         39 print $fh ( pack "x4N", $table_offset );
84 28         49 close $fh;
85 28         199 return 1;
86             }
87              
88             =head2 dispatch $data
89              
90             Takes serialized data structure C<$data> (see
91             L) and checks its type. Checks
92             the object against previously written objects. If no match
93             is found, calls the appropriate write_ method. Returns the
94             index into the offset table of the offset object that
95             points to the data's position in the binary file.
96              
97             =cut
98              
99             sub dispatch {
100 352     352 1 336 my $self = shift;
101 352         317 my ($arrayref) = @_;
102 352         401 my $type = $arrayref->[0];
103 352         368 my $method = "write_" . $type;
104 352         350 local $Storable::canonical = 1;
105 352         316 my $digest = eval { Digest::MD5::md5_hex( Storable::freeze($arrayref) ) };
  352         721  
106 352 100       10216 die "Can't $method" unless $self->can($method);
107 351 100       1170 $self->{objcache}{$digest} = $self->$method( $arrayref->[1] )
108             unless ( exists $self->{objcache}{$digest} );
109 351         859 return $self->{objcache}{$digest};
110             }
111              
112             =head2 make_type $type, $length
113              
114             Takes a string representing the object's type C<$type> and an
115             integer indicating its size C<$length>. Returns their binary
116             representation.
117              
118             Each object in the binary file is preceded by a byte - the
119             higher nybble denoting its type and the lower its size. For
120             objects whose size is equal to or great than 15, the lower
121             byte contains an f and an integer object is added right
122             after the first byte containing the object's actual size.
123              
124             =cut
125              
126             sub make_type {
127 45     45 1 44 my $self = shift;
128 45         60 my ( $type, $len ) = @_;
129 45         93 my $ans = "";
130              
131 45         44 my $optint = "";
132 45 100       87 if ( $len < 15 ) {
133 37         90 $type .= sprintf( "%x", $len );
134             } else {
135 8         13 $type .= "f";
136 8         27 my $optlen = $self->power($len);
137 8         24 $optint = pack( "C" . $self->pack_in($optlen), hex( "1" . $optlen ),
138             $len );
139             }
140 45         112 $ans = pack( "H*", $type ) . $optint;
141              
142 45         162 return $ans;
143             }
144              
145             =head2 write_integer $int, $type
146              
147             Takes an integer C<$int> and an optional type C<$type>
148             (used for writing UIDs, since they're essentially the
149             same). Returns the index into the offset table of the
150             offset object that points to the integer's location in the
151             binary file.
152              
153             =cut
154              
155             sub write_integer {
156 307     307 1 312 my $self = shift;
157 307         309 my ( $int, $type ) = @_;
158 307         235 my $fmt;
159             my $obj;
160              
161 307 100       457 unless ( defined $type ) {
162 306         294 $type = "1";
163             }
164 307         443 my $len = $self->power($int);
165              
166 307 100       404 if ( $len == 3 ) {
167 2 100       6 if ( $int < 0 ) {
168 1         9 $int += Math::BigInt->new(2)->bpow(64);
169             }
170 2         553 my $hw = Math::BigInt->new($int);
171 2         78 $hw->brsft(32);
172 2         483 my $lw = Math::BigInt->new($int);
173 2         48 $lw->band( Math::BigInt->new("4294967295") );
174              
175 2         452 $obj
176             = $self->make_type( $type, $len )
177             . pack( "N", $hw )
178             . pack( "N", $lw );
179             } else {
180 305         466 $fmt = $self->pack_in($len);
181 305         598 $obj = pack( "C" . $fmt, hex( $type . $len ), $int );
182             }
183 307         587 return $self->binary_write($obj);
184             }
185              
186             =head2 write_string $string
187              
188             Takes a string C<$string> and returns the index into the offset table
189             of the offset object that points to its location in the binary file.
190             It is encoded in the file using UTF-8.
191              
192             =cut
193              
194             sub write_string {
195 18     18 1 25 my $self = shift;
196 18         20 my ($string) = @_;
197 18         51 my $type = $self->make_type( "5", length($string) );
198 18         35 my $obj = $type . $string;
199 18         46 return $self->binary_write($obj);
200             }
201              
202             =head2 write_ustring $ustring
203              
204             Takes a string C<$ustring> and returns the index into the offset table
205             of the offset object that points to its location in the binary file.
206              
207             While C are technically supposed to be stored in UTF-16,
208             there is no known reason for them to not be written as UTF-8 encoded
209             Cs instead; thus, for simplicity, all Cs are written
210             as Cs.
211              
212             =cut
213              
214             sub write_ustring {
215 1     1 1 3 my $self = shift;
216 1         4 return $self->write_string(@_);
217             }
218              
219             =head2 write_dict $dict
220              
221             Takes a hash reference C<$dict> and recursively processes
222             each of its keys and values. Stores indices into the offset
223             table of the offset objects pointing to its keys and values
224             in the binary file. Returns the index into the offset table
225             of the offset object that points to its location in the
226             binary file.
227              
228             =cut
229              
230             sub write_dict {
231 6     6 1 9 my $self = shift;
232 6         25 my $fh = $self->{fh};
233 6         9 my ($hash) = @_;
234 6         8 my @keys;
235             my @values;
236 6         19 for my $key ( keys %$hash ) {
237 7         26 push @keys, $self->dispatch( [ "string", $key ] );
238 7         21 push @values, $self->dispatch( $hash->{$key} );
239             }
240 6         16 my $current = tell $self->{fh};
241 6         21 print $fh $self->make_type( "d", scalar keys(%$hash) );
242 6         22 my $packvar = $self->pack_in( $self->{refsize} - 1 );
243 6         30 print $fh pack $packvar, $_ for @keys, @values;
244 6         8 push @{ $self->{index} }, $current;
  6         13  
245 6         6 return ( @{ $self->{index} } - 1 );
  6         25  
246             }
247              
248             =head2 write_array $array
249              
250             Take an array reference C<$array> and recursively processes
251             its contents. Stores the indices into the offset table of
252             the offset objects pointing to its value. Returns the index
253             into the offset table of the offset object that points to
254             its location in the binary file.
255              
256             =cut
257              
258             sub write_array {
259 7     7 1 12 my $self = shift;
260 7         12 my $fh = $self->{fh};
261 7         9 my ($array) = @_;
262 7         8 my $size = @$array;
263 7         8 my @values;
264 7         13 for (@$array) {
265 309         548 push @values, $self->dispatch($_);
266             }
267 7         15 my $current = tell $self->{fh};
268 7         19 print $fh $self->make_type( "a", $size );
269 7         22 my $packvar = $self->pack_in( $self->{refsize} - 1 );
270 7         163 print $fh pack $packvar, $_ for @values;
271 7         7 push @{ $self->{index} }, $current;
  7         14  
272 7         7 return ( @{ $self->{index} } - 1 );
  7         30  
273             }
274              
275             =head2 write_UID $id
276              
277             Takes a UID C<$id> and returns the index into the offset
278             table of the offset object that points to its location in
279             the binary file. Passes the UID off to L for
280             actual writing, since they're processed in the same manner,
281             simply with different types.
282              
283             =cut
284              
285             sub write_UID {
286 1     1 1 3 my $self = shift;
287 1         3 my ($id) = @_;
288 1         5 return $self->write_integer( $id, "8" );
289             }
290              
291             =head2 write_real $real, $type
292              
293             Takes a float C<$real> and an optional type C<$type>
294             (used for writing dates, since they're essentially the
295             same), and returns the index into the
296             offset table of the offset object that points to its
297             location in the binary file. The bytes of the float are
298             packed in reverse.
299              
300             =cut
301              
302             sub write_real {
303 3     3 1 6 my $self = shift;
304 3         5 my ($float, $type) = @_;
305 3 100       9 unless ( defined $type ) {
306 2         3 $type = "2";
307             }
308 3         11 my $obj = $self->make_type( $type, 3 ) . reverse( pack( "d", $float ) );
309 3         10 return $self->binary_write($obj);
310             }
311              
312             =head2 write_date $date
313              
314             Takes a date C<$date> and returns the index into the offset
315             table of the offset object that points to its location in
316             the binary file. Passes the date off to L for
317             actual writing, since they're processed in the same manner,
318             simply with different types.
319              
320             =cut
321              
322             sub write_date {
323 1     1 1 4 my $self = shift;
324 1         2 my ($date) = @_;
325 1         4 return $self->write_real($date, "3");
326             }
327              
328             =head2 write_null $null
329              
330             Takes a null C<$null> and passes it to L, along
331             with an integer indicating what type of misc it is. The
332             null belongs to the misc category (see L).
333              
334             =cut
335              
336             sub write_null {
337 1     1 1 2 my $self = shift;
338 1         3 return $self->write_misc( 0 );
339             }
340              
341             =head2 write_false $false
342              
343             Takes a false C<$false> and passes it to L, along with an
344             integer indicating what type of misc it is. The false
345             belongs to the misc category (see L).
346              
347             =cut
348              
349             sub write_false {
350 1     1 1 2 my $self = shift;
351 1         4 return $self->write_misc( 8 );
352             }
353              
354             =head2 write_true $true
355              
356             Takes a true C<$true> and passes it to L, along with an
357             integer indicating what type of misc it is. The true
358             belongs to the misc category (see L).
359              
360             =cut
361              
362             sub write_true {
363 1     1 1 2 my $self = shift;
364 1         2 return $self->write_misc( 9 );
365             }
366              
367             =head2 write_fill $fill
368              
369             Takes a fill C<$fill> and passes it to L, along with an
370             integer indicating what type of misc it is. The fill
371             belongs to the misc category (see L).
372              
373             =cut
374              
375             sub write_fill {
376 1     1 1 1 my $self = shift;
377 1         3 return $self->write_misc( 15 );
378             }
379              
380             =head2 write_misc $type
381              
382             Takes an integer indicating an object belonging to the misc
383             category C<$type> (false, null, true or fill) and returns
384             the index into the offset table of the offset object that
385             points to its location in the file.
386              
387             Miscs are a group of data types not easily represented in Perl, and
388             they are written with the only header byte containing a 0 to indicate
389             that they are a misc and their misc type.
390              
391             =cut
392              
393             sub write_misc {
394 4     4 1 5 my $self = shift;
395 4         6 my ( $type ) = @_;
396 4         9 my $obj = $self->make_type( "0", $type );
397 4         10 return $self->binary_write($obj);
398             }
399              
400             =head2 write_data $data
401              
402             Takes some binary data C<$data> and returns the index into the
403             offset table of the offset object that points to its
404             location in the file. Doesn't attempt to process the data
405             at all.
406              
407             =cut
408              
409             sub write_data {
410 5     5 1 9 my $self = shift;
411 5         8 my ($data) = @_;
412 4     4   902 use bytes;
  4         18  
  4         31  
413 5         7 my $len = length $data;
414 5         15 my $obj = $self->make_type( 4, $len ) . $data;
415 5         15 return $self->binary_write($obj);
416             }
417              
418             =head2 count $data
419              
420             Recursively counts the number of objects in a serialized
421             data structure C<$data>. Does not take into account
422             duplicates, so this number might be slightly higher than
423             the number of objects that is indicated in the 32-byte
424             trailer.
425              
426             =cut
427              
428             sub count {
429              
430             # this might be slightly over, since it doesn't take into account duplicates
431 345     345 1 315 my $self = shift;
432 345         293 my ($arrayref) = @_;
433 345         309 my $type = $arrayref->[0];
434 345         247 my $value;
435 345 100       598 if ( $type eq "dict" ) {
    100          
436 6         7 my @keys = ( keys %{ $arrayref->[1] } );
  6         22  
437 6         9 $value = 1 + @keys;
438 6         15 $value += $_ for map { $self->count( $arrayref->[1]->{$_} ) } @keys;
  7         24  
439 6         21 return $value;
440             } elsif ( $type eq "array" ) {
441 7         9 $value = 1;
442 7         9 $value += $_ for map { $self->count($_) } @{ $arrayref->[1] };
  309         454  
  7         13  
443 7         21 return $value;
444             } else {
445 332         528 return 1;
446             }
447             }
448              
449             =head2 binary_write $obj
450              
451             Does the actual writing to the binary file. Takes some
452             binary data C<$obj> and writes it to the filehandle. Also
453             adds the location of the binary data to the offset table
454             and returns the index into the offset table of the current
455             object.
456              
457             =cut
458              
459             sub binary_write {
460 337     337 1 362 my $self = shift;
461 337         363 my $fh = $self->{fh};
462 337         348 my ($obj) = @_;
463 337         395 my $current = tell $self->{fh};
464 337         566 print $fh $obj;
465 337         272 push @{ $self->{index} }, $current;
  337         488  
466 337         294 return ( @{ $self->{index} } - 1 );
  337         1244  
467             }
468              
469             =head2 power $int
470              
471             Calculates the number of bytes necessary to encode an
472             integer C<$int>. Returns a power of 2 indicating the number
473             of bytes.
474              
475             =cut
476              
477             sub power {
478 315     315 1 249 my $self = shift;
479 315         279 my ($int) = @_;
480 315 100       772 if ( $int > 4294967295 ) {
    100          
    100          
    100          
481 1         3 return 3;
482              
483             # actually refers to 2^3 bytes
484             } elsif ( $int > 65535 ) {
485 3         6 return 2;
486              
487             # actually refers to 2^2 bytes
488             } elsif ( $int > 255 ) {
489 48         83 return 1;
490              
491             # I'm sure you see the trend
492             } elsif ( $int < 0 ) {
493 1         2 return 3;
494             } else {
495 262         378 return 0;
496             }
497             }
498              
499             =head2 bytes $int
500              
501             Calculates the number of bytes necessary to encode an
502             integer C<$int>. Returns the actual number of bytes.
503              
504             =cut
505              
506             sub bytes {
507 28     28 1 32 my $self = shift;
508 28         39 my ($int) = @_;
509 28 50       83 if ( $int >= 2**24 ) {
    100          
    100          
510 0         0 return 4;
511              
512             # actually refers to 4 bytes
513             } elsif ( $int >= 2**16 ) {
514 2         73 return 3;
515              
516             # actually refers to 3 bytes
517             } elsif ( $int >= 256 ) {
518 2         6 return 2;
519              
520             # I'm sure you see the trend
521             } else {
522 24         47 return 1;
523             }
524             }
525              
526             =head2 pack_in $int
527              
528             Takes either a power of 2 or a number of bytes C<$int> and
529             returns the format pack() needs for encoding.
530              
531             =cut
532              
533             sub pack_in {
534              
535             # can be used with powers or bytes
536 676     676 1 627 my $self = shift;
537 676         591 my ($int) = @_;
538 676         1288 my $fmt = [ "C", "n", "N", "N" ]->[$int];
539 676         1402 return $fmt;
540             }
541              
542             1;