File Coverage

blib/lib/Mac/PropertyList/ReadBinary.pm
Criterion Covered Total %
statement 172 188 91.4
branch 30 56 53.5
condition 3 3 100.0
subroutine 27 27 100.0
pod 2 2 100.0
total 234 276 84.7


line stmt bran cond sub pod time code
1 5     5   2912 use v5.10;
  5         14  
2              
3             package Mac::PropertyList::ReadBinary;
4 5     5   25 use strict;
  5         8  
  5         116  
5 5     5   23 use warnings;
  5         9  
  5         197  
6              
7 5     5   25 use Carp;
  5         8  
  5         251  
8 5     5   990 use Data::Dumper;
  5         10801  
  5         247  
9 5     5   1886 use Encode qw(decode);
  5         32692  
  5         327  
10 5     5   860 use Mac::PropertyList;
  5         10  
  5         181  
11 5     5   4668 use Math::BigInt;
  5         104983  
  5         28  
12 5     5   96688 use MIME::Base64 qw(decode_base64);
  5         3049  
  5         355  
13 5     5   1953 use POSIX qw(SEEK_END SEEK_SET);
  5         27135  
  5         26  
14 5     5   6190 use XML::Entities ();
  5         11  
  5         9478  
15              
16             our $VERSION = '1.504';
17              
18             my $Debug = $ENV{PLIST_DEBUG} || 0;
19              
20             __PACKAGE__->_run( @ARGV ) unless caller;
21              
22             =encoding utf8
23              
24             =head1 NAME
25              
26             Mac::PropertyList::ReadBinary - read binary property list files
27              
28             =head1 SYNOPSIS
29              
30             # use directly
31             use Mac::PropertyList::ReadBinary;
32              
33             my $parser = Mac::PropertyList::ReadBinary->new( $file );
34              
35             my $plist = $parser->plist;
36              
37             # use indirectly, automatically selects right reader
38             use Mac::PropertyList;
39              
40             my $plist = parse_plist_file( $file );
41              
42             =head1 DESCRIPTION
43              
44             This module is a low-level interface to the Mac OS X Property List
45             (plist) format. You probably shouldn't use this in
46             applications—build interfaces on top of this so you don't have to
47             put all the heinous multi-level object stuff where people have to look
48             at it.
49              
50             You can parse a plist file and get back a data structure. You can take
51             that data structure and get back the plist as XML (but not binary
52             yet). If you want to change the structure inbetween that's your
53             business. :)
54              
55             See L for more details.
56              
57             =head2 Methods
58              
59             =over 4
60              
61             =item new( FILENAME | SCALAR_REF | FILEHANDLE )
62              
63             Opens the data source, doing the right thing for filenames,
64             scalar references, or a filehandle.
65              
66             =cut
67              
68             sub new {
69 9     9 1 1099 my( $class, $source ) = @_;
70              
71 9         25 my $self = bless { source => $source }, $class;
72              
73 9         36 $self->_read;
74              
75 9         20 $self;
76             }
77              
78 38     38   330 sub _source { $_[0]->{source} }
79 1651     1651   3880 sub _fh { $_[0]->{fh} }
80 249     249   600 sub _trailer { $_[0]->{trailer} }
81 488     488   1916 sub _offsets { $_[0]->{offsets} }
82 195     195   268 sub _object_ref_size { $_[0]->_trailer->{ref_size} }
83              
84             =item plist
85              
86             Returns the C data structure.
87              
88             =cut
89              
90 9     9 1 455 sub plist { $_[0]->{parsed} }
91              
92             sub _object_size {
93             $_[0]->_trailer->{object_count} * $_[0]->_trailer->{offset_size}
94 9     9   37 }
95              
96             sub _read {
97 9     9   15 my( $self, $thingy ) = @_;
98              
99 9         29 $self->{fh} = $self->_get_filehandle;
100 9         27 $self->_read_plist_trailer;
101              
102 9         39 $self->_get_offset_table;
103              
104 9         16 my $top = $self->_read_object_at_offset( $self->_trailer->{top_object} );
105              
106 9         22 $self->{parsed} = $top;
107             }
108              
109             sub _get_filehandle {
110 13     13   2824 my( $self, $thingy ) = @_;
111              
112 13         19 my $fh;
113              
114 13 100       35 if( ! ref $self->_source ) { # filename
    100          
    50          
115 3 100       14 open $fh, "<", $self->_source
116 1         10 or die "Could not open [@{[$self->_source]}]! $!";
117             }
118             elsif( ref $self->_source eq ref \ '' ) { # scalar ref
119 3 50   3   16 open $fh, "<", $self->_source or croak "Could not open file! $!";
  3         5  
  3         24  
  9         37  
120             }
121             elsif( ref $self->_source ) { # filehandle
122 1         2 $fh = $self->_source;
123             }
124             else {
125 0         0 croak( 'No source to read from!' );
126             }
127              
128 12         1953 $fh;
129             }
130              
131             sub _read_plist_trailer {
132 9     9   17 my $self = shift;
133              
134 9         20 seek $self->_fh, -32, SEEK_END;
135              
136 9         12 my $buffer;
137 9         18 read $self->_fh, $buffer, 32;
138 9         31 my %hash;
139              
140 9         65 @hash{ qw( offset_size ref_size object_count top_object table_offset ) }
141             = unpack "x6 C C (x4 N)3", $buffer;
142              
143 9         22 $self->{trailer} = \%hash;
144             }
145              
146             sub _get_offset_table {
147 9     9   16 my $self = shift;
148              
149 9         16 seek $self->_fh, $self->_trailer->{table_offset}, SEEK_SET;
150              
151 9         20 my $try_to_read = $self->_object_size;
152              
153 9         11 my $raw_offset_table;
154 9         15 my $read = read $self->_fh, $raw_offset_table, $try_to_read;
155              
156 9 50       35 croak "reading offset table failed!" unless $read == $try_to_read;
157              
158 9         24 my @offsets = unpack ["","C*","n*","(H6)*","N*"]->[$self->_trailer->{offset_size}], $raw_offset_table;
159              
160 9         25 $self->{offsets} = \@offsets;
161              
162 9 50       17 if( $self->_trailer->{offset_size} == 3 ) {
163 0         0 @offsets = map { hex } @offsets;
  0         0  
164             }
165             }
166              
167             sub _read_object_at_offset {
168 488     488   731 my( $self, $offset ) = @_;
169              
170 488         2135 my @caller = caller(1);
171              
172 488         1178 seek $self->_fh, ${ $self->_offsets }[$offset], SEEK_SET;
  488         718  
173              
174 488         953 $self->_read_object;
175             }
176              
177             # # # # # # # # # # # # # #
178              
179 0         0 BEGIN {
180              
181 5     5   87 my %singletons = (
182             0 => undef,
183             8 => Mac::PropertyList::false->new(),
184             9 => Mac::PropertyList::true->new(),
185              
186             # 15 is also defined (as "fill") in the comments to Apple's
187             # implementation in CFBinaryPList.c but Apple's actual code has no
188             # support for it at all, either reading or writing, so it's
189             # probably not important to implement.
190             );
191              
192             my $type_readers = {
193             0 => sub { # the odd balls
194 4         8 my( $self, $length ) = @_;
195              
196 4 50       15 return $singletons{ $length } if exists $singletons{ $length };
197              
198 0         0 croak ( sprintf "Unknown type byte %02X\n", $length );
199             },
200              
201             1 => sub { # integers
202 174         277 my( $self, $power2 ) = @_;
203              
204 174 50       268 croak "Integer with <$power2> bytes is not supported" if $power2 > 3;
205              
206 174         217 my $byte_length = 1 << $power2;
207              
208 174         204 my( $buffer, $value );
209 174         252 read $self->_fh, $buffer, $byte_length;
210              
211 174         349 my @formats = qw( C n N NN NNNN );
212 174         299 my @values = unpack $formats[$power2], $buffer;
213              
214 174 50       365 if( $power2 == 3 ) {
    50          
215 0         0 my( $high, $low ) = @values;
216              
217 0         0 my $b = Math::BigInt->new($high)->blsft(32)->bior($low);
218 0 0       0 if( $b->bcmp(Math::BigInt->new(2)->bpow(63)) > 0) {
219 0         0 $b -= Math::BigInt->new(2)->bpow(64);
220             }
221              
222 0         0 @values = ( $b );
223             }
224             elsif( $power2 == 4 ) {
225 0         0 my( $highest, $higher, $high, $low ) = @values;
226 0         0 my $b = Math::BigInt
227             ->new($highest)
228             ->blsft(32)->bior($higher)
229             ->blsft(32)->bior($high)
230             ->blsft(32)->bior($low);
231              
232 0 0       0 if( $b->bcmp(Math::BigInt->new(2)->bpow(127)) > 0) {
233 0         0 $b -= Math::BigInt->new(2)->bpow(128);
234             }
235              
236 0         0 @values = ( $b );
237             }
238              
239 174         447 return Mac::PropertyList::integer->new( $values[0] );
240             },
241              
242             2 => sub { # reals
243 6         12 my( $self, $length ) = @_;
244 6 50       14 croak "Real > 8 bytes" if $length > 3;
245 6 50       10 croak "Bad length [$length]" if $length < 2;
246              
247 6         12 my $byte_length = 1 << $length;
248              
249 6         8 my( $buffer, $value );
250 6         19 read $self->_fh, $buffer, $byte_length;
251              
252 6         14 my @formats = qw( a a f> d> );
253 6         15 my @values = unpack $formats[$length], $buffer;
254              
255 6         30 return Mac::PropertyList::real->new( $values[0] );
256             },
257              
258             3 => sub { # date
259 6         11 my( $self, $length ) = @_;
260 6 50       12 croak "Date != 8 bytes" if $length != 3;
261 6         11 my $byte_length = 1 << $length;
262              
263 6         9 my( $buffer, $value );
264 6         10 read $self->_fh, $buffer, $byte_length;
265              
266 6         18 my @values = unpack 'd>', $buffer;
267              
268 6         15 $self->{MLen} += 9;
269              
270 6         297 my $adjusted_time = POSIX::strftime(
271             "%Y-%m-%dT%H:%M:%SZ",
272             gmtime( 978307200 + $values[0])
273             );
274              
275 6         42 return Mac::PropertyList::date->new( $adjusted_time );
276             },
277              
278             4 => sub { # binary data
279 2         5 my( $self, $length ) = @_;
280              
281 2         10 my( $buffer, $value );
282 2         7 read $self->_fh, $buffer, $length;
283              
284 2         18 return Mac::PropertyList::data->new( $buffer );
285             },
286              
287             5 => sub { # utf8 string
288 287         472 my( $self, $length ) = @_;
289              
290 287         333 my( $buffer, $value );
291 287         432 read $self->_fh, $buffer, $length;
292              
293 287         588 $buffer = Encode::decode( 'ascii', $buffer );
294              
295 287         6685 return Mac::PropertyList::string->new( $buffer );
296             },
297              
298             6 => sub { # unicode string
299 4         8 my( $self, $length ) = @_;
300              
301 4         6 my( $buffer, $value );
302 4         8 read $self->_fh, $buffer, 2 * $length;
303              
304 4         10 $buffer = Encode::decode( "UTF-16BE", $buffer );
305              
306 4         6313 return Mac::PropertyList::ustring->new( $buffer );
307             },
308              
309             8 => sub { # UIDs
310 8         14 my( $self, $length ) = @_;
311              
312 8         14 my $byte_length = $length + 1;
313              
314 8         12 read $self->_fh, ( my $buffer ), $byte_length;
315              
316 8         18 my $value = unpack 'H*', $buffer;
317              
318 8         27 return Mac::PropertyList::uid->new( $value );
319             },
320              
321             a => sub { # array
322 40         83 my( $self, $elements ) = @_;
323              
324 40         49 my @objects = do {
325 40         45 my $buffer;
326 40         70 read $self->_fh, $buffer, $elements * $self->_object_ref_size;
327 40 50       61 unpack(
328             ($self->_object_ref_size == 1 ? "C*" : "n*"), $buffer
329             );
330             };
331              
332             my @array =
333 40         84 map { $self->_read_object_at_offset( $objects[$_] ) }
  69         109  
334             0 .. $elements - 1;
335              
336 40         122 return Mac::PropertyList::array->new( \@array );
337             },
338              
339             d => sub { # dictionary
340 23         40 my( $self, $length ) = @_;
341              
342 23         29 my @key_indices = do {
343 23         27 my $buffer;
344 23         40 my $s = $self->_object_ref_size;
345 23         33 read $self->_fh, $buffer, $length * $self->_object_ref_size;
346 23 50       37 unpack(
347             ($self->_object_ref_size == 1 ? "C*" : "n*"), $buffer
348             );
349             };
350              
351 23         30 my @objects = do {
352 23         36 my $buffer;
353 23         35 read $self->_fh, $buffer, $length * $self->_object_ref_size;
354 23 50       60 unpack(
355             ($self->_object_ref_size == 1 ? "C*" : "n*"), $buffer
356             );
357             };
358              
359             my %dict = map {
360 23         53 my $key = $self->_read_object_at_offset($key_indices[$_])->value;
  205         353  
361 205         431 my $value = $self->_read_object_at_offset($objects[$_]);
362 205         509 ( $key, $value );
363             } 0 .. $length - 1;
364              
365 23         87 return Mac::PropertyList::dict->new( \%dict );
366             },
367 5         375 };
368              
369             sub _read_object {
370 554     554   725 my $self = shift;
371 554 50       881 say "Reading object!" if $Debug;
372 554         592 my $buffer;
373 554 50       746 say "\tTELL: ", tell( $self->_fh ) if $Debug;
374 554 50       733 croak "read() failed while trying to get type byte! $!"
375             unless read( $self->_fh, $buffer, 1) == 1;
376              
377 554         1211 my $length = unpack( "C*", $buffer ) & 0x0F;
378 554 50       816 say "\tlength is $length" if $Debug;
379 554         933 $buffer = unpack "H*", $buffer;
380 554 50       836 say "\t", join '', map { sprintf '%02x', ord } split //, $buffer if $Debug;
  0         0  
381 554         828 my $type = substr $buffer, 0, 1;
382 554 50       787 say "\ttype is $type" if $Debug;
383              
384 554 100 100     1503 $length = $self->_read_object->value if $type ne "0" && $length == 15;
385              
386 554         793 my $sub = $type_readers->{ $type };
387 554         647 my $result = eval { $sub->( $self, $length ) };
  554         866  
388 554 50       1198 croak "$@" if $@;
389 554 50       758 say "RESULT: ", Dumper($result) if $Debug;
390 554         1275 return $result;
391             }
392              
393             }
394              
395             =back
396              
397             =head1 SEE ALSO
398              
399             Some of the ideas are cribbed from CFBinaryPList.c
400              
401             http://opensource.apple.com/source/CF/CF-550/CFBinaryPList.c
402              
403             =head1 SOURCE AVAILABILITY
404              
405             This project is in Github:
406              
407             https://github.com/briandfoy/mac-propertylist.git
408              
409             =head1 CREDITS
410              
411             =head1 AUTHOR
412              
413             brian d foy, C<< >>
414              
415             Tom Wyant added support for UID types.
416              
417             =head1 COPYRIGHT AND LICENSE
418              
419             Copyright © 2004-2021, brian d foy . All rights reserved.
420              
421             This program is free software; you can redistribute it and/or modify
422             it under the terms of the Artistic License 2.0.
423              
424             =cut
425              
426             "See why 1984 won't be like 1984";