#!/usr/bin/perl -w

# Copyright (c) 2009 Jim Ursetto.  All rights reserved.
# License: BSD.

# Run this with no arguments, or --help, to get usage information.

use strict;
use Fcntl;
use File::Temp;
use File::Basename;
use Getopt::Long;

my $nerrors = 0;
my $skipped = 0;
my $fixed = 0;
my $seen = 0;
my $verbose = 0;
my $dry_run = 0;
my $quiet   = 0;
my $help    = 0;

sub verbose { warn @_ if $verbose; $verbose }
sub err { warn(@_), $nerrors++ };
sub syswrite_maybe {
    my ($fh, $buf) = @_;
    return syswrite($fh, $buf) unless $dry_run;
    return length($buf);
}
sub usage {
    die <<EOF;
usage: $0 [options] file ...
   -n, --dry-run   Show processing but don't make changes
   -q, --quiet     Be very, very quiet (don't report fixes)
   -v, --verbose   Be prolix (report all actions)

   `file ...` should be a list of AppleDouble files, typically
   prefixed with "._", which have been created via rsync+hfsmode.
   These files are unreadable when created on an Intel machine
   due to endian issues.  This tool will fix any file which
   exhibits these endian issues and update it in place.

   Quickstart:
     find . -name "._*" -type f -print0 | xargs -0 $0 -n -q
     find . -name "._*" -type f -print0 | xargs -0 $0

EOF
}

GetOptions('verbose|v' => \$verbose, 'dry-run|n' => \$dry_run,
           'help|h|?' => \$help, 'quiet|q' => \$quiet);
usage() if @ARGV == 0 or $help;
$verbose = 0 if $quiet;

FILE: for my $fn (@ARGV) {
    $seen++;
    sysopen my $fh, $fn, O_RDWR
      or err("$fn: unable to open: $!\n"), next;
    verbose("* Processing $fn\n");
    my $buf;
    # 4 bytes magic, 4 bytes version, 16 bytes filler, 2 bytes no. entries
    sysread($fh, $buf, 4 + 4 + 16 + 2) == (4 + 4 + 16 + 2)
      or err("$fn: unable to read header: $!\n"), next;
    my ($magic, $version, $filler, $nentries) =
      unpack("VVa16v", $buf);
    $magic == 0x00051607
      or verbose("$fn: skipping magic number ",   # let's not consider this an error
                 sprintf("%08x", $magic), "\n"), $skipped++, next;
    $version == 0x00020000
      or err("$fn: skipping AppleDouble version ",
             sprintf("%08x", $version), "\n"), next;
    # ignore filler
    # Gather entry descriptors and ensure we understand them.
    # Right now, we only handle resource forks and Finder info.
    my $file_size = -s $fn
      or err("$fn: unable to get size: $!\n"), next FILE;
    my @descriptors = map {
        sysread($fh, $buf, 4 + 4 + 4) == (4 + 4 + 4)
          or err("$fn: unable to read entry descriptor: $!\n"), next FILE;
        my ($id, $offset, $length) = unpack("VVV", $buf);
        $id == 2 or $id == 9   # resource, finder
          or err("$fn: cannot handle entry ID $id\n"), next FILE;
        $offset + $length <= $file_size
          or err("$fn: entry at $offset exceeds file size\n"), next FILE;
        verbose("  Found entry ID $id at $offset, len $length\n");
        [ $id, $offset, $length ]
    } 1 .. $nentries;

    my $tmp_fh;
    unless($dry_run) {
        $tmp_fh = File::Temp->new( DIR => dirname($fn) );
        err("$fn: Unable to create temporary file\n"), next
          unless $tmp_fh;
    }

    # Write header back out, correctly
    $buf = pack("NNa16n", $magic, $version, $filler, $nentries);
    syswrite_maybe($tmp_fh, $buf) == length($buf)
      or err("$fn: failed to write header\n"), next FILE;

    # Write entry descriptors back out
    for (@descriptors) {
        $buf = pack("NNN", @$_);
        syswrite_maybe($tmp_fh, $buf) == length($buf)
          or err("$fn: failed to write entry\n"), next FILE;
    }

    # Write entries
    for (@descriptors) {
        my ($id, $offset, $length) = @$_;
        seek($fh, $offset, SEEK_SET)
          or err("$fn: failed to seek to $offset\n"), next FILE;
        sysread($fh, $buf, $length) == $length
          or err("$fn: failed to read entry: $!\n"), next FILE;
        # Assume unhandled $ids have been filtered out; default
        # operation below is to just copy the data.
        if ($id == 9) {       # Finder info
            my ($type, $creator, $flags, $loc_v, $loc_h, $fldr,  # FInfo
                $icon, $resv1, $resv2, $resv3, $script, $xflags, $comment, $putaway)
              = unpack('VVvvvv vvvvccvV', $buf);
            $buf = pack('NNnnnn nnnnccnN',
                        $type, $creator, $flags, $loc_v, $loc_h, $fldr,
                        $icon, $resv1, $resv2, $resv3, $script, $xflags, $comment, $putaway);
        }
        syswrite_maybe($tmp_fh, $buf) == length($buf)
          or err("$fn: failed to write entry: $!\n"), next FILE;
        verbose("  Rewrote entry ID $id\n");
    }

    unless ($dry_run) {
        my $tmp_fn = $tmp_fh->filename;
        close $tmp_fh or die "$_: failed to close\n";
        my ( $mode, $uid, $gid ) = (stat($fn))[2,4,5];
        $mode = $mode & 07777;
        chmod $mode, $tmp_fn
          or err("$_: unable to change mode on temp file\n");
        chown $uid, $gid, $tmp_fn
          or err("$_: unable to change ownership on temp file\n");
        rename $tmp_fn, "$fn"
          or err("$_: unable to atomically update: $!\n"), next FILE;
    }
    unless ($quiet) {
        $dry_run
          ? warn("$fn: would fix\n")
            : warn("$fn: fixed\n");
    }
    $fixed++;
}

warn(($dry_run ? "Dry-run finished." : "Finished.") .
     "  $seen files seen, $fixed fixed, " .
     "$skipped skipped, $nerrors errors.\n");

exit $nerrors;

# Finder flags in FInfo and FXInfo:
# Developer/SDKs/MacOSX10.4u.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/Finder.h
