source: main/trunk/greenstone2/perllib/cpan/Image/ExifTool/MacOS.pm@ 34921

Last change on this file since 34921 was 34921, checked in by anupama, 3 years ago

Committing the improvements to EmbeddedMetaPlugin's processing of Keywords vs other metadata fields. Keywords were literally stored as arrays of words rather than phrases in PDFs (at least in Diego's sample PDF), whereas other meta fields like Subjects and Creators stored them as arrays of phrases. To get both to work, Kathy updated EXIF to a newer version, to retrieve the actual EXIF values stored in the PDF. And Kathy and Dr Bainbridge came up with a new option that I added called apply_join_before_split_to_metafields that's a regex which can list the metadata fields to apply the join_before_split to and whcih previously always got applied to all metadata fields. Now it's applied to any *Keywords metafields by default, as that's the metafield we have experience of that behaves differently to the others, as it stores by word instead of phrases. Tested on Diego's sample PDF. Diego has double-checked it to works on his sample PDF too, setting the split char to ; and turning on the join_before_split and leaving apply_join_before_split_to_metafields at its default of .*Keywords. File changes are strings.properties for the tooltip, the plugin introducing the option and working with it and Kathy's EXIF updates affecting cpan/File and cpan/Image.

File size: 30.7 KB
Line 
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
10package Image::ExifTool::MacOS;
11use strict;
12use vars qw($VERSION);
13use Image::ExifTool qw(:DataAccess :Utils);
14
15$VERSION = '1.11';
16
17sub MDItemLocalTime($);
18sub ProcessATTR($$$);
19
20my %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 L<MDItemTags|../ExifTool.html#MDItemTags> API option to 1 or the L<RequestAll|../ExifTool.html#RequestAll> API 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<FileCreateDate|Extra.html> 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 L<XAttrTags|../ExifTool.html#XAttrTags> API option to 1 or the L<RequestAll|../ExifTool.html#RequestAll> API option to 2 or higher.
248 And they 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
339sub MDItemLocalTime($)
340{
341 my $val = shift;
342 $val =~ tr/-/:/;
343 $val =~ s/ ?([-+]\d{2}):?(\d{2})/$1:$2/;
344 # convert from UTC to local time
345 if ($val =~ /\+00:00$/) {
346 my $time = Image::ExifTool::GetUnixTime($val);
347 $val = Image::ExifTool::ConvertUnixTime($time, 1) if $time;
348 }
349 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
357sub SetMacOSTags($$$)
358{
359 my ($et, $file, $setTags) = @_;
360 my $result = 0;
361 my $tag;
362
363 foreach $tag (@$setTags) {
364 my ($nvHash, $f, $v, $attr, $cmd, $err, $silentErr);
365 my $val = $et->GetNewValue($tag, \$nvHash);
366 next unless $nvHash;
367 my $overwrite = $et->IsOverwriting($nvHash);
368 unless ($$nvHash{TagInfo}{List}) {
369 next unless $overwrite;
370 if ($overwrite < 0) {
371 my $operation = $$nvHash{Shift} ? 'Shifting' : 'Conditional replacement';
372 $et->Warn("$operation of MacOS $tag not yet supported");
373 next;
374 }
375 }
376 if ($tag eq 'MDItemFSCreationDate' or $tag eq 'FileCreateDate') {
377 ($f = $file) =~ s/'/'\\''/g;
378 # convert to local time if value has a time zone
379 if ($val =~ /[-+Z]/) {
380 my $time = Image::ExifTool::GetUnixTime($val, 1);
381 $val = Image::ExifTool::ConvertUnixTime($time, 1) if $time;
382 }
383 $val =~ s{(\d{4}):(\d{2}):(\d{2})}{$2/$3/$1}; # reformat for setfile
384 $cmd = "setfile -d '${val}' '${f}'";
385 } elsif ($tag eq 'MDItemUserTags') {
386 # (tested with "tag" version 0.9.0)
387 ($f = $file) =~ s/'/'\\''/g;
388 my @vals = $et->GetNewValue($nvHash);
389 if ($overwrite < 0 and @{$$nvHash{DelValue}}) {
390 # delete specified tags
391 my @dels = @{$$nvHash{DelValue}};
392 s/'/'\\''/g foreach @dels;
393 my $del = join ',', @dels;
394 $err = system "tag -r '${del}' '${f}'>/dev/null 2>&1";
395 unless ($err) {
396 $et->VerboseValue("- $tag", $del);
397 $result = 1;
398 undef $err if @vals; # more to do if there are tags to add
399 }
400 }
401 unless (defined $err) {
402 # add new tags, or overwrite or delete existing tags
403 s/'/'\\''/g foreach @vals;
404 my $opt = $overwrite > 0 ? '-s' : '-a';
405 $val = @vals ? join(',', @vals) : '';
406 $cmd = "tag $opt '${val}' '${f}'";
407 $et->VPrint(1," - $tag = (all)\n") if $overwrite > 0;
408 undef $val if $val eq '';
409 }
410 } elsif ($tag eq 'XAttrQuarantine') {
411 ($f = $file) =~ s/'/'\\''/g;
412 $cmd = "xattr -d com.apple.quarantine '${f}'";
413 $silentErr = 256; # (will get this error if attribute doesn't exist)
414 } else {
415 ($f = $file) =~ s/(["\\])/\\$1/g; # escape necessary characters for script
416 $f =~ s/'/'"'"'/g;
417 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 $val = '' unless defined $val; # set to empty string instead of deleting
421 $v = $et->Encode($val, 'UTF8');
422 $v =~ s/(["\\])/\\$1/g;
423 $v =~ s/'/'"'"'/g;
424 $attr = 'comment';
425 } else { # $tag eq 'MDItemFSLabel'
426 $v = $val ? 8 - $val : 0; # convert from label to label index (0 for no label)
427 $attr = 'label index';
428 }
429 $cmd = qq(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 if (defined $cmd) {
433 $err = system $cmd . '>/dev/null 2>&1'; # (pipe all output to /dev/null)
434 }
435 if (not $err) {
436 $et->VerboseValue("+ $tag", $val) if defined $val;
437 $result = 1;
438 } elsif (not $silentErr or $err != $silentErr) {
439 $cmd =~ s/ .*//s;
440 $et->Warn(qq{Error $err running "$cmd" to set $tag});
441 $result = -1 unless $result;
442 }
443 }
444 return $result;
445}
446
447#------------------------------------------------------------------------------
448# Extract MacOS metadata item tags
449# Inputs: 0) ExifTool object ref, 1) file name
450sub ExtractMDItemTags($$)
451{
452 local $_;
453 my ($et, $file) = @_;
454 my ($fn, $tag, $val, $tmp);
455
456 ($fn = $file) =~ s/([`"\$\\])/\\$1/g; # escape necessary characters
457 $et->VPrint(0, '(running mdls)');
458 my @mdls = `mdls "$fn" 2> /dev/null`; # get MacOS metadata
459 if ($? or not @mdls) {
460 $et->Warn('Error running "mdls" to extract MDItem tags');
461 return;
462 }
463 my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::MDItem');
464 $$et{INDENT} .= '| ';
465 $et->VerboseDir('MDItem');
466 foreach (@mdls) {
467 chomp;
468 if (ref $val ne 'ARRAY') {
469 s/^k?(\w+)\s*= // or next;
470 $tag = $1;
471 $_ eq '(' and $val = [ ], next; # (start of a list)
472 $_ = '' if $_ eq '(null)';
473 s/^"// and s/"$//; # remove quotes if they exist
474 $val = $_;
475 } elsif ($_ eq ')') { # (end of a list)
476 $_ = $$val[0];
477 next unless defined $_;
478 } else {
479 # add item to list
480 s/^ //; # remove leading spaces
481 s/,$//; # remove trailing comma
482 $_ = '' if $_ eq '(null)';
483 s/^"// and s/"$//; # remove quotes if they exist
484 s/\\"/"/g; # un-escape quotes
485 $_ = $et->Decode($_, 'UTF8');
486 push @$val, $_;
487 next;
488 }
489 # add to Extra tags if not done already
490 unless ($$tagTablePtr{$tag}) {
491 # check for a date/time format
492 my %tagInfo;
493 %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 ($tmp = $tag) =~ s/^com_//; # remove leading "com_"
500 $tmp =~ s/_([a-z])/\u$1/g; # use CamelCase
501 $tagInfo{Name} = Image::ExifTool::MakeTagName($tmp);
502 $tagInfo{List} = 1 if ref $val eq 'ARRAY';
503 $tagInfo{Groups}{2} = 'Audio' if $tag =~ /Audio/;
504 $tagInfo{Groups}{2} = 'Author' if $tag =~ /(Copyright|Author)/;
505 $et->VPrint(0, " [adding $tag]\n");
506 AddTagToTable($tagTablePtr, $tag, \%tagInfo);
507 }
508 $val = $et->Decode($val, 'UTF8') unless ref $val;
509 $et->HandleTag($tagTablePtr, $tag, $val);
510 undef $val;
511 }
512 $$et{INDENT} =~ s/\| $//;
513}
514
515
516#------------------------------------------------------------------------------
517# Read MacOS XAttr value
518# Inputs: 0) ExifTool object ref, 1) file name
519sub ReadXAttrValue($$$$)
520{
521 my ($et, $tagTablePtr, $tag, $val) = @_;
522 # add to our table if necessary
523 unless ($$tagTablePtr{$tag}) {
524 my $name;
525 # generate tag name from attribute name
526 if ($tag =~ /^com\.apple\.(.*)$/) {
527 ($name = $1) =~ s/^metadata:_?k//;
528 $name =~ s/^metadata:(com_)?//;
529 } else {
530 $name = $tag;
531 }
532 $name =~ s/[.:_]([a-z])/\U$1/g;
533 $name = 'XAttr' . ucfirst $name;
534 my %tagInfo = ( Name => $name );
535 $tagInfo{Groups} = { 2 => 'Time' } if $tag=~/Date$/;
536 $et->VPrint(0, " [adding $tag]\n");
537 AddTagToTable($tagTablePtr, $tag, \%tagInfo);
538 }
539 if ($val =~ /^bplist0/) {
540 my %dirInfo = ( DataPt => \$val );
541 require Image::ExifTool::PLIST;
542 if (Image::ExifTool::PLIST::ProcessBinaryPLIST($et, \%dirInfo, $tagTablePtr)) {
543 return undef if ref $dirInfo{Value} eq 'HASH';
544 $val = $dirInfo{Value}
545 } else {
546 $et->Warn("Error decoding $$tagTablePtr{$tag}{Name}");
547 return undef;
548 }
549 }
550 if (not ref $val and ($val =~ /\0/ or length($val) > 200) or $tag eq 'XAttrMDLabel') {
551 my $buff = $val;
552 $val = \$buff;
553 }
554 return $val;
555}
556
557#------------------------------------------------------------------------------
558# Read MacOS extended attribute tags using 'xattr' utility
559# Inputs: 0) ExifTool object ref, 1) file name
560sub ExtractXAttrTags($$)
561{
562 local $_;
563 my ($et, $file) = @_;
564 my ($fn, $tag, $val, $warn);
565
566 ($fn = $file) =~ s/([`"\$\\])/\\$1/g; # escape necessary characters
567 $et->VPrint(0, '(running xattr)');
568 my @xattr = `xattr -lx "$fn" 2> /dev/null`; # get MacOS extended attributes
569 if ($? or not @xattr) {
570 $? and $et->Warn('Error running "xattr" to extract XAttr tags');
571 return;
572 }
573 my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::XAttr');
574 $$et{INDENT} .= '| ';
575 $et->VerboseDir('XAttr');
576 push @xattr, ''; # (for a list terminator)
577 foreach (@xattr) {
578 chomp;
579 if (s/^[\dA-Fa-f]{8}//) {
580 $tag or $warn = 1, next;
581 s/\|.*//;
582 tr/ //d;
583 (/[^\dA-Fa-f]/ or length($_) & 1) and $warn = 2, next;
584 $val = '' unless defined $val;
585 $val .= pack('H*', $_);
586 next;
587 } elsif ($tag and defined $val) {
588 $val = ReadXAttrValue($et, $tagTablePtr, $tag, $val);
589 $et->HandleTag($tagTablePtr, $tag, $val) if defined $val;
590 undef $tag;
591 undef $val;
592 }
593 next unless length;
594 s/:$// or $warn = 3, next; # attribute name must have trailing ":"
595 defined $val and $warn = 4, undef $val;
596 # remove random ID after kMDLabel in tag ID
597 ($tag = $_) =~ s/^com.apple.metadata:kMDLabel_.*/com.apple.metadata:kMDLabel/s;
598 }
599 $warn and $et->Warn(qq{Error $warn parsing "xattr" output});
600 $$et{INDENT} =~ s/\| $//;
601}
602
603#------------------------------------------------------------------------------
604# Extract MacOS file creation date/time
605# Inputs: 0) ExifTool object ref, 1) file name
606sub GetFileCreateDate($$)
607{
608 local $_;
609 my ($et, $file) = @_;
610 my ($fn, $tag, $val, $tmp);
611
612 ($fn = $file) =~ s/([`"\$\\])/\\$1/g; # escape necessary characters
613 $et->VPrint(0, '(running stat)');
614 my $time = `stat -f '%SB' -t '%Y:%m:%d %H:%M:%S%z' "$fn" 2> /dev/null`;
615 if ($? or not $time or $time !~ s/([-+]\d{2})(\d{2})\s*$/$1:$2/) {
616 $et->Warn('Error running "stat" to extract FileCreateDate');
617 return;
618 }
619 $$et{SET_GROUP1} = 'MacOS';
620 $et->FoundTag(FileCreateDate => $time);
621 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)
629sub ProcessATTR($$$)
630{
631 my ($et, $dirInfo, $tagTablePtr) = @_;
632 my $dataPt = $$dirInfo{DataPt};
633 my $dataPos = $$dirInfo{DataPos};
634 my $dataLen = length $$dataPt;
635
636 $dataLen >= 58 and $$dataPt =~ /^.{34}ATTR/s or $et->Warn('Invalid ATTR header'), return 0;
637 my $entries = Get32u($dataPt, 66);
638 $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 my $raf = $$et{RAF};
642 my $pos = 70; # first entry is after ATTR header
643 my $i;
644 for ($i=0; $i<$entries; ++$i) {
645 $pos + 12 > $dataLen and $et->Warn('Truncated ATTR entry'), last;
646 my $off = Get32u($dataPt, $pos);
647 my $len = Get32u($dataPt, $pos + 4);
648 my $n = Get8u($dataPt, $pos + 10); # number of characters in tag name
649 $pos + 11 + $n > $dataLen and $et->Warn('Truncated ATTR name'), last;
650 $off -= $dataPos; # convert to relative offset (grrr!)
651 $off < 0 or $off > $dataLen and $et->Warn('Invalid ATTR offset'), last;
652 my $tag = substr($$dataPt, $pos + 11, $n);
653 $tag =~ s/\0+$//; # remove null terminator
654 # remove random ID after kMDLabel in tag ID
655 $tag =~ s/^com.apple.metadata:kMDLabel_.*/com.apple.metadata:kMDLabel/s;
656 $off + $len > $dataLen and $et->Warn('Truncated ATTR value'), last;
657 my $val = ReadXAttrValue($et, $tagTablePtr, $tag, substr($$dataPt, $off, $len));
658 $et->HandleTag($tagTablePtr, $tag, $val,
659 DataPt => $dataPt,
660 DataPos => $dataPos,
661 Start => $off,
662 Size => $len,
663 ) if defined $val;
664 $pos += (11 + $n + 3) & -4; # step to next entry (on even 4-byte boundary)
665 }
666 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)
674sub ProcessMacOS($$)
675{
676 my ($et, $dirInfo) = @_;
677 my $raf = $$dirInfo{RAF};
678 my ($hdr, $buff, $i);
679
680 return 0 unless $raf->Read($hdr, 26) == 26 and $hdr =~ /^\0\x05\x16\x07\0(.)\0\0Mac OS X /s;
681 my $ver = ord $1;
682 # (extension may be anything, so just echo back the incoming file extension if it exists)
683 $et->SetFileType(undef, undef, $$et{FILE_EXT});
684 $ver == 2 or $et->Warn("Unsupported file version $ver"), return 1;
685 SetByteOrder('MM');
686 my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::Main');
687 my $entries = Get16u(\$hdr, 0x18);
688 $et->VerboseDir('MacOS', $entries);
689 $raf->Read($hdr, $entries * 12) == $entries * 12 or $et->Warn('Truncated header'), return 1;
690 for ($i=0; $i<$entries; ++$i) {
691 my $pos = $i * 12;
692 my $tag = Get32u(\$hdr, $pos);
693 my $off = Get32u(\$hdr, $pos + 4);
694 my $len = Get32u(\$hdr, $pos + 8);
695 $len > 100000000 and $et->Warn('Record size too large'), last;
696 $raf->Seek($off,0) and $raf->Read($buff,$len) == $len or $et->Warn('Truncated record'), last;
697 $et->HandleTag($tagTablePtr, $tag, undef, DataPt => \$buff, DataPos => $off, Index => $i);
698 }
699 return 1;
700}
701
7021; # end
703
704__END__
705
706=head1 NAME
707
708Image::ExifTool::MacOS - Read/write MacOS system tags
709
710=head1 SYNOPSIS
711
712This module is used by Image::ExifTool
713
714=head1 DESCRIPTION
715
716This module contains definitions required by Image::ExifTool to extract
717MDItem* and XAttr* tags on MacOS systems using the "mdls" and "xattr"
718utilities respectively. It also reads metadata directly from the MacOS "_."
719sidecar files that are used on some filesystems to store file attributes.
720Writable tags use "xattr", "setfile" or "osascript" for writing.
721
722=head1 AUTHOR
723
724Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com)
725
726This library is free software; you can redistribute it and/or modify it
727under the same terms as Perl itself.
728
729=head1 SEE ALSO
730
731L<Image::ExifTool::TagNames/MacOS Tags>,
732L<Image::ExifTool(3pm)|Image::ExifTool>
733
734=cut
735
Note: See TracBrowser for help on using the repository browser.