#!/usr/bin/perl

# "A BEER-WARE LICENSE" (revision `dd if=/dev/random bs=1 count=2`)
# <peter@makholm.net> wrote this file. As long as you retain this notice
# you can do whatever you want with this stuff. If we meet some day, and
# you think this stuff is worth it, you can buy me a beer in return.
#                                                          Peter Makholm 
#
# $MD5: bbf141081b419c7942e975328e7326b4$

use strict;
use warnings;

my $VERSION = '1';

use IO::Handle;
use IO::File;
use File::Basename qw( basename dirname );
use File::Temp qw( tempfile );

my ($add,$check) = (0,0);
my $digest;
use Getopt::Long;
GetOptions('add'      => \$add,
           'check'    => \$check,
           'digest=s' => \$digest,
           'help'     => \&usage,
           'version'  => \&version,
          );

$digest ||= "MD5";
use Digest;
my $ctx = Digest->new("$digest");

usage() if ($add == $check);

if ($add) {
    for (@ARGV) {
        my $filename = $_;
        unless (-f $filename) {
            warn "$filename is not a plain file";
            next;
        }

        my $mode = (stat($filename))[2];

        my $dirname =  dirname $filename;
        my ($out, $outname) = tempfile ( DIR => $dirname );
        my $in = IO::File->new($filename, "r") || die "oops";
        add($in,$out);
        $in->close;
        $out->close;

        chmod $mode & 07777, $outname;
        unlink $filename || do { warn "Couldn't unlink $filename"; next };
        rename $outname, $filename || do { warn "Couldn't rename $outname to $filename" };

    }
    unless (@ARGV) {
        my $in  = IO::Handle->new_from_fd(fileno(STDIN),"r");
        my $out = IO::Handle->new_from_fd(fileno(STDOUT),"w");
        add($in,$out);
    }
}

if ($check) {
    for (@ARGV) {
        unless (-r $_) {
            warn "Couldn't read $_";
            next;
        }
        my $in = IO::File->new($_, "r");
        print "Checking $_ "; 
        print check($in) ? "Ok\n" : "Not ok\n";
    }
    unless (@ARGV) {
        my $in = IO::Handle->new_from_fd(fileno(STDIN),"r");
        print check($in) ? "Ok\n" : "Not ok\n";
    }
}

sub add {
    my ($in,$out) = @_;

    my $twopass = $in->isa('IO::Seekable');
    my @lines;

    $ctx->reset;
    while(<$in>) {
        s/\$$digest: ([^\$]*)\$/\$$digest: \$/g;
        $ctx->add($_);
        push @lines, $_ unless $twopass;
    }
    my $sum = $ctx->hexdigest;

    if ($twopass) {
        $in->seek(0,0);
        while(<$in>) {
            s/\$$digest: ([^\$]*)\$/\$$digest: $sum\$/g;
            print $out $_;
        }
    } else {
        for (@lines) {
            s/\$$digest: ([^\$]*)\$/\$$digest: $sum\$/g;
            print $out $_;
        }
    }
}

sub check {
    my ($in) = @_;

    my $sum;
    my @sums;

    $ctx->reset;
    while (<$in>) {
        if (@sums = /\$$digest: ([^\$]*)\$/) {
            s/\$$digest: ([^\$]*)\$/\$$digest: \$/g;
            for (@sums) {
                if (defined($sum) && $_ ne "" && $sum ne $_) {
                    warn "mismatching hashes";
                    return 0;
                }
                $sum = $_;
            }
        }
        $ctx->add($_);
    }
    return $sum eq $ctx->hexdigest;
}

sub usage { 
    print <<"STOP";
To add a sum to a file:
    $0 -a [ --digest <digest> ] FILE ...

To check a file:
    $0 -c [ --digest <digest> ] FILE ...

<digest> can be any digest known by the perl module Digest (default: MD5)
STOP

    exit 0;
}

sub version { 
    print "$0 version $VERSION\n";
    exit 0;
 } 
