1#!perl
2
3	## shasum: filter for computing SHA digests (ref. sha1sum/md5sum)
4	##
5	## Copyright (C) 2003-2018 Mark Shelor, All Rights Reserved
6	##
7	## Version: 6.02
8	## Fri Apr 20 16:25:30 MST 2018
9
10	## shasum SYNOPSIS adapted from GNU Coreutils sha1sum. Add
11	## "-a" option for algorithm selection,
12	## "-U" option for Universal Newlines support, and
13	## "-0" option for reading bit strings.
14
15BEGIN { pop @INC if $INC[-1] eq '.' }
16
17use strict;
18use warnings;
19use Fcntl;
20use Getopt::Long;
21use Digest::SHA qw($errmsg);
22
23my $POD = <<'END_OF_POD';
24
25=head1 NAME
26
27shasum - Print or Check SHA Checksums
28
29=head1 SYNOPSIS
30
31 Usage: shasum [OPTION]... [FILE]...
32 Print or check SHA checksums.
33 With no FILE, or when FILE is -, read standard input.
34
35   -a, --algorithm   1 (default), 224, 256, 384, 512, 512224, 512256
36   -b, --binary      read in binary mode
37   -c, --check       read SHA sums from the FILEs and check them
38       --tag         create a BSD-style checksum
39   -t, --text        read in text mode (default)
40   -U, --UNIVERSAL   read in Universal Newlines mode
41                         produces same digest on Windows/Unix/Mac
42   -0, --01          read in BITS mode
43                         ASCII '0' interpreted as 0-bit,
44                         ASCII '1' interpreted as 1-bit,
45                         all other characters ignored
46
47 The following five options are useful only when verifying checksums:
48       --ignore-missing  don't fail or report status for missing files
49   -q, --quiet           don't print OK for each successfully verified file
50   -s, --status          don't output anything, status code shows success
51       --strict          exit non-zero for improperly formatted checksum lines
52   -w, --warn            warn about improperly formatted checksum lines
53
54   -h, --help        display this help and exit
55   -v, --version     output version information and exit
56
57 When verifying SHA-512/224 or SHA-512/256 checksums, indicate the
58 algorithm explicitly using the -a option, e.g.
59
60   shasum -a 512224 -c checksumfile
61
62 The sums are computed as described in FIPS PUB 180-4.  When checking,
63 the input should be a former output of this program.  The default
64 mode is to print a line with checksum, a character indicating type
65 (`*' for binary, ` ' for text, `U' for UNIVERSAL, `^' for BITS),
66 and name for each FILE.  The line starts with a `\' character if the
67 FILE name contains either newlines or backslashes, which are then
68 replaced by the two-character sequences `\n' and `\\' respectively.
69
70 Report shasum bugs to mshelor@cpan.org
71
72=head1 DESCRIPTION
73
74Running I<shasum> is often the quickest way to compute SHA message
75digests.  The user simply feeds data to the script through files or
76standard input, and then collects the results from standard output.
77
78The following command shows how to compute digests for typical inputs
79such as the NIST test vector "abc":
80
81	perl -e "print qq(abc)" | shasum
82
83Or, if you want to use SHA-256 instead of the default SHA-1, simply say:
84
85	perl -e "print qq(abc)" | shasum -a 256
86
87Since I<shasum> mimics the behavior of the combined GNU I<sha1sum>,
88I<sha224sum>, I<sha256sum>, I<sha384sum>, and I<sha512sum> programs,
89you can install this script as a convenient drop-in replacement.
90
91Unlike the GNU programs, I<shasum> encompasses the full SHA standard by
92allowing partial-byte inputs.  This is accomplished through the BITS
93option (I<-0>).  The following example computes the SHA-224 digest of
94the 7-bit message I<0001100>:
95
96	perl -e "print qq(0001100)" | shasum -0 -a 224
97
98=head1 AUTHOR
99
100Copyright (C) 2003-2018 Mark Shelor <mshelor@cpan.org>.
101
102=head1 SEE ALSO
103
104I<shasum> is implemented using the Perl module L<Digest::SHA>.
105
106=cut
107
108END_OF_POD
109
110my $VERSION = "6.02";
111
112sub usage {
113	my($err, $msg) = @_;
114
115	$msg = "" unless defined $msg;
116	if ($err) {
117		warn($msg . "Type shasum -h for help\n");
118		exit($err);
119	}
120	my($USAGE) = $POD =~ /SYNOPSIS(.+?)^=/sm;
121	$USAGE =~ s/^\s*//;
122	$USAGE =~ s/\s*$//;
123	$USAGE =~ s/^ //gm;
124	print $USAGE, "\n";
125	exit($err);
126}
127
128
129	## Sync stdout and stderr by forcing a flush after every write
130
131select((select(STDOUT), $| = 1)[0]);
132select((select(STDERR), $| = 1)[0]);
133
134
135	## Collect options from command line
136
137my ($alg, $binary, $check, $text, $status, $quiet, $warn, $help);
138my ($version, $BITS, $UNIVERSAL, $tag, $strict, $ignore_missing);
139
140eval { Getopt::Long::Configure ("bundling") };
141GetOptions(
142	'b|binary' => \$binary, 'c|check' => \$check,
143	't|text' => \$text, 'a|algorithm=i' => \$alg,
144	's|status' => \$status, 'w|warn' => \$warn,
145	'q|quiet' => \$quiet,
146	'h|help' => \$help, 'v|version' => \$version,
147	'0|01' => \$BITS,
148	'U|UNIVERSAL' => \$UNIVERSAL,
149	'tag' => \$tag,
150	'strict' => \$strict,
151	'ignore-missing' => \$ignore_missing,
152) or usage(1, "");
153
154
155	## Deal with help requests and incorrect uses
156
157usage(0)
158	if $help;
159usage(1, "shasum: Ambiguous file mode\n")
160	if scalar(grep {defined $_}
161		($binary, $text, $BITS, $UNIVERSAL)) > 1;
162usage(1, "shasum: --warn option used only when verifying checksums\n")
163	if $warn && !$check;
164usage(1, "shasum: --status option used only when verifying checksums\n")
165	if $status && !$check;
166usage(1, "shasum: --quiet option used only when verifying checksums\n")
167	if $quiet && !$check;
168usage(1, "shasum: --ignore-missing option used only when verifying checksums\n")
169	if $ignore_missing && !$check;
170usage(1, "shasum: --strict option used only when verifying checksums\n")
171	if $strict && !$check;
172usage(1, "shasum: --tag does not support --text mode\n")
173	if $tag && $text;
174usage(1, "shasum: --tag does not support Universal Newlines mode\n")
175	if $tag && $UNIVERSAL;
176usage(1, "shasum: --tag does not support BITS mode\n")
177	if $tag && $BITS;
178
179
180	## Default to SHA-1 unless overridden by command line option
181
182my %isAlg = map { $_ => 1 } (1, 224, 256, 384, 512, 512224, 512256);
183$alg = 1 unless defined $alg;
184usage(1, "shasum: Unrecognized algorithm\n") unless $isAlg{$alg};
185
186my %Tag = map { $_ => "SHA$_" } (1, 224, 256, 384, 512);
187$Tag{512224} = "SHA512/224";
188$Tag{512256} = "SHA512/256";
189
190
191	## Display version information if requested
192
193if ($version) {
194	print "$VERSION\n";
195	exit(0);
196}
197
198
199	## Try to figure out if the OS is DOS-like.  If it is,
200	## default to binary mode when reading files, unless
201	## explicitly overridden by command line "--text" or
202	## "--UNIVERSAL" options.
203
204my $isDOSish = ($^O =~ /^(MSWin\d\d|os2|dos|mint|cygwin)$/);
205if ($isDOSish) { $binary = 1 unless $text || $UNIVERSAL }
206
207my $modesym = $binary ? '*' : ($UNIVERSAL ? 'U' : ($BITS ? '^' : ' '));
208
209
210	## Read from STDIN (-) if no files listed on command line
211
212@ARGV = ("-") unless @ARGV;
213
214
215	## sumfile($file): computes SHA digest of $file
216
217sub sumfile {
218	my $file = shift;
219
220	my $mode = $binary ? 'b' : ($UNIVERSAL ? 'U' : ($BITS ? '0' : ''));
221	my $digest = eval { Digest::SHA->new($alg)->addfile($file, $mode) };
222	if ($@) { warn "shasum: $file: $errmsg\n"; return }
223	$digest->hexdigest;
224}
225
226
227	## %len2alg: maps hex digest length to SHA algorithm
228
229my %len2alg = (40 => 1, 56 => 224, 64 => 256, 96 => 384, 128 => 512);
230$len2alg{56} = 512224 if $alg == 512224;
231$len2alg{64} = 512256 if $alg == 512256;
232
233
234	## unescape: convert backslashed filename to plain filename
235
236sub unescape {
237	$_ = shift;
238	s/\\\\/\0/g;
239	s/\\n/\n/g;
240	s/\0/\\/g;
241	return $_;
242}
243
244
245	## verify: confirm the digest values in a checksum file
246
247sub verify {
248	my $checkfile = shift;
249	my ($err, $fmt_errs, $read_errs, $match_errs) = (0, 0, 0, 0);
250	my ($num_fmt_OK, $num_OK) = (0, 0);
251	my ($bslash, $sum, $fname, $rsp, $digest, $isOK);
252
253	local *FH;
254	$checkfile eq '-' and open(FH, '< -')
255		and $checkfile = 'standard input'
256	or sysopen(FH, $checkfile, O_RDONLY)
257		or die "shasum: $checkfile: $!\n";
258	while (<FH>) {
259		next if /^#/;
260		if (/^[ \t]*\\?SHA/) {
261			$modesym = '*';
262			($bslash, $alg, $fname, $sum) =
263			/^[ \t]*(\\?)SHA(\S+) \((.+)\) = ([\da-fA-F]+)/;
264			$alg =~ tr{/}{}d if defined $alg;
265		}
266		else {
267			($bslash, $sum, $modesym, $fname) =
268			/^[ \t]*(\\?)([\da-fA-F]+)[ \t]([ *^U])(.+)/;
269			$alg = defined $sum ? $len2alg{length($sum)} : undef;
270		}
271		if (grep { ! defined $_ } ($alg, $sum, $modesym, $fname) or
272			! $isAlg{$alg}) {
273			warn("shasum: $checkfile: $.: improperly " .
274				"formatted SHA checksum line\n") if $warn;
275			$fmt_errs++;
276			$err = 1 if $strict;
277			next;
278		}
279		$num_fmt_OK++;
280		$fname = unescape($fname) if $bslash;
281		next if $ignore_missing && ! -e $fname;
282		$rsp = "$fname: ";
283		($binary, $text, $UNIVERSAL, $BITS) =
284			map { $_ eq $modesym } ('*', ' ', 'U', '^');
285		$isOK = 0;
286		unless ($digest = sumfile($fname)) {
287			$rsp .= "FAILED open or read\n";
288			$err = 1; $read_errs++;
289		}
290		elsif (lc($sum) eq $digest) {
291			$rsp .= "OK\n";
292			$isOK = 1;
293			$num_OK++;
294		}
295		else { $rsp .= "FAILED\n"; $err = 1; $match_errs++ }
296		print $rsp unless ($status || ($quiet && $isOK));
297	}
298	close(FH);
299	if (! $num_fmt_OK) {
300		warn("shasum: $checkfile: no properly formatted " .
301			"SHA checksum lines found\n");
302		$err = 1;
303	}
304	elsif (! $status) {
305		warn("shasum: WARNING: $fmt_errs line" . ($fmt_errs>1?
306		's are':' is') . " improperly formatted\n") if $fmt_errs;
307		warn("shasum: WARNING: $read_errs listed file" .
308		($read_errs>1?'s':'') . " could not be read\n") if $read_errs;
309		warn("shasum: WARNING: $match_errs computed checksum" .
310		($match_errs>1?'s':'') . " did NOT match\n") if $match_errs;
311	}
312	if ($ignore_missing && ! $num_OK && $num_fmt_OK) {
313		warn("shasum: $checkfile: no file was verified\n")
314			unless $status;
315		$err = 1;
316	}
317	return($err == 0);
318}
319
320
321	## Verify or compute SHA checksums of requested files
322
323my($file, $digest);
324my $STATUS = 0;
325for $file (@ARGV) {
326	if ($check) { $STATUS = 1 unless verify($file) }
327	elsif ($digest = sumfile($file)) {
328		if ($file =~ /[\n\\]/) {
329			$file =~ s/\\/\\\\/g; $file =~ s/\n/\\n/g;
330			print "\\";
331		}
332		unless ($tag) { print "$digest $modesym$file\n" }
333		else          { print "$Tag{$alg} ($file) = $digest\n" }
334	}
335	else { $STATUS = 1 }
336}
337exit($STATUS);
338