#!/usr/bin/env perl

##
## Copyright 2010
## The Regents of the University of California
## All Rights Reserved
##
## Permission to use, copy, modify and distribute any part of this
## software package for educational, research and non-profit
## purposes, without fee, and without a written agreement is hereby
## granted, provided that the above copyright notice, this paragraph
## and the following paragraphs appear in all copies.
##
## IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY
## PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL
## DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS
## SOFTWARE, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF
## THE POSSIBILITY OF SUCH DAMAGE.
##
## THE SOFTWARE PROVIDED HEREIN IS ON AN "AS IS" BASIS, AND THE
## UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE,
## SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. THE UNIVERSITY
## OF CALIFORNIA MAKES NO REPRESENTATIONS AND EXTENDS NO WARRANTIES
## OF ANY KIND, EITHER IMPLIED OR EXPRESS, INCLUDING, BUT NOT LIMITED
## TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A
## PARTICULAR PURPOSE, OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE
## ANY PATENT, TRADEMARK OR OTHER RIGHTS.
##
##

=head1 PROGRAM:

md5check.pl

=head2 PURPOSE:

In many directories checksums for all files are stored either in a
single file with name md5.md5, or for each file <f> a file
<f>.md5 exists containing the checksum for <f>.
This script confirms file integrity by recalculating md5 checksums
for a list of files and comparing them against the checksums stored
alongside the files.

=head2 CALLING SEQUENCE:

 ls *.dag.gz | md5check.pl [-vac]
 md5check.pl [-vac] <list_file>
 ls *.dag.gz | md5check.pl -p
 echo file.dag.gz 9a2785cfca09eb8fc1e0e3cd586f2b09 | md5check.pl -us

=head2 INPUTS:

<list_file>
        file containing the names of the files to be checked

Alternatively file names are piped to stdin (fourth calling
sequence above).

Note that it is possible to explicitly specify an md5 checksum
alongside the file name (i.e. in the same format as stored
in the md5.md5 files). This checksum should represent the
actual (computed) checksum for the file, and bypasses the
checksum computation in this script.

=head2 OPTIONAL INPUT:

-v      by default messages are printed only for files that
        fail the checksum test. Set -v to get a message for
        every file processed.

-a      if set, then for files for which no checksum is stored
        yet, the checksum is added to md5.md5 (in the same
        directory).

-u      if set, then existing checksum entries are updated
        if they are wrong, and non-existing ones are added.

-c      if -a or -u is set, then an attempt is made to create md5.md5
        if it does not exist already.
        Ignored if neither -a nor -u are set.

-r      if set then the checksum entry for specified files
        is removed from the md5.md5 file

-P      print checksum and filename
-p      print checksum only
        The first checksum found for each file
        is printed, either from a checksum file md5.md5, or
        else by calcalating the checksum.
-n      do not print anything

-C      computes checksum from file content
        -A and -C are ignored unless -p or -P is set

-A      if one of -P,-p or -n is set, this will only check
        for a checksum stored in md5.md5 (i.e. no checksum
        is computed if it is not stored in md5.md5).
        if none of -P, -p or -n are set, this will skip
        all files for which there is a checksum available
        already (in combination with -a this provides a
        quick way to add missing checksums).

-s      if set, then both filename and checksum need to be
        specified in format: <filename> <checksum>

=head2 RESTRICTIONS:

This script has been tested on Linux, FreeBSD and SuhOS.

