File Coverage

blib/lib/Mac/PropertyList/SAX.pm
Criterion Covered Total %
statement 135 189 71.4
branch 16 56 28.5
condition 2 16 12.5
subroutine 44 51 86.2
pod 4 4 100.0
total 201 316 63.6


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Mac::PropertyList::SAX - work with Mac plists at a low level, fast
4              
5             =cut
6              
7             package Mac::PropertyList::SAX;
8              
9             =head1 SYNOPSIS
10              
11             See L
12              
13             =head1 DESCRIPTION
14              
15             L is useful, but very slow on large files
16             because it does XML parsing itself, intead of handing it off to a dedicated
17             parser. This module uses L to
18             select a parser capable of doing the heavy lifting, reducing parsing time on
19             large files by a factor of 30 or more.
20              
21             This module does not replace L: it depends
22             on it for some package definitions and plist printing routines. You should,
23             however, be able to replace all C
24             lines with C, without changing anything else, and
25             notice an immediate improvement in performance on large input files.
26              
27             Performance will depend largely on the parser that
28             L selects for you. By default,
29             L is used; to change the parser used, set the
30             environment variable C to a value accepted by
31             $XML::SAX::ParserPackage from
32             L (or set
33             C<$XML::SAX::ParserPackage> directly).
34              
35             =cut
36              
37 19     19   288429 use strict;
  19         61  
  19         599  
38 19     19   119 use warnings;
  19         51  
  19         647  
39              
40 19     19   123 use Carp qw(carp);
  19         53  
  19         1676  
41 19     19   10738 use HTML::Entities qw(encode_entities_numeric);
  19         129027  
  19         1728  
42 19     19   10968 use HTML::Entities::Numbered qw(hex2name name2hex_xml);
  19         44704  
  19         1442  
43             # Passthrough function
44 19     19   10093 use Mac::PropertyList qw(plist_as_string);
  19         551708  
  19         1633  
45 19     19   10911 use XML::SAX::ParserFactory;
  19         99296  
  19         743  
46              
47 19     19   167 use base qw(Exporter);
  19         53  
  19         17847  
