<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">#------------------------------------------------------------------------------
# File:         MacOS.pm
#
# Description:  Read/write MacOS system tags
#
# Revisions:    2017/03/01 - P. Harvey Created
#               2020/10/13 - PH Added ability to read MacOS "._" files
#------------------------------------------------------------------------------

package Image::ExifTool::MacOS;
use strict;
use vars qw($VERSION);
use Image::ExifTool qw(:DataAccess :Utils);

$VERSION = '1.12';

sub MDItemLocalTime($);
sub ProcessATTR($$$);

my %mdDateInfo = (
    ValueConv =&gt; \&amp;MDItemLocalTime,
    PrintConv =&gt; '$self-&gt;ConvertDateTime($val)',
);

# Information decoded from Mac OS sidecar files
%Image::ExifTool::MacOS::Main = (
    GROUPS =&gt; { 0 =&gt; 'File', 1 =&gt; 'MacOS' },
    NOTES =&gt; q{
        Note that on some filesystems, MacOS creates sidecar files with names that
        begin with "._".  ExifTool will read these files if specified, and extract
        the information listed in the following table without the need for extra
        options, but these files are not writable directly.
    },
    2 =&gt; {
        Name =&gt; 'RSRC',
        SubDirectory =&gt; { TagTable =&gt; 'Image::ExifTool::RSRC::Main' },
    },
    9 =&gt; {
        Name =&gt; 'ATTR',
        SubDirectory =&gt; {
            TagTable =&gt; 'Image::ExifTool::MacOS::XAttr',
            ProcessProc =&gt; \&amp;ProcessATTR,
        },
    },
);

