File Coverage

blib/lib/Image/ExifTool/PLIST.pm
Criterion Covered Total %
statement 165 193 85.4
branch 88 134 65.6
condition 43 75 57.3
subroutine 9 10 90.0
pod 0 5 0.0
total 305 417 73.1


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: PLIST.pm
3             #
4             # Description: Read Apple PLIST information
5             #
6             # Revisions: 2013-02-01 - P. Harvey Created
7             #
8             # References: 1) http://www.apple.com/DTDs/PropertyList-1.0.dtd
9             # 2) http://opensource.apple.com/source/CF/CF-550/CFBinaryPList.c
10             #
11             # Notes: - Sony MODD files also use XML PLIST format, but with a few quirks
12             #
13             # - Decodes both the binary and XML-based PLIST formats
14             #------------------------------------------------------------------------------
15              
16             package Image::ExifTool::PLIST;
17              
18 7     7   3658 use strict;
  7         16  
  7         256  
19 7     7   37 use vars qw($VERSION);
  7         14  
  7         288  
20 7     7   40 use Image::ExifTool qw(:DataAccess :Utils);
  7         14  
  7         1419  
21 7     7   2573 use Image::ExifTool::XMP;
  7         53  
  7         491  
22 7     7   39 use Image::ExifTool::GPS;
  7         13  
  7         14082  