48              
49             our @EXPORT_OK = qw(
50             parse_plist
51             parse_plist_fh
52             parse_plist_file
53             parse_plist_string
54             plist_as_string
55             create_from_ref
56             create_from_hash
57             create_from_array
58             );
59              
60             our %EXPORT_TAGS = (
61             all => \@EXPORT_OK,
62             create => [ qw(create_from_ref create_from_hash create_from_array plist_as_string) ],
63             parse => [ qw(parse_plist parse_plist_fh parse_plist_file parse_plist_string) ],
64             );
65              
66             our $VERSION = '0.86';
67              
68              
69              
70             =head1 CLASS VARIABLES
71              
72             Class scoped variables that control the packages settings.
73              
74             =over 4
75              
76             =item ENCODE_ENTITIES
77              
78             Allows the XHTML encoding of the data to be turned off. Default = C<1>
79              
80             =item ENCODE_UNSAFE_CHARS
81              
82             A Perl character class definition containing the only characters to be
83             XHTML encoded. See HTML::Entities::encode_entities for description of
84             the $unsafe_chars parameter. Default = C
85              
86             =cut
87              
88             our $ENCODE_ENTITIES = 1;
89             our $ENCODE_UNSAFE_CHARS = undef;
90              
91             =item OLD_BEHAVIOR
92              
93             Restores the old behavior of double encoding output data. Default = C<0>
94              
95             =cut
96              
97             our $OLD_BEHAVIOR = 0;
98              
99             =item XML::SAX::ParserPackage
100              
101             Parser to use. Can also be set with environment variable
102             C. Default = C<"XML::SAX::Expat">
103              
104             =cut
105              
106             $XML::SAX::ParserPackage = $ENV{MAC_PROPERTYLIST_SAX_PARSER} || "XML::SAX::Expat";
107              
108             =back
109              
110             =head1 EXPORTS
111              
112             By default, no functions are exported. Specify individual functions to export
113             as usual, or use the tags ':all', ':create', and ':parse' for the appropriate
114             sets of functions (':create' includes the create* functions as well as
115             plist_as_string; ':parse' includes the parse* functions).
116              
117             =head1 FUNCTIONS
118              
119             =over 4
120              
121             =item parse_plist_file
122              
123             See L
124              
125             =cut
126              
127             sub parse_plist_file {
128 1     1 1 1882 my $file = shift;
129              
130 1 50       5 if (ref $file) {
131 1         5 parse_plist_fh($file);
132             } else {
133 0 0       0 carp("parse_plist_file: file [$file] does not exist!"), return unless -e $file;
134 0         0 _parse("parse_uri", $file);
135             }
136             }
137              
138             =item parse_plist_fh
139              
140             See L
141              
142             =cut
143              
144 2     2 1 1683 sub parse_plist_fh { _parse("parse_file", @_) }
145              
146             =item parse_plist
147              
148             See L
149              
150             =cut
151              
152 23     23 1 36028 sub parse_plist { _parse("parse_string", @_) }
153              
154             =item parse_plist_string
155              
156             An alias to parse_plist, provided for better regularity compared to Perl SAX.
157              
158             =cut
159              
160             *parse_plist_string = \&parse_plist;
161              
162             sub _parse {
163             # shift off first param in case we use `goto` later (leaving @_ with $data)
164 25     25   68 my $sub = shift;
165 25         74 my ($data) = @_;
166              
167 25         92 my $first;
168             my $fh;
169 25         0 my $delegate;
170              
171             # read initial bytes of file
172             # if we have a binary plist, delegate to Mac::PropertyList
173 25 50       105 if ($sub eq "parse_uri") {
174 0         0 open $fh, "<", $_[0];
175 0         0 $sub = "parse_file";
176 0         0 $_[0] = $fh;
177             # delegate will be set below
178             }
179              
180 25 100       130 if ($sub eq "parse_file") {
    50          
181 2         39 read $_[0], $first, length "bplist";
182 2 50       17 seek $_[0], 0, 0 or die "Can't seek given filehandle"; # seek back to beginning
183 2         8 $delegate = \&Mac::PropertyList::parse_plist_fh;
184             } elsif ($sub eq "parse_string") {
185 23         54 $first = $_[0];
186 23         68 $delegate = \&Mac::PropertyList::parse_plist;
187             }
188              
189 25 50       101 if ($first =~ /^bplist/) {
190             # binary plist -- delegate to non-SAX module
191 0         0 goto $delegate;
192             } else {
193 25         190 my $handler = Mac::PropertyList::SAX::Handler->new;
194 25         1926 XML::SAX::ParserFactory->parser(Handler => $handler)->$sub($data);
195              
196 0         0 return $handler->{struct};
197             }
198             }
199              
200             =item create_from_ref( HASH_REF | ARRAY_REF )
201              
202             Create a plist from an array or hash reference.
203              
204             The values of the hash can be simple scalars or references. Hash and array
205             references are handled recursively, and L objects are output
206             correctly. All other scalars are treated as strings (use L
207             objects to represent other types of scalars).
208              
209             Returns a string representing the reference in serialized plist format.
210              
211             =cut
212              
213             sub create_from_ref {
214             sub _handle_value {
215 14     14   200 my ($val) = @_;
216              
217             sub _handle_hash {
218 3     3   6 my ($hash) = @_;
219             Mac::PropertyList::SAX::dict->write_open,
220 38         159 (map { "\t$_" } map {
221 3         21 Mac::PropertyList::SAX::dict->write_key($OLD_BEHAVIOR ? _escape($_) : $_),
222 7 50       153 _handle_value($hash->{$_}) } keys %$hash),
223             Mac::PropertyList::SAX::dict->write_close
224             }
225              
226             sub _handle_array {
227 2     2   3 my ($array) = @_;
228             Mac::PropertyList::SAX::array->write_open,
229 2         15 (map { "\t$_" } map { _handle_value($_) } @$array),
  8         86  
  5         119  
230             Mac::PropertyList::SAX::array->write_close
231             }
232              
233             # We could hand off serialization of all Mac::PropertyList::Item objects
234             # but there is no 'write' method defined for it (though all its
235             # subclasses have one). Let's just handle Scalars, which are safe.
236 14 100       86 if (UNIVERSAL::can($val, 'write')) { $val->write }
  1 100       4  
    100          
237 3         10 elsif (UNIVERSAL::isa($val, 'HASH')) { _handle_hash ($val) }
238 2         7 elsif (UNIVERSAL::isa($val, 'ARRAY')) { _handle_array($val) }
239 8 50       47 else { Mac::PropertyList::SAX::string->new($OLD_BEHAVIOR ? _escape($val) : $val)->write }
240             }
241              
242 2     2 1 172 $Mac::PropertyList::XML_head .
243             (join "\n", _handle_value(shift)) . "\n" .
244             $Mac::PropertyList::XML_foot;
245             }
246              
247             =item create_from_hash( HASH_REF )
248              
249             Provided for backward compatibility with L: aliases
250             create_from_ref.
251              
252             =cut
253              
254             *create_from_hash = \&create_from_ref;
255              
256             =item create_from_array( ARRAY_REF )
257              
258             Provided for backward compatibility with L: aliases
259             create_from_ref.
260              
261             =cut
262              
263             *create_from_array = \&create_from_ref;
264              
265             =item _escape( STRING )
266              
267             B Escapes illegal characters into XML entities.
268              
269             =cut
270              
271             sub _escape {
272 15     15   316 my $string = join("\n",grep(defined,@_));
273 15 50       53 $ENCODE_ENTITIES &&
274             return name2hex_xml(hex2name(encode_entities_numeric($string,
275             $ENCODE_UNSAFE_CHARS)));
276 0         0 return $string;
277             }
278              
279             package Mac::PropertyList::SAX::Handler;
280              
281 19     19   168 use strict;
  19         53  
  19         479  