# "mdls" tags (ref PH)
%Image::ExifTool::MacOS::MDItem = (
    WRITE_PROC =&gt; \&amp;Image::ExifTool::DummyWriteProc,
    VARS =&gt; { NO_ID =&gt; 1 },
    GROUPS =&gt; { 0 =&gt; 'File', 1 =&gt; 'MacOS', 2 =&gt; 'Other' },
    NOTES =&gt; 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 API L&lt;MDItemTags|../ExifTool.html#MDItemTags&gt; option to 1 or the API L&lt;RequestAll|../ExifTool.html#RequestAll&gt; option to 2 or
        higher.  Note that these tags do not necessarily reflect the current
        metadata of a file -- it may take some time for the MacOS mdworker daemon to
        index the file after a metadata change.
    },
    MDItemFinderComment =&gt; {
        Writable =&gt; 1,
        WritePseudo =&gt; 1,
        Protected =&gt; 1, # (all writable pseudo tags must be protected)
    },
    MDItemFSLabel =&gt; {
        Writable =&gt; 1,
        WritePseudo =&gt; 1,
        Protected =&gt; 1, # (all writable pseudo tags must be protected)
        WriteCheck =&gt; '$val =~ /^[0-7]$/ ? undef : "Not an integer in the range 0-7"',
        PrintConv =&gt; {
            0 =&gt; '0 (none)',
            1 =&gt; '1 (Gray)',
            2 =&gt; '2 (Green)',
            3 =&gt; '3 (Purple)',
            4 =&gt; '4 (Blue)',
            5 =&gt; '5 (Yellow)',
            6 =&gt; '6 (Red)',
            7 =&gt; '7 (Orange)',
        },
    },
    MDItemFSCreationDate =&gt; {
        Writable =&gt; 1,
        WritePseudo =&gt; 1,
        DelCheck =&gt; q{"Can't delete"},
        Protected =&gt; 1, # (all writable pseudo tags must be protected)
        Shift =&gt; 'Time', # (but not supported yet)
        Notes =&gt; 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&lt;FileCreateDate|Extra.html&gt; is updated immediately
        },
        Groups =&gt; { 2 =&gt; 'Time' },
        ValueConv =&gt; \&amp;MDItemLocalTime,
        ValueConvInv =&gt; '$val',
        PrintConv =&gt; '$self-&gt;ConvertDateTime($val)',
        PrintConvInv =&gt; '$self-&gt;InverseDateTime($val)',
    },
    MDItemAcquisitionMake         =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemAcquisitionModel        =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemAltitude                =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemAperture                =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemAudioBitRate            =&gt; { Groups =&gt; { 2 =&gt; 'Audio' } },
    MDItemAudioChannelCount       =&gt; { Groups =&gt; { 2 =&gt; 'Audio' } },
    MDItemAuthors                 =&gt; { Groups =&gt; { 2 =&gt; 'Author' } },
    MDItemBitsPerSample           =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemCity                    =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemCodecs                  =&gt; { },
    MDItemColorSpace              =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemComment                 =&gt; { },
    MDItemContentCreationDate     =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemContentCreationDateRanking =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemContentModificationDate =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemContentType             =&gt; { },
    MDItemContentTypeTree         =&gt; { },
    MDItemContributors            =&gt; { },
    MDItemCopyright               =&gt; { Groups =&gt; { 2 =&gt; 'Author' } },
    MDItemCountry                 =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemCreator                 =&gt; { Groups =&gt; { 2 =&gt; 'Document' } },
    MDItemDateAdded               =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemDescription             =&gt; { },
    MDItemDisplayName             =&gt; { },
    MDItemDownloadedDate          =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemDurationSeconds         =&gt; { PrintConv =&gt; 'ConvertDuration($val)' },
    MDItemEncodingApplications    =&gt; { },
    MDItemEXIFGPSVersion          =&gt; { Groups =&gt; { 2 =&gt; 'Location' }, Description =&gt; 'MD Item EXIF GPS Version' },
    MDItemEXIFVersion             =&gt; { },
    MDItemExposureMode            =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemExposureProgram         =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemExposureTimeSeconds     =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemFlashOnOff              =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemFNumber                 =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemFocalLength             =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemFSContentChangeDate     =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemFSCreatorCode           =&gt; { Groups =&gt; { 2 =&gt; 'Author' } },
    MDItemFSFinderFlags           =&gt; { },
    MDItemFSHasCustomIcon         =&gt; { },
    MDItemFSInvisible             =&gt; { },
    MDItemFSIsExtensionHidden     =&gt; { },
    MDItemFSIsStationery          =&gt; { },
    MDItemFSName                  =&gt; { },
    MDItemFSNodeCount             =&gt; { },
    MDItemFSOwnerGroupID          =&gt; { },
    MDItemFSOwnerUserID           =&gt; { },
    MDItemFSSize                  =&gt; { },
    MDItemFSTypeCode              =&gt; { },
    MDItemGPSDateStamp            =&gt; { Groups =&gt; { 2 =&gt; 'Time' } },
    MDItemGPSStatus               =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemGPSTrack                =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemHasAlphaChannel         =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemImageDirection          =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemInterestingDateRanking  =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemISOSpeed                =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemKeywords                =&gt; { },
    MDItemKind                    =&gt; { },
    MDItemLastUsedDate            =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemLastUsedDate_Ranking    =&gt; { },
    MDItemLatitude                =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemLensModel               =&gt; { },
    MDItemLogicalSize             =&gt; { },
    MDItemLongitude               =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemMediaTypes              =&gt; { },
    MDItemNumberOfPages           =&gt; { },
    MDItemOrientation             =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemOriginApplicationIdentifier =&gt; { },
    MDItemOriginMessageID         =&gt; { },
    MDItemOriginSenderDisplayName =&gt; { },
    MDItemOriginSenderHandle      =&gt; { },
    MDItemOriginSubject           =&gt; { },
    MDItemPageHeight              =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemPageWidth               =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemPhysicalSize            =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemPixelCount              =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemPixelHeight             =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemPixelWidth              =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemProfileName             =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemRedEyeOnOff             =&gt; { Groups =&gt; { 2 =&gt; 'Camera' } },
    MDItemResolutionHeightDPI     =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemResolutionWidthDPI      =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    MDItemSecurityMethod          =&gt; { },
    MDItemSpeed                   =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemStateOrProvince         =&gt; { Groups =&gt; { 2 =&gt; 'Location' } },
    MDItemStreamable              =&gt; { },
    MDItemTimestamp               =&gt; { Groups =&gt; { 2 =&gt; 'Time' } }, # (time only)
    MDItemTitle                   =&gt; { },
    MDItemTotalBitRate            =&gt; { },
    MDItemUseCount                =&gt; { },
    MDItemUsedDates               =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemUserDownloadedDate      =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemUserDownloadedUserHandle=&gt; { },
    MDItemUserSharedReceivedDate  =&gt; { },
    MDItemUserSharedReceivedRecipient =&gt; { },
    MDItemUserSharedReceivedRecipientHandle =&gt; { },
    MDItemUserSharedReceivedSender=&gt; { },
    MDItemUserSharedReceivedSenderHandle =&gt; { },
    MDItemUserSharedReceivedTransport =&gt; { },
    MDItemUserTags                =&gt; {
        List =&gt; 1,
        Writable =&gt; 1,
        WritePseudo =&gt; 1,
        Protected =&gt; 1, # (all writable pseudo tags must be protected)
        Notes =&gt; 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                 =&gt; { },
    MDItemVideoBitRate            =&gt; { Groups =&gt; { 2 =&gt; 'Video' } },
    MDItemWhereFroms              =&gt; { },
    MDItemWhiteBalance            =&gt; { Groups =&gt; { 2 =&gt; 'Image' } },
    # tags used by Apple Mail on .emlx files
    com_apple_mail_dateReceived   =&gt; { Name =&gt; 'AppleMailDateReceived', Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    com_apple_mail_dateSent       =&gt; { Name =&gt; 'AppleMailDateSent',     Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    com_apple_mail_flagged        =&gt; { Name =&gt; 'AppleMailFlagged' },
    com_apple_mail_messageID      =&gt; { Name =&gt; 'AppleMailMessageID' },
    com_apple_mail_priority       =&gt; { Name =&gt; 'AppleMailPriority' },
    com_apple_mail_read           =&gt; { Name =&gt; 'AppleMailRead' },
    com_apple_mail_repliedTo      =&gt; { Name =&gt; 'AppleMailRepliedTo' },
    com_apple_mail_isRemoteAttachment =&gt; { Name =&gt; 'AppleMailIsRemoteAttachment' },
    MDItemAccountHandles          =&gt; { },
    MDItemAccountIdentifier       =&gt; { },
    MDItemAuthorEmailAddresses    =&gt; { },
    MDItemBundleIdentifier        =&gt; { },
    MDItemContentCreationDate_Ranking=&gt;{Groups=&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemDateAdded_Ranking       =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemEmailConversationID     =&gt; { },
    MDItemIdentifier              =&gt; { },
    MDItemInterestingDate_Ranking =&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemIsApplicationManaged    =&gt; { },
    MDItemIsExistingThread        =&gt; { },
    MDItemIsLikelyJunk            =&gt; { },
    MDItemMailboxes               =&gt; { },
    MDItemMailDateReceived_Ranking=&gt; { Groups =&gt; { 2 =&gt; 'Time' }, %mdDateInfo },
    MDItemPrimaryRecipientEmailAddresses =&gt; { },
    MDItemRecipients              =&gt; { },
    MDItemSubject                 =&gt; { },
);

# "xattr" tags
%Image::ExifTool::MacOS::XAttr = (
    WRITE_PROC =&gt; \&amp;Image::ExifTool::DummyWriteProc,
    GROUPS =&gt; { 0 =&gt; 'File', 1 =&gt; 'MacOS', 2 =&gt; 'Other' },
    VARS =&gt; { NO_ID =&gt; 1 }, # (id's are too long)
    NOTES =&gt; 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 API L&lt;XAttrTags|../ExifTool.html#XAttrTags&gt; option to 1 or the API L&lt;RequestAll|../ExifTool.html#RequestAll&gt; option to 2 or higher.
        And they are extracted by default from MacOS "._" files when reading
        these files directly.
    },
    'com.apple.FinderInfo' =&gt; {
        Name =&gt; 'XAttrFinderInfo',
        ConvertBinary =&gt; 1,
        # ref https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-9A581/Finder.h
        ValueConv =&gt; q{
            my @a = unpack('a4a4n3x10nx2N', $$val);
            tr/\0//d, $_="'${_}'" foreach @a[0,1];
            return "@a";
        },
        PrintConv =&gt; q{
            $val =~ s/^('.*?') ('.*?') //s or return $val;
            my ($type, $creator) = ($1, $2);
            my ($flags, $y, $x, $exFlags, $putAway) = split ' ', $val;
            my $label = ($flags &gt;&gt; 1) &amp; 0x07;
            my $flags = DecodeBits((($exFlags&lt;&lt;16) | $flags) &amp; 0xfff1, {
                0 =&gt; 'OnDesk',
                6 =&gt; 'Shared',
                7 =&gt; 'HasNoInits',
                8 =&gt; 'Inited',
                10 =&gt; 'CustomIcon',
                11 =&gt; 'Stationery',
                12 =&gt; 'NameLocked',
                13 =&gt; 'HasBundle',
                14 =&gt; 'Invisible',
                15 =&gt; 'Alias',
                # extended flags
                22 =&gt; 'HasRoutingInfo',
                23 =&gt; 'ObjectBusy',
                24 =&gt; 'CustomBadge',
                31 =&gt; 'ExtendedFlagsValid',
            });
            my $str = "Type=$type Creator=$creator Flags=$flags Label=$label Pos=($x,$y)";
            $str .= " Putaway=$putAway" if $putAway;
            return $str;
        },
    },
    'com.apple.quarantine' =&gt; {
        Name =&gt; 'XAttrQuarantine',
        Writable =&gt; 1,
        WritePseudo =&gt; 1,
        WriteCheck =&gt; '"May only delete this tag"',
        Protected =&gt; 1,
        Notes =&gt; 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 =&gt; 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 =&gt; '$val',
    },
    'com.apple.metadata:com_apple_mail_dateReceived' =&gt; {
        Name =&gt; 'XAttrAppleMailDateReceived',
        Groups =&gt; { 2 =&gt; 'Time' },
    },
    'com.apple.metadata:com_apple_mail_dateSent' =&gt; {
        Name =&gt; 'XAttrAppleMailDateSent',
        Groups =&gt; { 2 =&gt; 'Time' },
    },
    'com.apple.metadata:com_apple_mail_isRemoteAttachment' =&gt; {
        Name =&gt; 'XAttrAppleMailIsRemoteAttachment',
    },
    'com.apple.metadata:kMDItemDownloadedDate' =&gt; {
        Name =&gt; 'XAttrMDItemDownloadedDate',
        Groups =&gt; { 2 =&gt; 'Time' },
    },
    'com.apple.metadata:kMDItemFinderComment'  =&gt; { Name =&gt; 'XAttrMDItemFinderComment' },
    'com.apple.metadata:kMDItemWhereFroms'     =&gt; { Name =&gt; 'XAttrMDItemWhereFroms' },
    'com.apple.metadata:kMDLabel'              =&gt; { Name =&gt; 'XAttrMDLabel', Binary =&gt; 1 },
    'com.apple.ResourceFork'                   =&gt; { Name =&gt; 'XAttrResourceFork', Binary =&gt; 1 },
    'com.apple.lastuseddate#PS'                =&gt; {
        Name =&gt; 'XAttrLastUsedDate',
        Groups =&gt; { 2 =&gt; 'Time' },
        # (first 4 bytes are date/time.  Not sure what remaining 12 bytes are for)
        RawConv =&gt; 'ConvertUnixTime(unpack("V",$$val))',
        PrintConv =&gt; '$self-&gt;ConvertDateTime($val)',
    },
);

#------------------------------------------------------------------------------
# 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-&gt;GetNewValue($tag, \$nvHash);
        next unless $nvHash;
        my $overwrite = $et-&gt;IsOverwriting($nvHash);
        unless ($$nvHash{TagInfo}{List}) {
            next unless $overwrite;
            if ($overwrite &lt; 0) {
                my $operation = $$nvHash{Shift} ? 'Shifting' : 'Conditional replacement';
                $et-&gt;Warn("$operation of MacOS $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 = "/usr/bin/setfile -d '${val}' '${f}'";
        } elsif ($tag eq 'MDItemUserTags') {
            # (tested with "tag" version 0.9.0)
            ($f = $file) =~ s/'/'\\''/g;
            my @vals = $et-&gt;GetNewValue($nvHash);
            if ($overwrite &lt; 0 and @{$$nvHash{DelValue}}) {
                # delete specified tags
                my @dels = @{$$nvHash{DelValue}};
                s/'/'\\''/g foreach @dels;
                my $del = join ',', @dels;
                $err = system "/usr/local/bin/tag -r '${del}' '${f}'&gt;/dev/null 2&gt;&amp;1";
                unless ($err) {
                    $et-&gt;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 &gt; 0 ? '-s' : '-a';
                $val = @vals ? join(',', @vals) : '';
                $cmd = "/usr/local/bin/tag $opt '${val}' '${f}'";
                $et-&gt;VPrint(1,"    - $tag = (all)\n") if $overwrite &gt; 0;
                undef $val if $val eq '';
            }
        } elsif ($tag eq 'XAttrQuarantine') {
            ($f = $file) =~ s/'/'\\''/g;
            $cmd = "/usr/bin/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-&gt;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(/usr/bin/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 . '&gt;/dev/null 2&gt;&amp;1'; # (pipe all output to /dev/null)
        }
        if (not $err) {
            $et-&gt;VerboseValue("+ $tag", $val) if defined $val;
            $result = 1;
        } elsif (not $silentErr or $err != $silentErr) {
            $cmd =~ s/ .*//s;
            $et-&gt;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-&gt;VPrint(0, '(running mdls)');
    my @mdls = `/usr/bin/mdls "$fn" 2&gt; /dev/null`;   # get MacOS metadata
    if ($? or not @mdls) {
        $et-&gt;Warn('Error running "mdls" to extract MDItem tags');
        return;
    }
    my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::MDItem');
    $$et{INDENT} .= '| ';
    $et-&gt;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-&gt;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 =&gt; { 2 =&gt; 'Time' },
                ValueConv =&gt; \&amp;MDItemLocalTime,
                PrintConv =&gt; '$self-&gt;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-&gt;VPrint(0, "  [adding $tag]\n");
            AddTagToTable($tagTablePtr, $tag, \%tagInfo);
        }
        $val = $et-&gt;Decode($val, 'UTF8') unless ref $val;
        $et-&gt;HandleTag($tagTablePtr, $tag, $val);
        undef $val;
    }
    $$et{INDENT} =~ s/\| $//;
}

        
#------------------------------------------------------------------------------
# Read MacOS XAttr value
# Inputs: 0) ExifTool object ref, 1) file name
sub ReadXAttrValue($$$$)
{
    my ($et, $tagTablePtr, $tag, $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//;
            $name =~ s/^metadata:(com_)?//;
        } else {
            $name = $tag;
        }
        $name =~ s/[.:_]([a-z])/\U$1/g;
        $name = 'XAttr' . ucfirst $name;
        my %tagInfo = ( Name =&gt; $name );
        $tagInfo{Groups} = { 2 =&gt; 'Time' } if $tag=~/Date$/;
        $et-&gt;VPrint(0, "  [adding $tag]\n");
        AddTagToTable($tagTablePtr, $tag, \%tagInfo);
    }
    if ($val =~ /^bplist0/) {
        my %dirInfo = ( DataPt =&gt; \$val );
        require Image::ExifTool::PLIST;
        if (Image::ExifTool::PLIST::ProcessBinaryPLIST($et, \%dirInfo, $tagTablePtr)) {
            return undef if ref $dirInfo{Value} eq 'HASH';
            $val = $dirInfo{Value}
        } else {
            $et-&gt;Warn("Error decoding $$tagTablePtr{$tag}{Name}");
            return undef;
        }
    }
    if (not ref $val and ($val =~ /\0/ or length($val) &gt; 200) or $tag eq 'XAttrMDLabel') {
        my $buff = $val;
        $val = \$buff;
    }
    return $val;
}

#------------------------------------------------------------------------------
# Read MacOS extended attribute tags using 'xattr' utility
# 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-&gt;VPrint(0, '(running xattr)');
    my @xattr = `/usr/bin/xattr -lx "$fn" 2&gt; /dev/null`; # get MacOS extended attributes
    if ($? or not @xattr) {
        $? and $et-&gt;Warn('Error running "xattr" to extract XAttr tags');
        return;
    }
    my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::XAttr');
    $$et{INDENT} .= '| ';
    $et-&gt;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($_) &amp; 1) and $warn = 2, next;
            $val = '' unless defined $val;
            $val .= pack('H*', $_);
            next;
        } elsif ($tag and defined $val) {
            $val = ReadXAttrValue($et, $tagTablePtr, $tag, $val);
            $et-&gt;HandleTag($tagTablePtr, $tag, $val) if defined $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-&gt;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-&gt;VPrint(0, '(running stat)');
    my $time = `/usr/bin/stat -f '%SB' -t '%Y:%m:%d %H:%M:%S%z' "$fn" 2&gt; /dev/null`;
    if ($? or not $time or $time !~ s/([-+]\d{2})(\d{2})\s*$/$1:$2/) {
        $et-&gt;Warn('Error running "stat" to extract FileCreateDate');
        return;
    }
    $$et{SET_GROUP1} = 'MacOS';
    $et-&gt;FoundTag(FileCreateDate =&gt; $time);
    delete $$et{SET_GROUP1};
}