23              
24             $VERSION = '1.09';
25              
26             sub ExtractObject($$;$);
27             sub Get24u($$);
28              
29             # access routines to read various-sized integer/real values (add 0x100 to size for reals)
30             my %readProc = (
31             1 => \&Get8u,
32             2 => \&Get16u,
33             3 => \&Get24u,
34             4 => \&Get32u,
35             8 => \&Get64u,
36             0x104 => \&GetFloat,
37             0x108 => \&GetDouble,
38             );
39              
40             # recognize different types of PLIST files based on certain tags
41             my %plistType = (
42             adjustmentBaseVersion => 'AAE',
43             );
44              
45             # PLIST tags (generated on-the-fly for most tags)
46             %Image::ExifTool::PLIST::Main = (
47             PROCESS_PROC => \&ProcessPLIST,
48             GROUPS => { 0 => 'PLIST', 1 => 'XML', 2 => 'Document' },
49             VARS => { LONG_TAGS => 4 },
50             NOTES => q{
51             Apple Property List tags. ExifTool reads both XML and binary-format PLIST
52             files, and will extract any existing tags even if they aren't listed below.
53             These tags belong to the family 0 "PLIST" group, but family 1 group may be
54             either "XML" or "PLIST" depending on whether the format is XML or binary.
55             },
56             #
57             # tags found in PLIST information of QuickTime iTunesInfo iTunMOVI atom (ref PH)
58             #
59             'cast//name' => { Name => 'Cast', List => 1 },
60             'directors//name' => { Name => 'Directors', List => 1 },
61             'producers//name' => { Name => 'Producers', List => 1 },
62             'screenwriters//name' => { Name => 'Screenwriters', List => 1 },
63             'codirectors//name' => { Name => 'Codirectors', List => 1 }, # (NC)
64             'studio//name' => { Name => 'Studio', List => 1 }, # (NC)
65             #
66             # tags found in MODD files (ref PH)
67             #
68             'MetaDataList//DateTimeOriginal' => {
69             Name => 'DateTimeOriginal',
70             Description => 'Date/Time Original',
71             Groups => { 2 => 'Time' },
72             # Sony uses a "real" here -- number of days since Dec 31, 1899
73             ValueConv => 'IsFloat($val) ? ConvertUnixTime(($val - 25569) * 24 * 3600) : $val',
74             PrintConv => '$self->ConvertDateTime($val)',
75             },
76             'MetaDataList//Duration' => {
77             Name => 'Duration',
78             Groups => { 2 => 'Video' },
79             PrintConv => 'ConvertDuration($val)',
80             },
81             'MetaDataList//Geolocation/Latitude' => {
82             Name => 'GPSLatitude',
83             Groups => { 2 => 'Location' },
84             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
85             },
86             'MetaDataList//Geolocation/Longitude' => {
87             Name => 'GPSLongitude',
88             Groups => { 2 => 'Location' },
89             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
90             },
91             'MetaDataList//Geolocation/MapDatum' => {
92             Name => 'GPSMapDatum',
93             Groups => { 2 => 'Location' },
94             },
95             XMLFileType => {
96             # recognize MODD files by their content
97             RawConv => q{
98             if ($val eq 'ModdXML' and $$self{FILE_TYPE} eq 'XMP') {
99             $self->OverrideFileType('MODD');
100             }
101             return $val;
102             },
103             },
104             );
105              
106             #------------------------------------------------------------------------------
107             # We found a PLIST XML property name/value
108             # Inputs: 0) ExifTool object ref, 1) tag table ref
109             # 2) reference to array of XML property names (last is current property)
110             # 3) property value, 4) attribute hash ref (not used here)
111             # Returns: 1 if valid tag was found
112             sub FoundTag($$$$;$)
113             {
114 37     37 0 67 my ($et, $tagTablePtr, $props, $val, $attrs) = @_;
115 37 50       62 return 0 unless @$props;
116 37         93 my $verbose = $et->Options('Verbose');
117 37   100     76 my $keys = $$et{PListKeys} || ( $$et{PListKeys} = [] );
118              
119 37         51 my $prop = $$props[-1];
120 37 50       58 if ($verbose > 1) {
121 0         0 $et->VPrint(0, $$et{INDENT}, '[', join('/',@$props), ' = ',
122             $et->Printable($val), "]\n");
123             }
124             # un-escape XML character entities
125 37         72 $val = Image::ExifTool::XMP::UnescapeXML($val);
126              
127             # handle the various PLIST properties
128 37 100 66     119 if ($prop eq 'data') {
    100          
    100          
129 2 50 33     14 if ($val =~ /^[0-9a-f]+$/ and not length($val) & 0x01) {
130             # MODD files use ASCII-hex encoded "data"...
131 0         0 my $buff = pack('H*', $val);
132 0         0 $val = \$buff;
133             } else {
134             # ...but the PLIST DTD specifies Base64 encoding
135 2         6 $val = Image::ExifTool::XMP::DecodeBase64($val);
136             }
137             } elsif ($prop eq 'date') {
138 2         9 $val = Image::ExifTool::XMP::ConvertXMPDate($val);
139             } elsif ($prop eq 'true' or $prop eq 'false') {
140 1         2 $val = ucfirst $prop;
141             } else {
142             # convert from UTF8 to ExifTool Charset
143 32         66 $val = $et->Decode($val, 'UTF8');
144 32 100       61 if ($prop eq 'key') {
145 17 100       30 if (@$props <= 3) { # top-level key should be plist/dict/key
146 15         25 @$keys = ( $val );
147             } else {
148             # save key names to be used in tag name
149 2         5 push @$keys, '' while @$keys < @$props - 3;
150 2         5 pop @$keys while @$keys > @$props - 2;
151 2         3 $$keys[@$props - 3] = $val;
152             }
153 17         36 return 0;
154             }
155             }
156              
157 20 100       44 return 0 unless @$keys; # can't store value if no associated key
158              
159 18         40 my $tag = join '/', @$keys; # generate tag ID from 'key' values
160 18         30 my $tagInfo = $$tagTablePtr{$tag};
161 18 100       28 unless ($tagInfo) {
162 16 50       26 $et->VPrint(0, $$et{INDENT}, "[adding $tag]\n") if $verbose;
163             # generate tag name from ID
164 16         19 my $name = $tag;
165 16         23 $name =~ s{^MetaDataList//}{}; # shorten long MODD metadata tag names
166 16         21 $name =~ s{//name$}{}; # remove unnecessary MODD "name" property
167 16         30 $name =~ s/([^A-Za-z])([a-z])/$1\u$2/g; # capitalize words
168 16         25 $name =~ tr/-_a-zA-Z0-9//dc; # remove illegal characters
169 16         54 $tagInfo = { Name => ucfirst($name), List => 1 };
170 16 100       28 if ($prop eq 'date') {
171 2         5 $$tagInfo{Groups}{2} = 'Time';
172 2         4 $$tagInfo{PrintConv} = '$self->ConvertDateTime($val)';
173             }
174 16         36 AddTagToTable($tagTablePtr, $tag, $tagInfo);
175             }
176             # allow list-behaviour only for consecutive tags with the same ID
177 18 100 100     65 if ($$et{LastPListTag} and $$et{LastPListTag} ne $tagInfo) {
178 14         36 delete $$et{LIST_TAGS}{$$et{LastPListTag}};
179             }
180 18         28 $$et{LastPListTag} = $tagInfo;
181             # override file type if applicable
182 18 50 66     40 $et->OverrideFileType($plistType{$tag}) if $plistType{$tag} and $$et{FILE_TYPE} eq 'XMP';
183             # save the tag
184 18         42 $et->HandleTag($tagTablePtr, $tag, $val);
185              
186 18         41 return 1;
187             }
188              
189             #------------------------------------------------------------------------------
190             # Get big-endian 24-bit integer
191             # Inputs: 0) data ref, 1) offset
192             # Returns: integer value
193             sub Get24u($$)
194             {
195 0     0 0 0 my ($dataPt, $off) = @_;
196 0         0 return unpack 'N', "\0" . substr($$dataPt, $off, 3);
197             }
198              
199             #------------------------------------------------------------------------------
200             # Extract object from binary PLIST file at the current file position (ref 2)
201             # Inputs: 0) ExifTool ref, 1) PLIST info ref, 2) parent tag ID (undef for top)
202             # Returns: the object, or undef on error
203             sub ExtractObject($$;$)
204             {
205 49     49 0 82 my ($et, $plistInfo, $parent) = @_;
206 49         69 my $raf = $$plistInfo{RAF};
207 49         59 my ($buff, $val);
208              
209 49 50       98 $raf->Read($buff, 1) == 1 or return undef;
210 49         71 my $type = ord($buff) >> 4;
211 49         62 my $size = ord($buff) & 0x0f;
212 49 100 100     252 if ($type == 0) { # null/bool/fill
    100 100        
    50          
213 1         11 $val = { 0x00=>'', 0x08=>'True', 0x09=>'False', 0x0f=>'' }->{$size};
214             } elsif ($type == 1 or $type == 2 or $type == 3) { # int, float or date
215 13         25 $size = 1 << $size;
216 13 100       50 my $proc = ($type == 1 ? $readProc{$size} : $readProc{$size + 0x100}) or return undef;
    50          
217 13 50       33 $val = &$proc(\$buff, 0) if $raf->Read($buff, $size) == $size;
218 13 100 66     65 if ($type == 3 and defined $val) { # date
219             # dates are referenced to Jan 1, 2001 (11323 days from Unix time zero)
220 3         15 $val = Image::ExifTool::ConvertUnixTime($val + 11323 * 24 * 3600, 1);
221 3         9 $$plistInfo{DateFormat} = 1;
222             }
223             } elsif ($type == 8) { # UID
224 0         0 ++$size;
225 0 0       0 $raf->Read($buff, $size) == $size or return undef;
226 0         0 my $proc = $readProc{$size};
227 0 0       0 if ($proc) {
    0          
228 0         0 $val = &$proc(\$buff, 0);
229             } elsif ($size == 16) {
230 0         0 require Image::ExifTool::ASF;
231 0         0 $val = Image::ExifTool::ASF::GetGUID($buff);
232             } else {
233 0         0 $val = "0x" . unpack 'H*', $buff;
234             }
235             } else {
236             # $size is the size of the remaining types
237 35 100       63 if ($size == 0x0f) {
238             # size is stored in extra integer object
239 4         28 $size = ExtractObject($et, $plistInfo);
240 4 50 33     39 return undef unless defined $size and $size =~ /^\d+$/;
241             }
242 35 100 66     96 if ($type == 4) { # data
    100 66        
    100          
    50          
243 1 50 33     5 if ($size < 1000000 or $et->Options('Binary')) {
244 1 50       4 $raf->Read($buff, $size) == $size or return undef;
245             } else {
246 0         0 $buff = "Binary data $size bytes";
247             }
248 1         2 $val = \$buff; # (return reference for binary data)
249             } elsif ($type == 5) { # ASCII string
250 26 50       59 $raf->Read($val, $size) == $size or return undef;
251             } elsif ($type == 6) { # UCS-2BE string
252 1         2 $size *= 2;
253 1 50       4 $raf->Read($buff, $size) == $size or return undef;
254 1         6 $val = $et->Decode($buff, 'UCS2');
255             } elsif ($type == 10 or $type == 12 or $type == 13) { # array, set or dict
256             # the remaining types store a list of references
257 7         16 my $refSize = $$plistInfo{RefSize};
258 7         13 my $refProc = $$plistInfo{RefProc};
259 7 100       17 my $num = $type == 13 ? $size * 2 : $size;
260 7         12 my $len = $num * $refSize;
261 7 50       20 $raf->Read($buff, $len) == $len or return undef;
262 7         16 my $table = $$plistInfo{Table};
263 7         15 my ($i, $ref, @refs, @array);
264 7         60 for ($i=0; $i<$num; ++$i) {
265 39         77 my $ref = &$refProc(\$buff, $i * $refSize);
266 39 50       70 return 0 if $ref >= @$table;
267 39         71 push @refs, $ref;
268             }
269 7 100       25 if ($type == 13) { # dict
270             # prevent infinite recursion
271 3 50 66     13 if (defined $parent and length $parent > 1000) {
272 0         0 $et->WarnOnce('Possible deep recursion while parsing PLIST');
273 0         0 return undef;
274             }
275 3         7 my $tagTablePtr = $$plistInfo{TagTablePtr};
276 3         12 my $verbose = $et->Options('Verbose');
277 3         13 for ($i=0; $i<$size; ++$i) {
278             # get the entry key
279 15 50       45 $raf->Seek($$table[$refs[$i]], 0) or return undef;
280 15         54 my $key = ExtractObject($et, $plistInfo);
281 15 50 33     50 next unless defined $key and length $key; # silently ignore bad dict entries
282             # get the entry value
283 15 50       36 $raf->Seek($$table[$refs[$i+$size]], 0) or return undef;
284             # generate an ID for this tag
285 15 100       35 my $tag = defined $parent ? "$parent/$key" : $key;
286 15         24 undef $$plistInfo{DateFormat};
287 15         30 my $val = ExtractObject($et, $plistInfo, $tag);
288 15 100 66     55 next if not defined $val or ref($val) eq 'HASH';
289 14         41 my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
290 14 50       27 unless ($tagInfo) {
291 0 0       0 $et->VPrint(0, $$et{INDENT}, "[adding $tag]\n") if $verbose;
292 0         0 my $name = $tag;
293 0         0 $name =~ s/([^A-Za-z])([a-z])/$1\u$2/g; # capitalize words
294 0         0 $name =~ tr/-_a-zA-Z0-9//dc; # remove illegal characters
295 0         0 $tagInfo = { Name => ucfirst($name), List => 1 };
296 0 0       0 if ($$plistInfo{DateFormat}) {
297 0         0 $$tagInfo{Groups}{2} = 'Time';
298 0         0 $$tagInfo{PrintConv} = '$self->ConvertDateTime($val)';
299             }
300 0         0 AddTagToTable($tagTablePtr, $tag, $tagInfo);
301             }
302             # allow list-behaviour only for consecutive tags with the same ID
303 14 100 66     61 if ($$et{LastPListTag} and $$et{LastPListTag} ne $tagInfo) {
304 12         33 delete $$et{LIST_TAGS}{$$et{LastPListTag}};
305             }
306 14         23 $$et{LastPListTag} = $tagInfo;
307 14         32 $et->HandleTag($tagTablePtr, $tag, $val);
308             }
309 3         10 $val = { }; # flag the value as a dictionary (ie. tags already saved)
310             } else {
311             # extract the referenced objects
312 4         9 foreach $ref (@refs) {
313 9 50       21 $raf->Seek($$table[$ref], 0) or return undef; # seek to this object
314 9         25 $val = ExtractObject($et, $plistInfo, $parent);
315 9 50 33     33 next unless defined $val and ref $val ne 'HASH';
316 9         19 push @array, $val;
317             }
318 4         13 $val = \@array;
319             }
320             }
321             }
322 49         112 return $val;
323             }
324              
325             #------------------------------------------------------------------------------
326             # Process binary PLIST data (ref 2)
327             # Inputs: 0) ExifTool object ref, 1) DirInfo ref, 2) tag table ref
328             # Returns: 1 on success (and returns plist value as $$dirInfo{Value})
329             sub ProcessBinaryPLIST($$$)
330             {
331 6     6 0 13 my ($et, $dirInfo, $tagTablePtr) = @_;
332 6         12 my ($i, $buff, @table);
333 6         10 my $dataPt = $$dirInfo{DataPt};
334              
335 6         19 $et->VerboseDir('Binary PLIST');
336 6         18 SetByteOrder('MM');
337              
338 6 100       24 if ($dataPt) {
339 5         9 my $start = $$dirInfo{DirStart};
340 5 100 33     26 if ($start or ($$dirInfo{DirLen} and $$dirInfo{DirLen} != length $$dataPt)) {
      66        
341 1   50     5 my $buf2 = substr($$dataPt, $start || 0, $$dirInfo{DirLen});
342 1         3 $$dirInfo{RAF} = new File::RandomAccess(\$buf2);
343             } else {
344 4         15 $$dirInfo{RAF} = new File::RandomAccess($dataPt);
345             }
346 5   100     18 my $strt = $$dirInfo{DirStart} || 0;
347             }
348             # read and parse the trailer
349 6 50       17 my $raf = $$dirInfo{RAF} or return 0;
350 6 50 33     14 $raf->Seek(-32,2) and $raf->Read($buff,32)==32 or return 0;
351 6         22 my $intSize = Get8u(\$buff, 6);
352 6         13 my $refSize = Get8u(\$buff, 7);
353 6         37 my $numObj = Get64u(\$buff, 8);
354 6         19 my $topObj = Get64u(\$buff, 16);
355 6         13 my $tableOff = Get64u(\$buff, 24);
356              
357 6 50       19 return 0 if $topObj >= $numObj;
358 6 50       23 my $intProc = $readProc{$intSize} or return 0;
359 6 50       14 my $refProc = $readProc{$refSize} or return 0;
360              
361             # read and parse the offset table
362 6         11 my $tableSize = $intSize * $numObj;
363 6 50 33     25 $raf->Seek($tableOff, 0) and $raf->Read($buff, $tableSize) == $tableSize or return 0;
364 6         21 for ($i=0; $i<$numObj; ++$i) {
365 45         85 push @table, &$intProc(\$buff, $i * $intSize);
366             }
367 6         31 my %plistInfo = (
368             RAF => $raf,
369             RefSize => $refSize,
370             RefProc => $refProc,
371             Table => \@table,
372             TagTablePtr => $tagTablePtr,
373             );
374             # position file pointer at the top object, and extract it
375 6 50       20 $raf->Seek($table[$topObj], 0) or return 0;
376 6         22 $$dirInfo{Value} = ExtractObject($et, \%plistInfo);
377 6 50       36 return defined $$dirInfo{Value} ? 1 : 0;
378             }
379              
380             #------------------------------------------------------------------------------
381             # Extract information from a PLIST file
382             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
383             # Returns: 1 on success, 0 if this wasn't valid PLIST
384             sub ProcessPLIST($$;$)
385             {
386 6     6 0 20 my ($et, $dirInfo, $tagTablePtr) = @_;
387              
388             # process XML PLIST data using the XMP module
389 6         19 $$dirInfo{XMPParseOpts}{FoundProc} = \&FoundTag;
390 6         25 my $result = Image::ExifTool::XMP::ProcessXMP($et, $dirInfo, $tagTablePtr);
391 6         18 delete $$dirInfo{XMPParseOpts};
392              
393 6 100       67 unless ($result) {
394 4         10 my $buff;
395 4 50       15 my $raf = $$dirInfo{RAF} or return 0;
396 4 50 33     16 $raf->Seek(0,0) and $raf->Read($buff, 64) or return 0;
397 4 100 33     30 if ($buff =~ /^bplist0/) {
    50 33        
398             # binary PLIST file
399 1         4 my $tagTablePtr = GetTagTable('Image::ExifTool::PLIST::Main');
400 1         3 $et->SetFileType('PLIST', 'application/x-plist');
401 1         2 $$et{SET_GROUP1} = 'PLIST';
402 1 50       4 unless (ProcessBinaryPLIST($et, $dirInfo, $tagTablePtr)) {
403 0         0 $et->Error('Error reading binary PLIST file');
404             }
405 1         3 delete $$et{SET_GROUP1};
406 1         2 $result = 1;
407             } elsif ($$et{FILE_EXT} and $$et{FILE_EXT} eq 'PLIST' and
408             $buff =~ /^\xfe\xff\x00/)
409             {
410             # (have seen very old PLIST files encoded as UCS-2BE with leading BOM)
411 0         0 $et->Error('Old PLIST format currently not supported');
412 0         0 $result = 1;
413             }
414             }
415 6         15 return $result;
416             }
417              
418             1; # end
419              
420             __END__