282 19     19   271 use warnings;
  19         50  
  19         868  
283             # State definitions
284 19     19   9554 use enum qw(S_EMPTY S_TOP S_FREE S_DICT S_ARRAY S_KEY S_TEXT);
  19         21593  
  19         130  
285              
286 19     19   12221 use Carp qw(carp croak);
  19         51  
  19         961  
287 19     19   9254 use MIME::Base64;
  19         11591  
  19         1315  
288              
289             # Element-name definitions
290 19         2081 use constant +{ qw( ROOT plist
291             KEY key
292             DATA data
293             DICT dict
294 19     19   160 ARRAY array ) };
  19         90  
295              
296 19     19   139 use base qw(XML::SAX::Base);
  19         44  
  19         15945  
297              
298             # From the plist DTD
299             our (%types, %simple_types, %complex_types, %numerical_types);
300             {
301             my @complex_types = (DICT, ARRAY);
302             my @numerical_types = qw(real integer true false);
303             my @simple_types = qw(data date real integer string true false);
304             my @types = (@complex_types, @numerical_types, @simple_types);
305              
306             my $atoh = sub { map { $_ => 1 } @_ };
307              
308             %types = $atoh->(@ types);
309             %simple_types = $atoh->(@ simple_types);
310             %complex_types = $atoh->(@ complex_types);
311             %numerical_types = $atoh->(@numerical_types);
312             }
313              
314             sub new {
315 25     25   180 my %args = (
316             accum => "",
317             context => S_EMPTY,
318             key => undef,
319             stack => [ ],
320             struct => undef,
321             );
322              
323 25         276 shift->SUPER::new(%args, @_)
324             }
325              
326             sub start_element {
327 0     0   0 my $self = shift;
328 0         0 my ($data) = @_;
329 0         0 my $name = $data->{Name};
330              
331             # State transition definitions
332 0 0 0     0 if ($self->{context} == S_EMPTY and $name eq ROOT) {
    0 0        
      0        
333 0         0 $self->{context} = S_TOP;
334             } elsif ($self->{context} == S_TOP or $types{$name} or $name eq KEY) {
335 0         0 push @{ $self->{stack} }, {
336             key => $self->{key},
337             context => $self->{context},
338             struct => $self->{struct},
339 0         0 };
340              
341 0 0       0 if ($complex_types{$name}) {
    0          
    0          
342 0         0 $self->{struct} = "Mac::PropertyList::SAX::$name"->new;
343 0         0 $self->{context} = eval "S_" . uc $name;
344 0         0 delete $self->{key};
345             }
346 0         0 elsif ($simple_types{$name}) { $self->{context} = S_TEXT }
347             elsif ($name eq KEY) {
348 0 0       0 croak " in improper context $self->{context}" unless $self->{context} == S_DICT;
349 0         0 $self->{context} = S_KEY;
350             }
351 0         0 else { croak "Top-level element '$name' in plist is not recognized" }
352             } else {
353 0         0 croak "Received invalid start element '$name'";
354             }
355             }
356              
357             sub end_element {
358 0     0   0 my $self = shift;
359 0         0 my ($data) = @_;
360 0         0 my $name = $data->{Name};
361              
362 0 0       0 if ($name ne ROOT) { # Discard plist element
363 0         0 my $elt = pop @{ $self->{stack} };
  0         0  
364              
365 0         0 my $value = $self->{struct};
366 0         0 ($self->{struct}, $self->{key}, $self->{context}) = @{$elt}{qw(struct key context)};
  0         0  
367              
368 0 0       0 if ($simple_types{$name}) {
    0          
369             # Wrap accumulated character data in an object
370             $value = "Mac::PropertyList::SAX::$name"->new(
371             exists $self->{accum}
372             ? $name eq DATA
373             ? MIME::Base64::decode_base64($self->{accum})
374             : $self->{accum}
375 0 0       0 : ""
    0          
376             );
377              
378 0         0 delete $self->{accum};
379             } elsif ($name eq KEY) {
380 0         0 $self->{key} = $self->{accum};
381 0         0 delete $self->{accum};
382 0         0 return;
383             }
384              
385 0 0       0 if ($self->{context} == S_DICT ) { $self->{struct}{$self->{key}} = $value }
  0 0       0  
    0          
386 0         0 elsif ($self->{context} == S_ARRAY) { push @{ $self->{struct} }, $value }
  0         0  
387 0         0 elsif ($self->{context} == S_TOP ) { $self->{struct} = $value }
388 0         0 else { croak "Bad context $self->{context}" }
389             }
390             }
391              
392             sub characters {
393 0     0   0 my $self = shift;
394 0         0 my ($data) = @_;
395 0 0 0     0 $self->{accum} .= $data->{Data} if $self->{context} == S_TEXT or $self->{context} == S_KEY;
396             }
397              
398             # Convenient subclasses
399             package Mac::PropertyList::SAX::array;
400 19     19   336220 use base qw(Mac::PropertyList::array);
  19         74  
  19         7697  