#------------------------------------------------------------------------------
# Read ATTR metadata from "._" file
# Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
# Return: 1 on success
# (ref https://www.swiftforensics.com/2018/11/the-dot-underscore-file-format.html)
sub ProcessATTR($$$)
{
    my ($et, $dirInfo, $tagTablePtr) = @_;
    my $dataPt = $$dirInfo{DataPt};
    my $dataPos = $$dirInfo{DataPos};
    my $dataLen = length $$dataPt;

    $dataLen &gt;= 58 and $$dataPt =~ /^.{34}ATTR/s or $et-&gt;Warn('Invalid ATTR header'), return 0;
    my $entries = Get32u($dataPt, 66);
    $et-&gt;VerboseDir('ATTR', $entries);
    # (Note: The RAF is not in $dirInfo because it would break RSRC reading --
    # the RSCR block uses relative offsets, while the ATTR block uses absolute! grrr!)
    my $raf = $$et{RAF};
    my $pos = 70;       # first entry is after ATTR header
    my $i;
    for ($i=0; $i&lt;$entries; ++$i) {
        $pos + 12 &gt; $dataLen and $et-&gt;Warn('Truncated ATTR entry'), last;
        my $off = Get32u($dataPt, $pos);
        my $len = Get32u($dataPt, $pos + 4);
        my $n = Get8u($dataPt, $pos + 10);  # number of characters in tag name
        $pos + 11 + $n &gt; $dataLen and $et-&gt;Warn('Truncated ATTR name'), last;
        $off -= $dataPos;       # convert to relative offset (grrr!)
        $off &lt; 0 or $off &gt; $dataLen and $et-&gt;Warn('Invalid ATTR offset'), last;
        my $tag = substr($$dataPt, $pos + 11, $n);
        $tag =~ s/\0+$//;       # remove null terminator
        # remove random ID after kMDLabel in tag ID
        $tag =~ s/^com.apple.metadata:kMDLabel_.*/com.apple.metadata:kMDLabel/s;
        $off + $len &gt; $dataLen and $et-&gt;Warn('Truncated ATTR value'), last;
        my $val = ReadXAttrValue($et, $tagTablePtr, $tag, substr($$dataPt, $off, $len));
        $et-&gt;HandleTag($tagTablePtr, $tag, $val,
            DataPt  =&gt; $dataPt,
            DataPos =&gt; $dataPos,
            Start   =&gt; $off,
            Size    =&gt; $len,
        ) if defined $val;
        $pos += (11 + $n + 3) &amp; -4; # step to next entry (on even 4-byte boundary)
    }
    return 1;
}

