#!/usr/bin/perl
$^W=1;
use strict;

use Digest::MD5;
use Digest::SHA;

my $my_exit=0; # exit value

# digests this program knows about
my @known_digests=qw(MD5 SHA-1 SHA-224 SHA-256 SHA-384 SHA-512);

# digests we'll use by default
my @default_digests=qw(MD5 SHA-1 SHA-256 SHA-512);

# block size (in bytes) we'll use (may be changed later)
my $bs=4096; # 4 KiB

# digests we'll use (to be set a bit later)
my @digests=();

# program basename
my $prog=$0;
$prog =~ s;^.*/;;o;

my $digest_help=<<""
algorithms to use, by default we use these:

;
$digest_help.=join(' ',@default_digests);
$digest_help.="\n";
$digest_help.=<<""
algorithm(s) used may be specified by options below,
specifying any of these overrides use of the default algorithm(s):

;
for (@known_digests){
	my $H=$_;
	$_=lc;
	my $h="--$_ --${_}sum";
	$h.=" --$_ --${_}sum" if s/-//go;
	$digest_help.="\t$h - $H\n";
};
$digest_help.="\t--all --allsum --allsums - use all of the above algorithms";
$digest_help =~ s/^/\t/gmo;

my $helptext=<<""
$prog [options] [file ...]
	Options are case insensitive, except as noted
	--help - print this help text and exit, overrides other options
	--blocksize blocksize
	--blocksize=blocksize
	--bs blocksize
	--bs=blocksize
		set input read blocksize to blocksize, default units in bytes,
		default blocksize is $bs bytes, the following suffix multipliers
		may be used, those ending in iB are case sensitive:
		s     - 512 byte "blocks" (sectors)
		k KiB - 2*s
		m MiB - 1024 KiB
		g GiB - 1024 MiB
		t TiB - 1024 GiB
		p PiB - 1024 TiB
		e EiB - 1024 PiB
		z ZiB - 1024 EiB
		y YiB - 1024 ZiB
$digest_help

;

# detabify
$helptext =~ s/\t/    /go;

my %digests=();	# digest(s) requested via option(s)

# we use our own custom option processing to be more flexible
OPTIONS:
while(@ARGV && ($_=$ARGV[0]) && /^-/o){
	shift(@ARGV);
	last if $_ eq '--';
	if(/^--help$/io){
		print($helptext);
		exit 0;
	}elsif(/^--(?:bs|blocksize)(?:=(.+))?$/io){
		if(defined($1)){
			$bs=$1;
		}else{
			die(
				"$prog: missing blocksize, aborting\n",
				"usage: $helptext",
			) if ! @ARGV;
			$bs=shift(@ARGV);
		};
		if($bs =~ /^(\d+)((?i)[skmgtpezy](?-i)|[KMGTPEZY]iB)?$/o){
			my $n=$1;
			$n =~ s/^0+(\d)/\1/o;
			$n+=0;
			die(
				"$prog: bad blocksize specifier: $bs < 1, aborting\n",
				"usage: $helptext",
			) if $n < 1;
			if(defined($2)){
				local $_=$2;
				if(/^s/io){$bs=$n*512; # * 512 B ("sectors")
				}elsif(/^k/io){$bs=$n*1024; # *  1 KiB
				}elsif(/^m/io){$bs=$n*1048576; # *  1 MiB
				}elsif(/^g/io){$bs=$n*1073741824; # *  1 GiB
				}elsif(/^t/io){$bs=$n*1099511627776; # *  1 TiB
				}elsif(/^p/io){$bs=$n*1125899906842624; # *  1 PiB
				}elsif(/^e/io){$bs=$n*1152921504606846976; # *  1 EiB
				}elsif(/^z/io){$bs=$n*1180591620717411303424; # *  1 ZiB
				}elsif(/^y/io){$bs=$n*1208925819614629174706176; # *  1 YiB
				}else{
					die("$prog: internal program error processing blocksize: $bs, aborting");
				};
			}else{
				$bs=$n;
			};
		}else{
			die(
				"$prog: bad blocksize specifier: $bs, aborting\n",
				"usage: $helptext",
			);
		};
	}elsif(/^--all(?:sums?)?$/oi){
		for(@known_digests){
			$digests{$_}=undef;
		};
	}elsif(/^--(.+?)(?:sum)?$/oi){
		my $a=$1;
		$a=lc($a);
		$a =~ s/-//go;
		for my $k (@known_digests){
			local $_=lc($k);
			s/-//go;
			if($a eq $_){
				$digests{$k}=undef;
				next OPTIONS;
			};
		};
		die(
			"$prog: unknown option: $_, aborting\n",
			"usage: $helptext",
		);
	}else{
		die(
			"$prog: unknown option: $_, aborting\n",
			"usage: $helptext",
		);
	};
};

if(%digests){
	for(@known_digests){
		push(@digests,$_) if exists($digests{$_})
	};
}else{
	@digests=@default_digests;
};

my @files=@ARGV;
if (! @files){
	@files=('-');
};

# calculate sums/hashes on our already opened FILE $file
sub sumfile{
	my $file=$_[0];
	binmode(FILE);
	my @digest_sums=();
	for (@digests){
		if(/^sha-?(\d+)$/io){
			push(
					@digest_sums,
					Digest::SHA->new($1),
			);
		}elsif(/^md5$/io){
			push(
					@digest_sums,
					Digest::MD5->new,
			);
		}else{
			die("$prog: internal program error, unknown digest: $_, aborting");
		};
	};
	{
		local $/=\$bs;
		while(<FILE>){
			for my $sum (@digest_sums){
				$sum->add($_);
			};
		};
		for my $sum (@digest_sums){
			print(
				$sum->hexdigest,
				'  ',
				$file,
				"\n",
			);
		};
	};
};

if(@ARGV){
	for my $file (@ARGV){
		if(!open(FILE,'<',$file)){
			warn "$prog: failed to open file $file, skipping\n";
			$my_exit=1;
			next;
		};
		&sumfile($file);
		if(!close(FILE)){
			warn "$prog: failed to close file $file\n";
			$my_exit=1;
			next;
		};
	};
}else{
	*FILE=*STDIN;
	&sumfile('-');
};

exit($my_exit);
