File Coverage

blib/lib/Image/ExifTool/MacOS.pm
Criterion Covered Total %
statement 79 243 32.5
branch 21 150 14.0
condition 10 48 20.8
subroutine 6 11 54.5
pod 0 8 0.0
total 116 460 25.2


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: MacOS.pm
3             #
4             # Description: Read/write MacOS system tags
5             #
6             # Revisions: 2017/03/01 - P. Harvey Created
7             # 2020/10/13 - PH Added ability to read MacOS "._" files
8             #------------------------------------------------------------------------------
9              
10             package Image::ExifTool::MacOS;
11 42     42   4919 use strict;
  42         112  
  42         1949  
12 42     42   267 use vars qw($VERSION);
  42         101  
  42         2509  
13 42     42   295 use Image::ExifTool qw(:DataAccess :Utils);
  42         129  
  42         197667  
14              
15             $VERSION = '1.12';
16              
17             sub MDItemLocalTime($);
18             sub ProcessATTR($$$);
19              
20             my %mdDateInfo = (
21             ValueConv => \&MDItemLocalTime,
22             PrintConv => '$self->ConvertDateTime($val)',
23             );
24              
25             # Information decoded from Mac OS sidecar files
26             %Image::ExifTool::MacOS::Main = (
27             GROUPS => { 0 => 'File', 1 => 'MacOS' },
28             NOTES => q{
29             Note that on some filesystems, MacOS creates sidecar files with names that
30             begin with "._". ExifTool will read these files if specified, and extract
31             the information listed in the following table without the need for extra
32             options, but these files are not writable directly.
33             },
34             2 => {
35             Name => 'RSRC',
36             SubDirectory => { TagTable => 'Image::ExifTool::RSRC::Main' },
37             },
38             9 => {
39             Name => 'ATTR',
40             SubDirectory => {
41             TagTable => 'Image::ExifTool::MacOS::XAttr',
42             ProcessProc => \&ProcessATTR,
43             },
44             },
45             );
46              
47             # "mdls" tags (ref PH)
48             %Image::ExifTool::MacOS::MDItem = (
49             WRITE_PROC => \&Image::ExifTool::DummyWriteProc,
50             VARS => { NO_ID => 1 },
51             GROUPS => { 0 => 'File', 1 => 'MacOS', 2 => 'Other' },
52             NOTES => q{
53             MDItem tags are extracted using the "mdls" utility. They are extracted if
54             any "MDItem*" tag or the MacOS group is specifically requested, or by
55             setting the API L option to 1 or the API L option to 2 or
56             higher. Note that these tags do not necessarily reflect the current
57             metadata of a file -- it may take some time for the MacOS mdworker daemon to
58             index the file after a metadata change.
59             },
60             MDItemFinderComment => {
61             Writable => 1,
62             WritePseudo => 1,
63             Protected => 1, # (all writable pseudo tags must be protected)
64             },
65             MDItemFSLabel => {
66             Writable => 1,
67             WritePseudo => 1,
68             Protected => 1, # (all writable pseudo tags must be protected)
69             WriteCheck => '$val =~ /^[0-7]$/ ? undef : "Not an integer in the range 0-7"',
70             PrintConv => {
71             0 => '0 (none)',
72             1 => '1 (Gray)',
73             2 => '2 (Green)',
74             3 => '3 (Purple)',
75             4 => '4 (Blue)',
76             5 => '5 (Yellow)',
77             6 => '6 (Red)',
78             7 => '7 (Orange)',
79             },
80             },
81             MDItemFSCreationDate => {
82             Writable => 1,
83             WritePseudo => 1,
84             DelCheck => q{"Can't delete"},
85             Protected => 1, # (all writable pseudo tags must be protected)
86             Shift => 'Time', # (but not supported yet)
87             Notes => q{
88             file creation date. Requires "setfile" for writing. Note that when
89             reading, it may take a few seconds after writing a file before this value
90             reflects the change. However, L is updated immediately
91             },
92             Groups => { 2 => 'Time' },
93             ValueConv => \&MDItemLocalTime,
94             ValueConvInv => '$val',
95             PrintConv => '$self->ConvertDateTime($val)',
96             PrintConvInv => '$self->InverseDateTime($val)',
97             },
98             MDItemAcquisitionMake => { Groups => { 2 => 'Camera' } },
99             MDItemAcquisitionModel => { Groups => { 2 => 'Camera' } },
100             MDItemAltitude => { Groups => { 2 => 'Location' } },
101             MDItemAperture => { Groups => { 2 => 'Camera' } },
102             MDItemAudioBitRate => { Groups => { 2 => 'Audio' } },
103             MDItemAudioChannelCount => { Groups => { 2 => 'Audio' } },
104             MDItemAuthors => { Groups => { 2 => 'Author' } },
105             MDItemBitsPerSample => { Groups => { 2 => 'Image' } },
106             MDItemCity => { Groups => { 2 => 'Location' } },
107             MDItemCodecs => { },
108             MDItemColorSpace => { Groups => { 2 => 'Image' } },
109             MDItemComment => { },
110             MDItemContentCreationDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
111             MDItemContentCreationDateRanking => { Groups => { 2 => 'Time' }, %mdDateInfo },
112             MDItemContentModificationDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
113             MDItemContentType => { },
114             MDItemContentTypeTree => { },
115             MDItemContributors => { },
116             MDItemCopyright => { Groups => { 2 => 'Author' } },
117             MDItemCountry => { Groups => { 2 => 'Location' } },
118             MDItemCreator => { Groups => { 2 => 'Document' } },
119             MDItemDateAdded => { Groups => { 2 => 'Time' }, %mdDateInfo },
120             MDItemDescription => { },
121             MDItemDisplayName => { },
122             MDItemDownloadedDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
123             MDItemDurationSeconds => { PrintConv => 'ConvertDuration($val)' },
124             MDItemEncodingApplications => { },
125             MDItemEXIFGPSVersion => { Groups => { 2 => 'Location' }, Description => 'MD Item EXIF GPS Version' },
126             MDItemEXIFVersion => { },
127             MDItemExposureMode => { Groups => { 2 => 'Camera' } },
128             MDItemExposureProgram => { Groups => { 2 => 'Camera' } },
129             MDItemExposureTimeSeconds => { Groups => { 2 => 'Camera' } },
130             MDItemFlashOnOff => { Groups => { 2 => 'Camera' } },
131             MDItemFNumber => { Groups => { 2 => 'Camera' } },
132             MDItemFocalLength => { Groups => { 2 => 'Camera' } },
133             MDItemFSContentChangeDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
134             MDItemFSCreatorCode => { Groups => { 2 => 'Author' } },
135             MDItemFSFinderFlags => { },
136             MDItemFSHasCustomIcon => { },
137             MDItemFSInvisible => { },
138             MDItemFSIsExtensionHidden => { },
139             MDItemFSIsStationery => { },
140             MDItemFSName => { },
141             MDItemFSNodeCount => { },
142             MDItemFSOwnerGroupID => { },
143             MDItemFSOwnerUserID => { },
144             MDItemFSSize => { },
145             MDItemFSTypeCode => { },
146             MDItemGPSDateStamp => { Groups => { 2 => 'Time' } },
147             MDItemGPSStatus => { Groups => { 2 => 'Location' } },
148             MDItemGPSTrack => { Groups => { 2 => 'Location' } },
149             MDItemHasAlphaChannel => { Groups => { 2 => 'Image' } },
150             MDItemImageDirection => { Groups => { 2 => 'Location' } },
151             MDItemInterestingDateRanking => { Groups => { 2 => 'Time' }, %mdDateInfo },
152             MDItemISOSpeed => { Groups => { 2 => 'Camera' } },
153             MDItemKeywords => { },
154             MDItemKind => { },
155             MDItemLastUsedDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
156             MDItemLastUsedDate_Ranking => { },
157             MDItemLatitude => { Groups => { 2 => 'Location' } },
158             MDItemLensModel => { },
159             MDItemLogicalSize => { },
160             MDItemLongitude => { Groups => { 2 => 'Location' } },
161             MDItemMediaTypes => { },
162             MDItemNumberOfPages => { },
163             MDItemOrientation => { Groups => { 2 => 'Image' } },
164             MDItemOriginApplicationIdentifier => { },
165             MDItemOriginMessageID => { },
166             MDItemOriginSenderDisplayName => { },
167             MDItemOriginSenderHandle => { },
168             MDItemOriginSubject => { },
169             MDItemPageHeight => { Groups => { 2 => 'Image' } },
170             MDItemPageWidth => { Groups => { 2 => 'Image' } },
171             MDItemPhysicalSize => { Groups => { 2 => 'Image' } },
172             MDItemPixelCount => { Groups => { 2 => 'Image' } },
173             MDItemPixelHeight => { Groups => { 2 => 'Image' } },
174             MDItemPixelWidth => { Groups => { 2 => 'Image' } },
175             MDItemProfileName => { Groups => { 2 => 'Image' } },
176             MDItemRedEyeOnOff => { Groups => { 2 => 'Camera' } },
177             MDItemResolutionHeightDPI => { Groups => { 2 => 'Image' } },
178             MDItemResolutionWidthDPI => { Groups => { 2 => 'Image' } },
179             MDItemSecurityMethod => { },
180             MDItemSpeed => { Groups => { 2 => 'Location' } },
181             MDItemStateOrProvince => { Groups => { 2 => 'Location' } },
182             MDItemStreamable => { },
183             MDItemTimestamp => { Groups => { 2 => 'Time' } }, # (time only)
184             MDItemTitle => { },
185             MDItemTotalBitRate => { },
186             MDItemUseCount => { },
187             MDItemUsedDates => { Groups => { 2 => 'Time' }, %mdDateInfo },
188             MDItemUserDownloadedDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
189             MDItemUserDownloadedUserHandle=> { },
190             MDItemUserSharedReceivedDate => { },
191             MDItemUserSharedReceivedRecipient => { },
192             MDItemUserSharedReceivedRecipientHandle => { },
193             MDItemUserSharedReceivedSender=> { },
194             MDItemUserSharedReceivedSenderHandle => { },
195             MDItemUserSharedReceivedTransport => { },
196             MDItemUserTags => {
197             List => 1,
198             Writable => 1,
199             WritePseudo => 1,
200             Protected => 1, # (all writable pseudo tags must be protected)
201             Notes => q{
202             requires "tag" utility for writing -- install with "brew install tag". Note
203             that user tags may not contain a comma, and that duplicate user tags will
204             not be written
205             },
206             },
207             MDItemVersion => { },
208             MDItemVideoBitRate => { Groups => { 2 => 'Video' } },
209             MDItemWhereFroms => { },
210             MDItemWhiteBalance => { Groups => { 2 => 'Image' } },
211             # tags used by Apple Mail on .emlx files
212             com_apple_mail_dateReceived => { Name => 'AppleMailDateReceived', Groups => { 2 => 'Time' }, %mdDateInfo },
213             com_apple_mail_dateSent => { Name => 'AppleMailDateSent', Groups => { 2 => 'Time' }, %mdDateInfo },
214             com_apple_mail_flagged => { Name => 'AppleMailFlagged' },
215             com_apple_mail_messageID => { Name => 'AppleMailMessageID' },
216             com_apple_mail_priority => { Name => 'AppleMailPriority' },
217             com_apple_mail_read => { Name => 'AppleMailRead' },
218             com_apple_mail_repliedTo => { Name => 'AppleMailRepliedTo' },
219             com_apple_mail_isRemoteAttachment => { Name => 'AppleMailIsRemoteAttachment' },
220             MDItemAccountHandles => { },
221             MDItemAccountIdentifier => { },
222             MDItemAuthorEmailAddresses => { },
223             MDItemBundleIdentifier => { },
224             MDItemContentCreationDate_Ranking=>{Groups=> { 2 => 'Time' }, %mdDateInfo },
225             MDItemDateAdded_Ranking => { Groups => { 2 => 'Time' }, %mdDateInfo },
226             MDItemEmailConversationID => { },
227             MDItemIdentifier => { },
228             MDItemInterestingDate_Ranking => { Groups => { 2 => 'Time' }, %mdDateInfo },
229             MDItemIsApplicationManaged => { },
230             MDItemIsExistingThread => { },
231             MDItemIsLikelyJunk => { },
232             MDItemMailboxes => { },
233             MDItemMailDateReceived_Ranking=> { Groups => { 2 => 'Time' }, %mdDateInfo },
234             MDItemPrimaryRecipientEmailAddresses => { },
235             MDItemRecipients => { },
236             MDItemSubject => { },
237             );
238              
239             # "xattr" tags
240             %Image::ExifTool::MacOS::XAttr = (
241             WRITE_PROC => \&Image::ExifTool::DummyWriteProc,
242             GROUPS => { 0 => 'File', 1 => 'MacOS', 2 => 'Other' },
243             VARS => { NO_ID => 1 }, # (id's are too long)
244             NOTES => q{
245             XAttr tags are extracted using the "xattr" utility. They are extracted if
246             any "XAttr*" tag or the MacOS group is specifically requested, or by setting
247             the API L option to 1 or the API L option to 2 or higher.
248             And they are extracted by default from MacOS "._" files when reading
249             these files directly.
250             },
251             'com.apple.FinderInfo' => {
252             Name => 'XAttrFinderInfo',
253             ConvertBinary => 1,
254             # ref https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-9A581/Finder.h
255             ValueConv => q{
256             my @a = unpack('a4a4n3x10nx2N', $$val);
257             tr/\0//d, $_="'${_}'" foreach @a[0,1];
258             return "@a";
259             },
260             PrintConv => q{
261             $val =~ s/^('.*?') ('.*?') //s or return $val;
262             my ($type, $creator) = ($1, $2);
263             my ($flags, $y, $x, $exFlags, $putAway) = split ' ', $val;
264             my $label = ($flags >> 1) & 0x07;
265             my $flags = DecodeBits((($exFlags<<16) | $flags) & 0xfff1, {
266             0 => 'OnDesk',
267             6 => 'Shared',
268             7 => 'HasNoInits',
269             8 => 'Inited',
270             10 => 'CustomIcon',
271             11 => 'Stationery',
272             12 => 'NameLocked',
273             13 => 'HasBundle',
274             14 => 'Invisible',
275             15 => 'Alias',
276             # extended flags
277             22 => 'HasRoutingInfo',
278             23 => 'ObjectBusy',
279             24 => 'CustomBadge',
280             31 => 'ExtendedFlagsValid',
281             });
282             my $str = "Type=$type Creator=$creator Flags=$flags Label=$label Pos=($x,$y)";
283             $str .= " Putaway=$putAway" if $putAway;
284             return $str;
285             },
286             },
287             'com.apple.quarantine' => {
288             Name => 'XAttrQuarantine',
289             Writable => 1,
290             WritePseudo => 1,
291             WriteCheck => '"May only delete this tag"',
292             Protected => 1,
293             Notes => q{
294             quarantine information for files downloaded from the internet. May only be
295             deleted when writing
296             },
297             # ($a[1] is the time when the quarantine tag was set)
298             PrintConv => q{
299             my @a = split /;/, $val;
300             $a[0] = 'Flags=' . $a[0];
301             $a[1] = 'set at ' . ConvertUnixTime(hex $a[1]);
302             $a[2] = 'by ' . $a[2];
303             return join ' ', @a;
304             },
305             PrintConvInv => '$val',
306             },
307             'com.apple.metadata:com_apple_mail_dateReceived' => {
308             Name => 'XAttrAppleMailDateReceived',
309             Groups => { 2 => 'Time' },
310             },
311             'com.apple.metadata:com_apple_mail_dateSent' => {
312             Name => 'XAttrAppleMailDateSent',
313             Groups => { 2 => 'Time' },
314             },
315             'com.apple.metadata:com_apple_mail_isRemoteAttachment' => {
316             Name => 'XAttrAppleMailIsRemoteAttachment',
317             },
318             'com.apple.metadata:kMDItemDownloadedDate' => {
319             Name => 'XAttrMDItemDownloadedDate',
320             Groups => { 2 => 'Time' },
321             },
322             'com.apple.metadata:kMDItemFinderComment' => { Name => 'XAttrMDItemFinderComment' },
323             'com.apple.metadata:kMDItemWhereFroms' => { Name => 'XAttrMDItemWhereFroms' },
324             'com.apple.metadata:kMDLabel' => { Name => 'XAttrMDLabel', Binary => 1 },
325             'com.apple.ResourceFork' => { Name => 'XAttrResourceFork', Binary => 1 },
326             'com.apple.lastuseddate#PS' => {
327             Name => 'XAttrLastUsedDate',
328             Groups => { 2 => 'Time' },
329             # (first 4 bytes are date/time. Not sure what remaining 12 bytes are for)
330             RawConv => 'ConvertUnixTime(unpack("V",$$val))',
331             PrintConv => '$self->ConvertDateTime($val)',
332             },
333             );
334              
335             #------------------------------------------------------------------------------
336             # Convert OS MDItem time string to standard EXIF-formatted local time
337             # Inputs: 0) time string (eg. "2017-02-21 17:21:43 +0000")
338             # Returns: EXIF-formatted local time string with timezone
339             sub MDItemLocalTime($)
340             {
341 0     0 0 0 my $val = shift;
342 0         0 $val =~ tr/-/:/;
343 0         0 $val =~ s/ ?([-+]\d{2}):?(\d{2})/$1:$2/;
344             # convert from UTC to local time
345 0 0       0 if ($val =~ /\+00:00$/) {
346 0         0 my $time = Image::ExifTool::GetUnixTime($val);
347 0 0       0 $val = Image::ExifTool::ConvertUnixTime($time, 1) if $time;
348             }
349 0         0 return $val;
350             }
351              
352             #------------------------------------------------------------------------------
353             # Set MacOS MDItem and XAttr tags from new tag values
354             # Inputs: 0) ExifTool ref, 1) file name, 2) list of tags to set
355             # Returns: 1=something was set OK, 0=didn't try, -1=error (and warning set)
356             # Notes: There may be errors even if 1 is returned
357             sub SetMacOSTags($$$)
358             {
359 0     0 0 0 my ($et, $file, $setTags) = @_;
360 0         0 my $result = 0;
361 0         0 my $tag;
362              
363 0         0 foreach $tag (@$setTags) {
364 0         0 my ($nvHash, $f, $v, $attr, $cmd, $err, $silentErr);
365 0         0 my $val = $et->GetNewValue($tag, \$nvHash);
366 0 0       0 next unless $nvHash;
367 0         0 my $overwrite = $et->IsOverwriting($nvHash);
368 0 0       0 unless ($$nvHash{TagInfo}{List}) {
369 0 0       0 next unless $overwrite;
370 0 0       0 if ($overwrite < 0) {
371 0 0       0 my $operation = $$nvHash{Shift} ? 'Shifting' : 'Conditional replacement';
372 0         0 $et->Warn("$operation of MacOS $tag not yet supported");
373 0         0 next;
374             }
375             }
376 0 0 0     0 if ($tag eq 'MDItemFSCreationDate' or $tag eq 'FileCreateDate') {
    0          
    0          
377 0         0 ($f = $file) =~ s/'/'\\''/g;
378             # convert to local time if value has a time zone
379 0 0       0 if ($val =~ /[-+Z]/) {
380 0         0 my $time = Image::ExifTool::GetUnixTime($val, 1);
381 0 0       0 $val = Image::ExifTool::ConvertUnixTime($time, 1) if $time;
382             }
383 0         0 $val =~ s{(\d{4}):(\d{2}):(\d{2})}{$2/$3/$1}; # reformat for setfile
384 0         0 $cmd = "/usr/bin/setfile -d '${val}' '${f}'";
385             } elsif ($tag eq 'MDItemUserTags') {
386             # (tested with "tag" version 0.9.0)
387 0         0 ($f = $file) =~ s/'/'\\''/g;
388 0         0 my @vals = $et->GetNewValue($nvHash);
389 0 0 0     0 if ($overwrite < 0 and @{$$nvHash{DelValue}}) {
  0         0  
390             # delete specified tags
391 0         0 my @dels = @{$$nvHash{DelValue}};
  0         0  
392 0         0 s/'/'\\''/g foreach @dels;
393 0         0 my $del = join ',', @dels;
394 0         0 $err = system "/usr/local/bin/tag -r '${del}' '${f}'>/dev/null 2>&1";
395 0 0       0 unless ($err) {
396 0         0 $et->VerboseValue("- $tag", $del);
397 0         0 $result = 1;
398 0 0       0 undef $err if @vals; # more to do if there are tags to add
399             }
400             }
401 0 0       0 unless (defined $err) {
402             # add new tags, or overwrite or delete existing tags
403 0         0 s/'/'\\''/g foreach @vals;
404 0 0       0 my $opt = $overwrite > 0 ? '-s' : '-a';
405 0 0       0 $val = @vals ? join(',', @vals) : '';
406 0         0 $cmd = "/usr/local/bin/tag $opt '${val}' '${f}'";
407 0 0       0 $et->VPrint(1," - $tag = (all)\n") if $overwrite > 0;
408 0 0       0 undef $val if $val eq '';
409             }
410             } elsif ($tag eq 'XAttrQuarantine') {
411 0         0 ($f = $file) =~ s/'/'\\''/g;
412 0         0 $cmd = "/usr/bin/xattr -d com.apple.quarantine '${f}'";
413 0         0 $silentErr = 256; # (will get this error if attribute doesn't exist)
414             } else {
415 0         0 ($f = $file) =~ s/(["\\])/\\$1/g; # escape necessary characters for script
416 0         0 $f =~ s/'/'"'"'/g;
417 0 0       0 if ($tag eq 'MDItemFinderComment') {
418             # (write finder comment using osascript instead of xattr
419             # because it is more work to construct the necessary bplist)
420 0 0       0 $val = '' unless defined $val; # set to empty string instead of deleting
421 0         0 $v = $et->Encode($val, 'UTF8');
422 0         0 $v =~ s/(["\\])/\\$1/g;
423 0         0 $v =~ s/'/'"'"'/g;
424 0         0 $attr = 'comment';
425             } else { # $tag eq 'MDItemFSLabel'
426 0 0       0 $v = $val ? 8 - $val : 0; # convert from label to label index (0 for no label)
427 0         0 $attr = 'label index';
428             }
429 0         0 $cmd = qq(/usr/bin/osascript -e 'set fp to POSIX file "$f" as alias' -e \\
430             'tell application "Finder" to set $attr of file fp to "$v"');
431             }
432 0 0       0 if (defined $cmd) {
433 0         0 $err = system $cmd . '>/dev/null 2>&1'; # (pipe all output to /dev/null)
434             }
435 0 0 0     0 if (not $err) {
    0          
436 0 0       0 $et->VerboseValue("+ $tag", $val) if defined $val;
437 0         0 $result = 1;
438             } elsif (not $silentErr or $err != $silentErr) {
439 0         0 $cmd =~ s/ .*//s;
440 0         0 $et->Warn(qq{Error $err running "$cmd" to set $tag});
441 0 0       0 $result = -1 unless $result;
442             }
443             }
444 0         0 return $result;
445             }
446              
447             #------------------------------------------------------------------------------
448             # Extract MacOS metadata item tags
449             # Inputs: 0) ExifTool object ref, 1) file name
450             sub ExtractMDItemTags($$)
451             {
452 0     0 0 0 local $_;
453 0         0 my ($et, $file) = @_;
454 0         0 my ($fn, $tag, $val, $tmp);
455              
456 0         0 ($fn = $file) =~ s/([`"\$\\])/\\$1/g; # escape necessary characters
457 0         0 $et->VPrint(0, '(running mdls)');
458 0         0 my @mdls = `/usr/bin/mdls "$fn" 2> /dev/null`; # get MacOS metadata
459 0 0 0     0 if ($? or not @mdls) {
460 0         0 $et->Warn('Error running "mdls" to extract MDItem tags');
461 0         0 return;
462             }
463 0         0 my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::MDItem');
464 0         0 $$et{INDENT} .= '| ';
465 0         0 $et->VerboseDir('MDItem');
466 0         0 foreach (@mdls) {
467 0         0 chomp;
468 0 0       0 if (ref $val ne 'ARRAY') {
    0          
469 0 0       0 s/^k?(\w+)\s*= // or next;
470 0         0 $tag = $1;
471 0 0       0 $_ eq '(' and $val = [ ], next; # (start of a list)
472 0 0       0 $_ = '' if $_ eq '(null)';
473 0 0       0 s/^"// and s/"$//; # remove quotes if they exist
474 0         0 $val = $_;
475             } elsif ($_ eq ')') { # (end of a list)
476 0         0 $_ = $$val[0];
477 0 0       0 next unless defined $_;
478             } else {
479             # add item to list
480 0         0 s/^ //; # remove leading spaces
481 0         0 s/,$//; # remove trailing comma
482 0 0       0 $_ = '' if $_ eq '(null)';
483 0 0       0 s/^"// and s/"$//; # remove quotes if they exist
484 0         0 s/\\"/"/g; # un-escape quotes
485 0         0 $_ = $et->Decode($_, 'UTF8');
486 0         0 push @$val, $_;
487 0         0 next;
488             }
489             # add to Extra tags if not done already
490 0 0       0 unless ($$tagTablePtr{$tag}) {
491             # check for a date/time format
492 0         0 my %tagInfo;
493 0 0       0 %tagInfo = (
494             Groups => { 2 => 'Time' },
495             ValueConv => \&MDItemLocalTime,
496             PrintConv => '$self->ConvertDateTime($val)',
497             ) if /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/;
498             # change tags like "com_apple_mail_xxx" to "AppleMailXxx"
499 0         0 ($tmp = $tag) =~ s/^com_//; # remove leading "com_"
500 0         0 $tmp =~ s/_([a-z])/\u$1/g; # use CamelCase
501 0         0 $tagInfo{Name} = Image::ExifTool::MakeTagName($tmp);
502 0 0       0 $tagInfo{List} = 1 if ref $val eq 'ARRAY';
503 0 0       0 $tagInfo{Groups}{2} = 'Audio' if $tag =~ /Audio/;
504 0 0       0 $tagInfo{Groups}{2} = 'Author' if $tag =~ /(Copyright|Author)/;
505 0         0 $et->VPrint(0, " [adding $tag]\n");
506 0         0 AddTagToTable($tagTablePtr, $tag, \%tagInfo);
507             }
508 0 0       0 $val = $et->Decode($val, 'UTF8') unless ref $val;
509 0         0 $et->HandleTag($tagTablePtr, $tag, $val);
510 0         0 undef $val;
511             }
512 0         0 $$et{INDENT} =~ s/\| $//;
513             }
514              
515            
516             #------------------------------------------------------------------------------
517             # Read MacOS XAttr value
518             # Inputs: 0) ExifTool object ref, 1) file name
519             sub ReadXAttrValue($$$$)
520             {
521 8     8 0 34 my ($et, $tagTablePtr, $tag, $val) = @_;
522             # add to our table if necessary
523 8 100       27 unless ($$tagTablePtr{$tag}) {
524 2         4 my $name;
525             # generate tag name from attribute name
526 2 100       11 if ($tag =~ /^com\.apple\.(.*)$/) {
527 1         9 ($name = $1) =~ s/^metadata:_?k//;
528 1         3 $name =~ s/^metadata:(com_)?//;
529             } else {
530 1         6 $name = $tag;
531             }
532 2         18 $name =~ s/[.:_]([a-z])/\U$1/g;
533 2         9 $name = 'XAttr' . ucfirst $name;
534 2         9 my %tagInfo = ( Name => $name );
535 2 50       8 $tagInfo{Groups} = { 2 => 'Time' } if $tag=~/Date$/;
536 2         14 $et->VPrint(0, " [adding $tag]\n");
537 2         9 AddTagToTable($tagTablePtr, $tag, \%tagInfo);
538             }
539 8 100       28 if ($val =~ /^bplist0/) {
540 4         18 my %dirInfo = ( DataPt => \$val );
541 4         648 require Image::ExifTool::PLIST;
542 4 50       21 if (Image::ExifTool::PLIST::ProcessBinaryPLIST($et, \%dirInfo, $tagTablePtr)) {
543 4 50       14 return undef if ref $dirInfo{Value} eq 'HASH';
544             $val = $dirInfo{Value}
545 4         10 } else {
546 0         0 $et->Warn("Error decoding $$tagTablePtr{$tag}{Name}");
547 0         0 return undef;
548             }
549             }
550 8 50 66     76 if (not ref $val and ($val =~ /\0/ or length($val) > 200) or $tag eq 'XAttrMDLabel') {
      66        
      66        
551 1         9 my $buff = $val;
552 1         4 $val = \$buff;
553             }
554 8         23 return $val;
555             }
556              
557             #------------------------------------------------------------------------------
558             # Read MacOS extended attribute tags using 'xattr' utility
559             # Inputs: 0) ExifTool object ref, 1) file name
560             sub ExtractXAttrTags($$)
561             {
562 0     0 0 0 local $_;
563 0         0 my ($et, $file) = @_;
564 0         0 my ($fn, $tag, $val, $warn);
565              
566 0         0 ($fn = $file) =~ s/([`"\$\\])/\\$1/g; # escape necessary characters
567 0         0 $et->VPrint(0, '(running xattr)');
568 0         0 my @xattr = `/usr/bin/xattr -lx "$fn" 2> /dev/null`; # get MacOS extended attributes
569 0 0 0     0 if ($? or not @xattr) {
570 0 0       0 $? and $et->Warn('Error running "xattr" to extract XAttr tags');
571 0         0 return;
572             }
573 0         0 my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::XAttr');
574 0         0 $$et{INDENT} .= '| ';
575 0         0 $et->VerboseDir('XAttr');
576 0         0 push @xattr, ''; # (for a list terminator)
577 0         0 foreach (@xattr) {
578 0         0 chomp;
579 0 0 0     0 if (s/^[\dA-Fa-f]{8}//) {
    0          
580 0 0       0 $tag or $warn = 1, next;
581 0         0 s/\|.*//;
582 0         0 tr/ //d;
583 0 0 0     0 (/[^\dA-Fa-f]/ or length($_) & 1) and $warn = 2, next;
584 0 0       0 $val = '' unless defined $val;
585 0         0 $val .= pack('H*', $_);
586 0         0 next;
587             } elsif ($tag and defined $val) {
588 0         0 $val = ReadXAttrValue($et, $tagTablePtr, $tag, $val);
589 0 0       0 $et->HandleTag($tagTablePtr, $tag, $val) if defined $val;
590 0         0 undef $tag;
591 0         0 undef $val;
592             }
593 0 0       0 next unless length;
594 0 0       0 s/:$// or $warn = 3, next; # attribute name must have trailing ":"
595 0 0       0 defined $val and $warn = 4, undef $val;
596             # remove random ID after kMDLabel in tag ID
597 0         0 ($tag = $_) =~ s/^com.apple.metadata:kMDLabel_.*/com.apple.metadata:kMDLabel/s;
598             }
599 0 0       0 $warn and $et->Warn(qq{Error $warn parsing "xattr" output});
600 0         0 $$et{INDENT} =~ s/\| $//;
601             }
602              
603             #------------------------------------------------------------------------------
604             # Extract MacOS file creation date/time
605             # Inputs: 0) ExifTool object ref, 1) file name
606             sub GetFileCreateDate($$)
607             {
608 0     0 0 0 local $_;
609 0         0 my ($et, $file) = @_;
610 0         0 my ($fn, $tag, $val, $tmp);
611              
612 0         0 ($fn = $file) =~ s/([`"\$\\])/\\$1/g; # escape necessary characters
613 0         0 $et->VPrint(0, '(running stat)');
614 0         0 my $time = `/usr/bin/stat -f '%SB' -t '%Y:%m:%d %H:%M:%S%z' "$fn" 2> /dev/null`;
615 0 0 0     0 if ($? or not $time or $time !~ s/([-+]\d{2})(\d{2})\s*$/$1:$2/) {
      0        
616 0         0 $et->Warn('Error running "stat" to extract FileCreateDate');
617 0         0 return;
618             }
619 0         0 $$et{SET_GROUP1} = 'MacOS';
620 0         0 $et->FoundTag(FileCreateDate => $time);
621 0         0 delete $$et{SET_GROUP1};
622             }
623              
624             #------------------------------------------------------------------------------
625             # Read ATTR metadata from "._" file
626             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
627             # Return: 1 on success
628             # (ref https://www.swiftforensics.com/2018/11/the-dot-underscore-file-format.html)
629             sub ProcessATTR($$$)
630             {
631 1     1 0 4 my ($et, $dirInfo, $tagTablePtr) = @_;
632 1         2 my $dataPt = $$dirInfo{DataPt};
633 1         2 my $dataPos = $$dirInfo{DataPos};
634 1         4 my $dataLen = length $$dataPt;
635              
636 1 50 33     12 $dataLen >= 58 and $$dataPt =~ /^.{34}ATTR/s or $et->Warn('Invalid ATTR header'), return 0;
637 1         4 my $entries = Get32u($dataPt, 66);
638 1         8 $et->VerboseDir('ATTR', $entries);
639             # (Note: The RAF is not in $dirInfo because it would break RSRC reading --
640             # the RSCR block uses relative offsets, while the ATTR block uses absolute! grrr!)
641 1         3 my $raf = $$et{RAF};
642 1         2 my $pos = 70; # first entry is after ATTR header
643 1         3 my $i;
644 1         4 for ($i=0; $i<$entries; ++$i) {
645 8 50       18 $pos + 12 > $dataLen and $et->Warn('Truncated ATTR entry'), last;
646 8         21 my $off = Get32u($dataPt, $pos);
647 8         22 my $len = Get32u($dataPt, $pos + 4);
648 8         29 my $n = Get8u($dataPt, $pos + 10); # number of characters in tag name
649 8 50       27 $pos + 11 + $n > $dataLen and $et->Warn('Truncated ATTR name'), last;
650 8         15 $off -= $dataPos; # convert to relative offset (grrr!)
651 8 50 33     20 $off < 0 or $off > $dataLen and $et->Warn('Invalid ATTR offset'), last;
652 8         20 my $tag = substr($$dataPt, $pos + 11, $n);
653 8         53 $tag =~ s/\0+$//; # remove null terminator
654             # remove random ID after kMDLabel in tag ID
655 8         21 $tag =~ s/^com.apple.metadata:kMDLabel_.*/com.apple.metadata:kMDLabel/s;
656 8 50       19 $off + $len > $dataLen and $et->Warn('Truncated ATTR value'), last;
657 8         29 my $val = ReadXAttrValue($et, $tagTablePtr, $tag, substr($$dataPt, $off, $len));
658 8 50       49 $et->HandleTag($tagTablePtr, $tag, $val,
659             DataPt => $dataPt,
660             DataPos => $dataPos,
661             Start => $off,
662             Size => $len,
663             ) if defined $val;
664 8         34 $pos += (11 + $n + 3) & -4; # step to next entry (on even 4-byte boundary)
665             }
666 1         14 return 1;
667             }
668              
669             #------------------------------------------------------------------------------
670             # Read information from a MacOS "._" sidecar file
671             # Inputs: 0) ExifTool ref, 1) dirInfo ref
672             # Returns: 1 on success, 0 if this wasn't a valid "._" file
673             # (ref https://www.swiftforensics.com/2018/11/the-dot-underscore-file-format.html)
674             sub ProcessMacOS($$)
675             {
676 1     1 0 4 my ($et, $dirInfo) = @_;
677 1         4 my $raf = $$dirInfo{RAF};
678 1         4 my ($hdr, $buff, $i);
679              
680 1 50 33     21 return 0 unless $raf->Read($hdr, 26) == 26 and $hdr =~ /^\0\x05\x16\x07\0(.)\0\0Mac OS X /s;
681 1         6 my $ver = ord $1;
682             # (extension may be anything, so just echo back the incoming file extension if it exists)
683 1         9 $et->SetFileType(undef, undef, $$et{FILE_EXT});
684 1 50       3 $ver == 2 or $et->Warn("Unsupported file version $ver"), return 1;
685 1         6 SetByteOrder('MM');
686 1         3 my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::Main');
687 1         6 my $entries = Get16u(\$hdr, 0x18);
688 1         7 $et->VerboseDir('MacOS', $entries);
689 1 50       6 $raf->Read($hdr, $entries * 12) == $entries * 12 or $et->Warn('Truncated header'), return 1;
690 1         6 for ($i=0; $i<$entries; ++$i) {
691 2         7 my $pos = $i * 12;
692 2         8 my $tag = Get32u(\$hdr, $pos);
693 2         8 my $off = Get32u(\$hdr, $pos + 4);
694 2         9 my $len = Get32u(\$hdr, $pos + 8);
695 2 50       9 $len > 100000000 and $et->Warn('Record size too large'), last;
696 2 50 33     7 $raf->Seek($off,0) and $raf->Read($buff,$len) == $len or $et->Warn('Truncated record'), last;
697 2         15 $et->HandleTag($tagTablePtr, $tag, undef, DataPt => \$buff, DataPos => $off, Index => $i);
698             }
699 1         5 return 1;
700             }
701              
702             1; # end
703              
704             __END__