If multiple checksum entries for a file are found in an md5.md5
file the program aborts. The only way out is to fix the md5.md5
manually (this isn't supposed to happen, you know.).

=head2 PROCEDURE:

Only regular, readable files are checked.
Files with extension .md5 are ignored.

Checksums are computed using the Digest::MD5 module.
If this is not available then a simple edit (see code neare line
"use Digest::MD5") can force the script to use instead an
external program. Currently it can use md5, openssl or md5sum.

=head2 MODIFICATION HISTORY:

 APR-2010, Paul Hick (UCSD/CAIDA)
 JUN-2010, Paul Hick (UCSD/CAIDA)
    Added -v and -a options
 SEP-2011, Paul Hick (UCSD/CAIDA; pphick@caida.org)
    Added option to provide a 'computed' checksum alongside
    filename.

=cut

use strict;
use warnings;

use FindBin qw($RealBin);
use lib "$FindBin::RealBin/../lib";
use tool_md5;
use Data::Dumper;

use constant {
	FALSE	=> 0,
	TRUE	=> 1,
};

#==================================================================
# If Digest::MD5 is not available then comment the following line,
# and set $have_digest = 0;
# If $have_digest = 0 then one of the utilities in @md5_cmds (the
# first one found in the path) will be used to compute checksums.

use Digest::MD5;
my $have_digest = 1;

my @md5_cmds = ("md5", "openssl md5","md5sum") unless $have_digest;
#==================================================================



#use FindBin;
#use lib "$FindBin::RealBin/../lib";
use File::Basename qw(dirname basename);

use Getopt::Std;
my %opts;
getopts('hvacnursPpAC',\%opts);

if ( $opts{h} ) {
	print `perldoc $0`;
	exit 0;
}

my $verbose		  = $opts{v};
my $add_md5		  = $opts{u} ? 2 : $opts{a} ? 1 : 0;	# -u implies -a
my $new_md5dotmd5 = $opts{c};
my $remove_md5	  = $opts{r};

my $get_md5			 = ($opts{p} or $opts{P} or $opts{n}) ? 1 : undef;
my $get_stored_md5   =      $get_md5  ? $opts{A} : undef;
my $skip_stored_md5  = (not $get_md5) ? $opts{A} : undef;
my $get_computed_md5 = $get_md5 ? $opts{C} : undef;

die "Make up your mind. Set -A or -C, not both\n" if $opts{A} and $opts{C};

my $md5util;		# Default: use Digest::MD5;

unless ( $have_digest ) {

	# Pick the first of @cmd5_cmds found

	foreach (@md5_cmds) {
		my $cmd = (split(/ /,$_))[0];
		next unless $cmd = `which $cmd`;
		chomp( $md5util = $_ );
		last;
	}

	# No utility found to compute checksums; abort

	unless ( $md5util ) {
		print STDERR "Do not know how to compute md5 checksums.\nLooked for the following utilities:\n";
		print STDERR "   $_\n" for (@md5_cmds);
		exit 1;
	}

	print "\nCalculating md5 checksums using '$md5util'\n\n" if $verbose;

}

# Get a list of files; keep regular, readable files only.
# Ignore .md5 files.
# Typically these are piped in from the ls command.

chomp( my @files = <> );

my %md5_substitute;
my $use_substitutes = FALSE;

# Not sure this is a good idea.
# If $opts{s} then filename and new checksum can be set as
#	<filename> <md5_new>

if ( $opts{s} ) {
	foreach (@files) {
		next unless /(.*)\s+(.*)/;
		next unless length($2) == 32;
		$md5_substitute{$1} = $2;
		$use_substitutes = TRUE;
		$_ = $1;
	}
}

@files = grep { -r and -f _ and ( $_ !~ /\.md5$/ or $get_md5 ) } @files unless $use_substitutes;
my $nbad = 0;

foreach my $f (@files) {

	if ( $get_md5 ) {

		my $md5_old = $get_computed_md5 ? undef : &tool_md5::get_stored_md5( $f );

		if ( $md5_old ) {

			print $opts{p} ? "$md5_old\n" : "$md5_old $f\n" unless $opts{n};

		} elsif ( $get_stored_md5 ) {

			print STDERR "no checksum stored for '$f'\n";
			$nbad++;

		} else {

			my $md5_new = &tool_md5::compute_md5( $f, $md5util );
			if ( $md5_new ) {
				print $opts{p} ? "$md5_new\n" : "$md5_new $f\n" unless $opts{n};
			} else {
				print STDERR "unable to compute checksum for '$f'\n";
				$nbad++;
			}
		}

	} elsif ( $remove_md5 ) {

		&tool_md5::remove_stored_md5( $f, $verbose );

	} else {

		unless ( $skip_stored_md5 and &tool_md5::get_stored_md5( $f ) ) {
			my $md5_new = $use_substitutes ? $md5_substitute{$f} : undef; 
			my $status = &tool_md5::check( $f, $md5_new, $add_md5, $new_md5dotmd5, $verbose, $md5util) ;
			$nbad += 1-$status;
		}

	}

}

exit $nbad;