401             package Mac::PropertyList::SAX::dict;
402 19     19   160 use base qw(Mac::PropertyList::dict);
  19         54  
  19         6775  
403 7   50 7   16 sub write_key { "" . (Mac::PropertyList::SAX::_escape($_[1]) || '') . "" }
404             package Mac::PropertyList::SAX::Scalar;
405 19     19   176 use base qw(Mac::PropertyList::Scalar);
  19         68  
  19         7041  
406             sub write {
407 8   50 8   23 $_[0]->write_open .
408             (Mac::PropertyList::SAX::_escape($_[0]->value) || '') .
409             $_[0]->write_close
410             }
411 19     19   166 use overload '""' => sub { $_[0]->as_basic_data };
  19     0   60  
  19         266  
  0         0  
412             package Mac::PropertyList::SAX::date;
413 19     19   1605 use base qw(Mac::PropertyList::date Mac::PropertyList::SAX::Scalar);
  19         50  
  19         10196  
414             package Mac::PropertyList::SAX::real;
415 19     19   154 use base qw(Mac::PropertyList::real Mac::PropertyList::SAX::Scalar);
  19         48  
  19         9889  
416             package Mac::PropertyList::SAX::integer;
417 19     19   156 use base qw(Mac::PropertyList::integer Mac::PropertyList::SAX::Scalar);
  19         47  
  19         9742  
418             package Mac::PropertyList::SAX::string;
419 19     19   147 use base qw(Mac::PropertyList::string Mac::PropertyList::SAX::Scalar);
  19         44  
  19         9934  
420 8     8   71 sub write { $_[0]->Mac::PropertyList::SAX::Scalar::write }
421 19     19   149 use overload '""' => sub { $_[0]->as_basic_data };
  19     0   45  
  19         234  
  0         0  
422             package Mac::PropertyList::SAX::data;
423 19     19   1473 use base qw(Mac::PropertyList::data Mac::PropertyList::SAX::Scalar);
  19         52  
  19         9397  
424             package Mac::PropertyList::SAX::Boolean;
425 19     19   10068 use Object::MultiType;
  19         35716  
  19         749  
426 19     19   154 use base qw(Mac::PropertyList::Boolean Object::MultiType);
  19         46  
  19         7243  
427 19     19   162 use overload '""' => sub { shift->value };
  19     0   49  
  19         154  
  0         0  
428             sub new {
429 2     2   36 my $class = shift;
430 2         17 my ($type) = $class =~ /::([^:]+)$/;
431 2         8 my $b = lc $type eq "true";
432 2         15 bless Object::MultiType->new(scalar => $type, bool => $b) => $class
433             }
434 0     0     sub value { ${${$_[0]}->scalar} }
  0            
  0            
435             package Mac::PropertyList::SAX::true;
436 19     19   3460 use base qw(Mac::PropertyList::SAX::Boolean Mac::PropertyList::true);
  19         51  
  19         10767  
437             package Mac::PropertyList::SAX::false;
438 19     19   145 use base qw(Mac::PropertyList::SAX::Boolean Mac::PropertyList::true);
  19         47  
  19         10126  
439              
440             1;
441              
442             __END__