#------------------------------------------------------------------------------
# Read information from a MacOS "._" sidecar file
# Inputs: 0) ExifTool ref, 1) dirInfo ref
# Returns: 1 on success, 0 if this wasn't a valid "._" file
# (ref https://www.swiftforensics.com/2018/11/the-dot-underscore-file-format.html)
sub ProcessMacOS($$)
{
    my ($et, $dirInfo) = @_;
    my $raf = $$dirInfo{RAF};
    my ($hdr, $buff, $i);

    return 0 unless $raf-&gt;Read($hdr, 26) == 26 and $hdr =~ /^\0\x05\x16\x07\0(.)\0\0Mac OS X        /s;
    my $ver = ord $1;
    # (extension may be anything, so just echo back the incoming file extension if it exists)
    $et-&gt;SetFileType(undef, undef, $$et{FILE_EXT});
    $ver == 2 or $et-&gt;Warn("Unsupported file version $ver"), return 1;
    SetByteOrder('MM');
    my $tagTablePtr = GetTagTable('Image::ExifTool::MacOS::Main');
    my $entries = Get16u(\$hdr, 0x18);
    $et-&gt;VerboseDir('MacOS', $entries);
    $raf-&gt;Read($hdr, $entries * 12) == $entries * 12 or $et-&gt;Warn('Truncated header'), return 1;
    for ($i=0; $i&lt;$entries; ++$i) {
        my $pos = $i * 12;
        my $tag = Get32u(\$hdr, $pos);
        my $off = Get32u(\$hdr, $pos + 4);
        my $len = Get32u(\$hdr, $pos + 8);
        $len &gt; 100000000 and $et-&gt;Warn('Record size too large'), last;
        $raf-&gt;Seek($off,0) and $raf-&gt;Read($buff,$len) == $len or $et-&gt;Warn('Truncated record'), last;
        $et-&gt;HandleTag($tagTablePtr, $tag, undef, DataPt =&gt; \$buff, DataPos =&gt; $off, Index =&gt; $i);
    }
    return 1;
}

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.  It also reads metadata directly from the MacOS "_."
sidecar files that are used on some filesystems to store file attributes. 
Writable tags use "xattr", "setfile" or "osascript" for writing.

=head1 AUTHOR

Copyright 2003-2023, Phil Harvey (philharvey66 at gmail.com)

This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=head1 SEE ALSO

L&lt;Image::ExifTool::TagNames/MacOS Tags&gt;,
L&lt;Image::ExifTool(3pm)|Image::ExifTool&gt;

=cut

</pre></body></html>