test_pie/external/exiftool/lib/Image/ExifTool/MacOS.pm

567 lines
24 KiB
Perl

#------------------------------------------------------------------------------
# File: MacOS.pm
#
# Description: Read/write MacOS system tags
#
# Revisions: 2017/03/01 - P. Harvey Created
#------------------------------------------------------------------------------
package Image::ExifTool::MacOS;
use strict;
use vars qw($VERSION);
use Image::ExifTool qw(:DataAccess :Utils);
$VERSION = '1.06';
sub MDItemLocalTime($);
my %mdDateInfo = (
ValueConv => \&MDItemLocalTime,
PrintConv => '$self->ConvertDateTime($val)',
);
# "mdls" tags (ref PH)
%Image::ExifTool::MacOS::MDItem = (
WRITE_PROC => \&Image::ExifTool::DummyWriteProc,
VARS => { NO_ID => 1 },
GROUPS => { 0 => 'File', 1 => 'MacOS', 2 => 'Other' },
NOTES => q{
MDItem tags are extracted using the "mdls" utility. They are extracted if
any "MDItem*" tag or the MacOS group is specifically requested, or by
setting the L<MDItemTags API option|../ExifTool.html#MDItemTags> to 1 or the L<RequestAll API option|../ExifTool.html#RequestAll> to 2 or
higher.
},
MDItemFinderComment => {
Writable => 1,
WritePseudo => 1,
Protected => 1, # (all writable pseudo tags must be protected)
},
MDItemFSLabel => {
Writable => 1,
WritePseudo => 1,
Protected => 1, # (all writable pseudo tags must be protected)
Notes => 'label number: 0-7',
WriteCheck => '$val =~ /^[0-7]$/ ? undef : "Not an integer in the range 0-7"',
},
MDItemFSCreationDate => {
Writable => 1,
WritePseudo => 1,
DelCheck => q{"Can't delete"},
Protected => 1, # (all writable pseudo tags must be protected)
Shift => 'Time', # (but not supported yet)
Notes => q{
file creation date. Requires "setfile" for writing. Note that when
reading, it may take a few seconds after writing a file before this value
reflects the change. However, L<FileCreateDate|Extra.html> is updated immediately
},
Groups => { 2 => 'Time' },
ValueConv => \&MDItemLocalTime,
ValueConvInv => '$val',
PrintConv => '$self->ConvertDateTime($val)',
PrintConvInv => '$self->InverseDateTime($val)',
},
MDItemAcquisitionMake => { Groups => { 2 => 'Camera' } },
MDItemAcquisitionModel => { Groups => { 2 => 'Camera' } },
MDItemAltitude => { Groups => { 2 => 'Location' } },
MDItemAperture => { Groups => { 2 => 'Camera' } },
MDItemAuthors => { Groups => { 2 => 'Author' } },
MDItemBitsPerSample => { Groups => { 2 => 'Image' } },
MDItemCity => { Groups => { 2 => 'Location' } },
MDItemColorSpace => { Groups => { 2 => 'Image' } },
MDItemComment => { },
MDItemContentCreationDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemContentModificationDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemContentType => { },
MDItemContentTypeTree => { },
MDItemContributors => { },
MDItemCopyright => { Groups => { 2 => 'Author' } },
MDItemCountry => { Groups => { 2 => 'Location' } },
MDItemCreator => { Groups => { 2 => 'Document' } },
MDItemDateAdded => { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemDescription => { },
MDItemDisplayName => { },
MDItemDownloadedDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemEncodingApplications => { },
MDItemEXIFGPSVersion => { Groups => { 2 => 'Location' } },
MDItemEXIFVersion => { },
MDItemExposureMode => { Groups => { 2 => 'Camera' } },
MDItemExposureProgram => { Groups => { 2 => 'Camera' } },
MDItemExposureTimeSeconds => { Groups => { 2 => 'Camera' } },
MDItemFlashOnOff => { Groups => { 2 => 'Camera' } },
MDItemFNumber => { Groups => { 2 => 'Camera' } },
MDItemFocalLength => { Groups => { 2 => 'Camera' } },
MDItemFSContentChangeDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemFSCreatorCode => { Groups => { 2 => 'Author' } },
MDItemFSFinderFlags => { },
MDItemFSHasCustomIcon => { },
MDItemFSInvisible => { },
MDItemFSIsExtensionHidden => { },
MDItemFSIsStationery => { },
MDItemFSName => { },
MDItemFSNodeCount => { },
MDItemFSOwnerGroupID => { },
MDItemFSOwnerUserID => { },
MDItemFSSize => { },
MDItemFSTypeCode => { },
MDItemGPSDateStamp => { Groups => { 2 => 'Time' } },
MDItemGPSStatus => { Groups => { 2 => 'Location' } },
MDItemGPSTrack => { Groups => { 2 => 'Location' } },
MDItemHasAlphaChannel => { Groups => { 2 => 'Image' } },
MDItemImageDirection => { Groups => { 2 => 'Location' } },
MDItemISOSpeed => { Groups => { 2 => 'Camera' } },
MDItemKeywords => { },
MDItemKind => { },
MDItemLastUsedDate => { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemLatitude => { Groups => { 2 => 'Location' } },
MDItemLogicalSize => { },
MDItemLongitude => { Groups => { 2 => 'Location' } },
MDItemNumberOfPages => { },
MDItemOrientation => { Groups => { 2 => 'Image' } },
MDItemOriginApplicationIdentifier => { },
MDItemOriginMessageID => { },
MDItemOriginSenderDisplayName => { },
MDItemOriginSenderHandle => { },
MDItemOriginSubject => { },
MDItemPageHeight => { Groups => { 2 => 'Image' } },
MDItemPageWidth => { Groups => { 2 => 'Image' } },
MDItemPhysicalSize => { Groups => { 2 => 'Image' } },
MDItemPixelCount => { Groups => { 2 => 'Image' } },
MDItemPixelHeight => { Groups => { 2 => 'Image' } },
MDItemPixelWidth => { Groups => { 2 => 'Image' } },
MDItemProfileName => { Groups => { 2 => 'Image' } },
MDItemRedEyeOnOff => { Groups => { 2 => 'Camera' } },
MDItemResolutionHeightDPI => { Groups => { 2 => 'Image' } },
MDItemResolutionWidthDPI => { Groups => { 2 => 'Image' } },
MDItemSecurityMethod => { },
MDItemSpeed => { Groups => { 2 => 'Location' } },
MDItemStateOrProvince => { Groups => { 2 => 'Location' } },
MDItemTimestamp => { Groups => { 2 => 'Time' } }, # (time only)
MDItemTitle => { },
MDItemUseCount => { },
MDItemUsedDates => { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemUserTags => {
List => 1,
Writable => 1,
WritePseudo => 1,
Protected => 1, # (all writable pseudo tags must be protected)
Notes => q{
requires "tag" utility for writing -- install with "brew install tag". Note
that user tags may not contain a comma, and that duplicate user tags will
not be written
},
},
MDItemVersion => { },
MDItemWhereFroms => { },
MDItemWhiteBalance => { Groups => { 2 => 'Image' } },
# tags used by Apple Mail on .emlx files
com_apple_mail_dateReceived => { Name => 'AppleMailDateReceived', Groups => { 2 => 'Time' }, %mdDateInfo },
com_apple_mail_flagged => { Name => 'AppleMailFlagged' },
com_apple_mail_messageID => { Name => 'AppleMailMessageID' },
com_apple_mail_priority => { Name => 'AppleMailPriority' },
com_apple_mail_read => { Name => 'AppleMailRead' },
com_apple_mail_repliedTo => { Name => 'AppleMailRepliedTo' },
MDItemAccountHandles => { },
MDItemAccountIdentifier => { },
MDItemAuthorEmailAddresses => { },
MDItemBundleIdentifier => { },
MDItemContentCreationDate_Ranking=>{Groups=> { 2 => 'Time' }, %mdDateInfo },
MDItemDateAdded_Ranking => { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemEmailConversationID => { },
MDItemIdentifier => { },
MDItemInterestingDate_Ranking => { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemIsApplicationManaged => { },
MDItemIsExistingThread => { },
MDItemIsLikelyJunk => { },
MDItemMailboxes => { },
MDItemMailDateReceived_Ranking=> { Groups => { 2 => 'Time' }, %mdDateInfo },
MDItemPrimaryRecipientEmailAddresses => { },
MDItemRecipients => { },
MDItemSubject => { },
);
# "xattr" tags
%Image::ExifTool::MacOS::XAttr = (
WRITE_PROC => \&Image::ExifTool::DummyWriteProc,
GROUPS => { 0 => 'File', 1 => 'MacOS', 2 => 'Other' },
VARS => { NO_ID => 1 }, # (id's are too long)
NOTES => q{
XAttr tags are extracted using the "xattr" utility. They are extracted if
any "XAttr*" tag or the MacOS group is specifically requested, or by setting
the L<XAttrTags API option|../ExifTool.html#XAttrTags> to 1 or the L<RequestAll API option|../ExifTool.html#RequestAll> to 2 or higher.
},
'com.apple.FinderInfo' => {
Name => 'XAttrFinderInfo',
ConvertBinary => 1,
# ref https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-9A581/Finder.h
ValueConv => q{
my @a = unpack('a4a4n3x10nx2N', $$val);
tr/\0//d, $_="'${_}'" foreach @a[0,1];
return "@a";
},
PrintConv => q{
$val =~ s/^('.*?') ('.*?') //s or return $val;
my ($type, $creator) = ($1, $2);
my ($flags, $y, $x, $exFlags, $putAway) = split ' ', $val;
my $label = ($flags >> 1) & 0x07;
my $flags = DecodeBits((($exFlags<<16) | $flags) & 0xfff1, {
0 => 'OnDesk',
6 => 'Shared',
7 => 'HasNoInits',
8 => 'Inited',
10 => 'CustomIcon',
11 => 'Stationery',
12 => 'NameLocked',
13 => 'HasBundle',
14 => 'Invisible',
15 => 'Alias',
# extended flags
22 => 'HasRoutingInfo',
23 => 'ObjectBusy',
24 => 'CustomBadge',
31 => 'ExtendedFlagsValid',
});
my $str = "Type=$type Creator=$creator Flags=$flags Label=$label Pos=($x,$y)";
$str .= " Putaway=$putAway" if $putAway;
return $str;
},
},
'com.apple.quarantine' => {
Name => 'XAttrQuarantine',
Writable => 1,
WritePseudo => 1,
WriteCheck => '"May only delete this tag"',
Protected => 1,
Notes => q{
quarantine information for files downloaded from the internet. May only be
deleted when writing
},
# ($a[1] is the time when the quarantine tag was set)
PrintConv => q{
my @a = split /;/, $val;
$a[0] = 'Flags=' . $a[0];
$a[1] = 'set at ' . ConvertUnixTime(hex $a[1]);
$a[2] = 'by ' . $a[2];
return join ' ', @a;
},
PrintConvInv => '$val',
},
'com.apple.metadata:kMDItemDownloadedDate' => {
Name => 'XAttrMDItemDownloadedDate',
Groups => { 2 => 'Time' },
},
'com.apple.metadata:kMDItemFinderComment' => { Name => 'XAttrMDItemFinderComment' },
'com.apple.metadata:kMDItemWhereFroms' => { Name => 'XAttrMDItemWhereFroms' },
'com.apple.metadata:kMDLabel' => { Name => 'XAttrMDLabel', Binary => 1 },
'com.apple.ResourceFork' => { Name => 'XAttrResourceFork', Binary => 1 },
);
#------------------------------------------------------------------------------
# Convert OS MDItem time string to standard EXIF-formatted local time
# Inputs: 0) time string (eg. "2017-02-21 17:21:43 +0000")
# Returns: EXIF-formatted local time string with timezone
sub MDItemLocalTime($)
{
my $val = shift;
$val =~ tr/-/:/;
$val =~ s/ ?([-+]\d{2}):?(\d{2})/$1:$2/;
# convert from UTC to local time
if ($val =~ /\+00:00$/) {
my $time = Image::ExifTool::GetUnixTime($val);
$val = Image::ExifTool::ConvertUnixTime($time, 1) if $time;
}
return $val;
}
#------------------------------------------------------------------------------
# Set MacOS MDItem and XAttr tags from new tag values
# Inputs: 0) Exiftool ref, 1) file name, 2) list of tags to set
# Returns: 1=something was set OK, 0=didn't try, -1=error (and warning set)
# Notes: There may be errors even if 1 is returned
sub SetMacOSTags($$$)
{
my ($et, $file, $setTags) = @_;
my $result = 0;
my $tag;
foreach $tag (@$setTags) {
my ($nvHash, $f, $v, $attr, $cmd, $err, $silentErr);
my $val = $et->GetNewValue($tag, \$nvHash);
next unless $nvHash;
my $overwrite = $et->IsOverwriting($nvHash);
unless ($$nvHash{TagInfo}{List}) {
next unless $overwrite;
if ($overwrite < 0) {
my $operation = $$nvHash{Shift} ? 'Shifting' : 'Conditional replacement';
$et->Warn("$operation of $tag not yet supported");
next;
}
}
if ($tag eq 'MDItemFSCreationDate' or $tag eq 'FileCreateDate') {
($f = $file) =~ s/'/'\\''/g;
# convert to local time if value has a time zone
if ($val =~ /[-+Z]/) {
my $time = Image::ExifTool::GetUnixTime($val, 1);
$val = Image::ExifTool::ConvertUnixTime($time, 1) if $time;
}
$val =~ s{(\d{4}):(\d{2}):(\d{2})}{$2/$3/$1}; # reformat for setfile
$cmd = "setfile -d '${val}' '${f}'";
} elsif ($tag eq 'MDItemUserTags') {
# (tested with "tag" version 0.9.0)
($f = $file) =~ s/'/'\\''/g;
my @vals = $et->GetNewValue($nvHash);
if ($overwrite < 0 and @{$$nvHash{DelValue}}) {
# delete specified tags
my @dels = @{$$nvHash{DelValue}};
s/'/'\\''/g foreach @dels;
my $del = join ',', @dels;
$err = system "tag -r '${del}' '${f}'>/dev/null 2>&1";
unless ($err) {
$et->VerboseValue("- $tag", $del);
$result = 1;
undef $err if @vals; # more to do if there are tags to add
}
}
unless (defined $err) {
# add new tags, or overwrite or delete existing tags
s/'/'\\''/g foreach @vals;
my $opt = $overwrite > 0 ? '-s' : '-a';
$val = @vals ? join(',', @vals) : '';
$cmd = "tag $opt '${val}' '${f}'";
$et->VPrint(1," - $tag = (all)\n") if $overwrite > 0;
undef $val if $val eq '';
}
} elsif ($tag eq 'XAttrQuarantine') {
($f = $file) =~ s/'/'\\''/g;
$cmd = "xattr -d com.apple.quarantine '${f}'";
$silentErr = 256; # (will get this error if attribute doesn't exist)
} else {
($f = $file) =~ s/(["\\])/\\$1/g; # escape necessary characters for script
$f =~ s/'/'"'"'/g;
if ($tag eq 'MDItemFinderComment') {
# (write finder comment using osascript instead of xattr
# because it is more work to construct the necessary bplist)
$val = '' unless defined $val; # set to empty string instead of deleting
$v = $et->Encode($val, 'UTF8');
$v =~ s/(["\\])/\\$1/g;
$v =~ s/'/'"'"'/g;
$attr = 'comment';
} else { # $tag eq 'MDItemFSLabel'
$v = $val ? 8 - $val : 0; # convert from label to label index (0 for no label)
$attr = 'label index';
}
$cmd = qq(osascript -e 'set fp to POSIX file "$f" as alias' -e \\
'tell application "Finder" to set $attr of file fp to "$v"');
}
if (defined $cmd) {
$err = system $cmd . '>/dev/null 2>&1'; # (pipe all output to /dev/null)
}
if (not $err) {
$et->VerboseValue("+ $tag", $val) if defined $val;
$result = 1;
} elsif (not $silentErr or $err != $silentErr) {
$cmd =~ s/ .*//s;
$et->Warn(qq{Error $err running "$cmd" to set $tag});
$result = -1 unless $result;
}
}
return $result;
}
#------------------------------------------------------------------------------
# Extract MacOS metadata item tags
# Inputs: 0) ExifTool object ref, 1) file name
sub ExtractMDItemTags($$)
{
local $_;
my ($et, $file) = @_;
my ($fn, $tag, $val, $tmp);
($fn = $file) =~ s/([`"\$\\])/\\$1/g; # escape necessary characters
$et->VPrint(0, '(running mdls)');
my @mdls = `mdls "$fn" 2> /dev/null`; # get MacOS metadata
if ($? or not @mdls) {
$et->Warn('Error running "mdls" to extract MDItem tags');
return;
}
my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::MDItem');
$$et{INDENT} .= '| ';
$et->VerboseDir('MDItem');
foreach (@mdls) {
chomp;
if (ref $val ne 'ARRAY') {
s/^k?(\w+)\s*= // or next;
$tag = $1;
$_ eq '(' and $val = [ ], next; # (start of a list)
$_ = '' if $_ eq '(null)';
s/^"// and s/"$//; # remove quotes if they exist
$val = $_;
} elsif ($_ eq ')') { # (end of a list)
$_ = $$val[0];
next unless defined $_;
} else {
# add item to list
s/^ //; # remove leading spaces
s/,$//; # remove trailing comma
$_ = '' if $_ eq '(null)';
s/^"// and s/"$//; # remove quotes if they exist
s/\\"/"/g; # un-escape quotes
$_ = $et->Decode($_, 'UTF8');
push @$val, $_;
next;
}
# add to Extra tags if not done already
unless ($$tagTablePtr{$tag}) {
# check for a date/time format
my %tagInfo;
%tagInfo = (
Groups => { 2 => 'Time' },
ValueConv => \&MDItemLocalTime,
PrintConv => '$self->ConvertDateTime($val)',
) if /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/;
# change tags like "com_apple_mail_xxx" to "AppleMailXxx"
($tmp = $tag) =~ s/^com_//; # remove leading "com_"
$tmp =~ s/_([a-z])/\u$1/g; # use CamelCase
$tagInfo{Name} = Image::ExifTool::MakeTagName($tmp);
$tagInfo{List} = 1 if ref $val eq 'ARRAY';
$tagInfo{Groups}{2} = 'Audio' if $tag =~ /Audio/;
$tagInfo{Groups}{2} = 'Author' if $tag =~ /(Copyright|Author)/;
$et->VPrint(0, " [adding $tag]\n");
AddTagToTable($tagTablePtr, $tag, \%tagInfo);
}
$val = $et->Decode($val, 'UTF8') unless ref $val;
$et->HandleTag($tagTablePtr, $tag, $val);
undef $val;
}
$$et{INDENT} =~ s/\| $//;
}
#------------------------------------------------------------------------------
# Extract MacOS extended attribute tags
# Inputs: 0) ExifTool object ref, 1) file name
sub ExtractXAttrTags($$)
{
local $_;
my ($et, $file) = @_;
my ($fn, $tag, $val, $warn);
($fn = $file) =~ s/([`"\$\\])/\\$1/g; # escape necessary characters
$et->VPrint(0, '(running xattr)');
my @xattr = `xattr -lx "$fn" 2> /dev/null`; # get MacOS extended attributes
if ($? or not @xattr) {
$? and $et->Warn('Error running "xattr" to extract XAttr tags');
return;
}
my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::XAttr');
$$et{INDENT} .= '| ';
$et->VerboseDir('XAttr');
push @xattr, ''; # (for a list terminator)
foreach (@xattr) {
chomp;
if (s/^[\dA-Fa-f]{8}//) {
$tag or $warn = 1, next;
s/\|.*//;
tr/ //d;
(/[^\dA-Fa-f]/ or length($_) & 1) and $warn = 2, next;
$val = '' unless defined $val;
$val .= pack('H*', $_);
next;
} elsif ($tag and defined $val) {
# add to our table if necessary
unless ($$tagTablePtr{$tag}) {
my $name;
# generate tag name from attribute name
if ($tag =~ /^com\.apple\.(.*)$/) {
($name = $1) =~ s/^metadata:_?k//;
} else {
($name = $tag) =~ s/[.:]([a-z])/\U$1/g;
}
$name = 'XAttr' . ucfirst $name;
my %tagInfo = ( Name => $name );
$tagInfo{Groups} = { 2 => 'Time' } if $tag=~/Date$/;
$et->VPrint(0, " [adding $tag]\n");
AddTagToTable($tagTablePtr, $tag, \%tagInfo);
}
if ($val =~ /^bplist0/) {
my %dirInfo = ( DataPt => \$val );
require Image::ExifTool::PLIST;
if (Image::ExifTool::PLIST::ProcessBinaryPLIST($et, \%dirInfo, $tagTablePtr)) {
next if ref $dirInfo{Value} eq 'HASH';
$val = $dirInfo{Value}
} else {
$et->Warn("Error decoding $$tagTablePtr{$tag}{Name}");
next;
}
}
if (not ref $val and ($val =~ /\0/ or length($val) > 200) or $tag eq 'XAttrMDLabel') {
my $buff = $val;
$val = \$buff;
}
$et->HandleTag($tagTablePtr, $tag, $val);
undef $tag;
undef $val;
}
next unless length;
s/:$// or $warn = 3, next; # attribute name must have trailing ":"
defined $val and $warn = 4, undef $val;
# remove random ID after kMDLabel in tag ID
($tag = $_) =~ s/^com.apple.metadata:kMDLabel_.*/com.apple.metadata:kMDLabel/s;
}
$warn and $et->Warn(qq{Error $warn parsing "xattr" output});
$$et{INDENT} =~ s/\| $//;
}
#------------------------------------------------------------------------------
# Extract MacOS file creation date/time
# Inputs: 0) ExifTool object ref, 1) file name
sub GetFileCreateDate($$)
{
local $_;
my ($et, $file) = @_;
my ($fn, $tag, $val, $tmp);
($fn = $file) =~ s/([`"\$\\])/\\$1/g; # escape necessary characters
$et->VPrint(0, '(running stat)');
my $time = `stat -f '%SB' -t '%Y:%m:%d %H:%M:%S%z' "$fn" 2> /dev/null`;
if ($? or not $time or $time !~ s/([-+]\d{2})(\d{2})\s*$/$1:$2/) {
$et->Warn('Error running "stat" to extract FileCreateDate');
return;
}
$$et{SET_GROUP1} = 'MacOS';
$et->FoundTag(FileCreateDate => $time);
delete $$et{SET_GROUP1};
}
1; # end
__END__
=head1 NAME
Image::ExifTool::MacOS - Read/write MacOS system tags
=head1 SYNOPSIS
This module is used by Image::ExifTool
=head1 DESCRIPTION
This module contains definitions required by Image::ExifTool to extract
MDItem* and XAttr* tags on MacOS systems using the "mdls" and "xattr"
utilities respectively. Writable tags use "xattr", "setfile" or "osascript"
for writing.
=head1 AUTHOR
Copyright 2003-2018, Phil Harvey (phil at owl.phy.queensu.ca)
This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=head1 SEE ALSO
L<Image::ExifTool::TagNames/MacOS Tags>,
L<Image::ExifTool(3pm)|Image::ExifTool>
=cut