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 | use strict;
|
---|
12 | use vars qw($VERSION);
|
---|
13 | use Image::ExifTool qw(:DataAccess :Utils);
|
---|
14 |
|
---|
15 | $VERSION = '1.11';
|
---|
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 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
|
---|
339 | sub 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
|
---|
357 | sub 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
|
---|
450 | sub 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
|
---|
519 | sub 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
|
---|
560 | sub 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
|
---|
606 | sub 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)
|
---|
629 | sub 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)
|
---|
674 | sub 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 |
|
---|
702 | 1; # end
|
---|
703 |
|
---|
704 | __END__
|
---|
705 |
|
---|
706 | =head1 NAME
|
---|
707 |
|
---|
708 | Image::ExifTool::MacOS - Read/write MacOS system tags
|
---|
709 |
|
---|
710 | =head1 SYNOPSIS
|
---|
711 |
|
---|
712 | This module is used by Image::ExifTool
|
---|
713 |
|
---|
714 | =head1 DESCRIPTION
|
---|
715 |
|
---|
716 | This module contains definitions required by Image::ExifTool to extract
|
---|
717 | MDItem* and XAttr* tags on MacOS systems using the "mdls" and "xattr"
|
---|
718 | utilities respectively. It also reads metadata directly from the MacOS "_."
|
---|
719 | sidecar files that are used on some filesystems to store file attributes.
|
---|
720 | Writable tags use "xattr", "setfile" or "osascript" for writing.
|
---|
721 |
|
---|
722 | =head1 AUTHOR
|
---|
723 |
|
---|
724 | Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com)
|
---|
725 |
|
---|
726 | This library is free software; you can redistribute it and/or modify it
|
---|
727 | under the same terms as Perl itself.
|
---|
728 |
|
---|
729 | =head1 SEE ALSO
|
---|
730 |
|
---|
731 | L<Image::ExifTool::TagNames/MacOS Tags>,
|
---|
732 | L<Image::ExifTool(3pm)|Image::ExifTool>
|
---|
733 |
|
---|
734 | =cut
|
---|
735 |
|
---|