comparison shasum.py @ 51:58d5a0b6e5b3

Implement the OpenBSD variant (with --base64) to encode digests in base64, not hexadecimal
author Franz Glasner <f.glasner@feldmann-mg.com>
date Wed, 26 Jan 2022 14:15:43 +0100
parents 5bec7a5d894a
children 5935055edea6
comparison
equal deleted inserted replaced
50:cc3d6fe7eda7 51:58d5a0b6e5b3
21 __revision__ = "|VCSRevision|" 21 __revision__ = "|VCSRevision|"
22 __date__ = "|VCSJustDate|" 22 __date__ = "|VCSJustDate|"
23 23
24 24
25 import argparse 25 import argparse
26 import base64
27 import binascii
26 import hashlib 28 import hashlib
27 import io 29 import io
28 try: 30 try:
29 import mmap 31 import mmap
30 except ImportError: 32 except ImportError:
47 fromfile_prefix_chars='@') 49 fromfile_prefix_chars='@')
48 aparser.add_argument( 50 aparser.add_argument(
49 "--algorithm", "-a", action="store", type=argv2algo, 51 "--algorithm", "-a", action="store", type=argv2algo,
50 help="1 (default), 224, 256, 384, 512, 3-224, 3-256, 3-384, 3-512, blake2b, blake2s, md5") 52 help="1 (default), 224, 256, 384, 512, 3-224, 3-256, 3-384, 3-512, blake2b, blake2s, md5")
51 aparser.add_argument( 53 aparser.add_argument(
54 "--base64", action="store_true",
55 help="output checksums in base64 notation, not hexadecimal (OpenBSD).")
56 aparser.add_argument(
52 "--binary", "-b", action="store_false", dest="text_mode", default=False, 57 "--binary", "-b", action="store_false", dest="text_mode", default=False,
53 help="read in binary mode (default)") 58 help="read in binary mode (default)")
54 aparser.add_argument( 59 aparser.add_argument(
55 "--bsd", "-B", action="store_true", dest="bsd", default=False, 60 "--bsd", "-B", action="store_true", dest="bsd", default=False,
56 help="Write BSD style output. This is also the default output format of :command:`openssl dgst`.") 61 help="Write BSD style output. This is also the default output format of :command:`openssl dgst`.")
101 106
102 return shasum(opts) 107 return shasum(opts)
103 108
104 109
105 def gen_opts(files=[], algorithm="SHA1", bsd=False, text_mode=False, 110 def gen_opts(files=[], algorithm="SHA1", bsd=False, text_mode=False,
106 checklist=False, check=False, dest=None): 111 checklist=False, check=False, dest=None, base64=False):
107 if text_mode: 112 if text_mode:
108 raise ValueError("text mode not supported") 113 raise ValueError("text mode not supported")
109 if checklist and check: 114 if checklist and check:
110 raise ValueError("only one of `checklist' or `check' is allowed") 115 raise ValueError("only one of `checklist' or `check' is allowed")
111 opts = argparse.Namespace(files=files, 116 opts = argparse.Namespace(files=files,
113 algorithm), 118 algorithm),
114 bsd=bsd, 119 bsd=bsd,
115 checklist=checklist, 120 checklist=checklist,
116 check=check, 121 check=check,
117 text_mode=False, 122 text_mode=False,
118 dest=dest) 123 dest=dest,
124 base64=base64)
119 return opts 125 return opts
120 126
121 127
122 def shasum(opts): 128 def shasum(opts):
123 if opts.check: 129 if opts.check:
143 source = sys.stdin.buffer 149 source = sys.stdin.buffer
144 out(sys.stdout, 150 out(sys.stdout,
145 compute_digest_stream(opts.algorithm[0], source), 151 compute_digest_stream(opts.algorithm[0], source),
146 None, 152 None,
147 opts.algorithm[1], 153 opts.algorithm[1],
148 True) 154 True,
155 opts.base64)
149 else: 156 else:
150 for fn in opts.files: 157 for fn in opts.files:
151 out(opts.dest or sys.stdout, 158 out(opts.dest or sys.stdout,
152 compute_digest_file(opts.algorithm[0], fn), 159 compute_digest_file(opts.algorithm[0], fn),
153 fn, 160 fn,
154 opts.algorithm[1], 161 opts.algorithm[1],
155 True) 162 True,
163 opts.base64)
156 return 0 164 return 0
165
166
167 def compare_digests_equal(given_digest, expected_digest, algo):
168 """Compare a newly computed binary digest `given_digest` with a digest
169 string (hex or base64) in `expected_digest`.
170
171 :param bytes given_digest:
172 :param str expected_digest: hexlified or base64 encoded digest
173 :param algo: The algorithm (factory)
174 :return: `True` if the digests are equal, `False` if not
175 :rtype: bool
176
177 """
178 if len(expected_digest) == algo().digest_size * 2:
179 # hex
180 try:
181 exd = binascii.unhexlify(expected_digest)
182 except TypeError:
183 return False
184 else:
185 # base64
186 try:
187 exd = base64.b64decode(expected_digest)
188 except TypeError:
189 return False
190 return given_digest == exd
157 191
158 192
159 def verify_digests_with_checklist(opts): 193 def verify_digests_with_checklist(opts):
160 dest = opts.dest or sys.stdout 194 dest = opts.dest or sys.stdout
161 exit_code = 0 195 exit_code = 0
172 exit_code = 1 206 exit_code = 1
173 print("-: MISSING", file=dest) 207 print("-: MISSING", file=dest)
174 else: 208 else:
175 tag, algo, cl_filename, cl_digest = pl 209 tag, algo, cl_filename, cl_digest = pl
176 computed_digest = compute_digest_stream(algo, source) 210 computed_digest = compute_digest_stream(algo, source)
177 if cl_digest.lower() == computed_digest.lower(): 211 if compare_digests_equal(computed_digest, cl_digest, algo):
178 res = "OK" 212 res = "OK"
179 else: 213 else:
180 res = "FAILED" 214 res = "FAILED"
181 exit_code = 1 215 exit_code = 1
182 print("{}: {}: {}".format(tag, "-", res), file=dest) 216 print("{}: {}: {}".format(tag, "-", res), file=dest)
187 print("{}: MISSING".format(fn), file=dest) 221 print("{}: MISSING".format(fn), file=dest)
188 exit_code = 1 222 exit_code = 1
189 else: 223 else:
190 tag, algo, cl_filename, cl_digest = pl 224 tag, algo, cl_filename, cl_digest = pl
191 computed_digest = compute_digest_file(algo, fn) 225 computed_digest = compute_digest_file(algo, fn)
192 if cl_digest.lower() == computed_digest.lower(): 226 if compare_digests_equal(computed_digest, cl_digest, algo):
193 res = "OK" 227 res = "OK"
194 else: 228 else:
195 exit_code = 1 229 exit_code = 1
196 res = "FAILED" 230 res = "FAILED"
197 print("{}: {}: {}".format(tag, fn, res), file=dest) 231 print("{}: {}: {}".format(tag, fn, res), file=dest)
234 raise ValueError( 268 raise ValueError(
235 "improperly formatted digest line: {}".format(line)) 269 "improperly formatted digest line: {}".format(line))
236 tag, algo, fn, digest = parts 270 tag, algo, fn, digest = parts
237 try: 271 try:
238 d = compute_digest_file(algo, fn) 272 d = compute_digest_file(algo, fn)
239 if d.lower() == digest.lower(): 273 if compare_digests_equal(d, digest, algo):
240 return ("ok", fn, tag) 274 return ("ok", fn, tag)
241 else: 275 else:
242 return ("failed", fn, tag) 276 return ("failed", fn, tag)
243 except EnvironmentError: 277 except EnvironmentError:
244 return ("missing", fn, tag) 278 return ("missing", fn, tag)
375 return hashlib.md5 409 return hashlib.md5
376 else: 410 else:
377 raise ValueError("unknown algorithm: {}".format(s)) 411 raise ValueError("unknown algorithm: {}".format(s))
378 412
379 413
380 def out_bsd(dest, digest, filename, digestname, binary): 414 def out_bsd(dest, digest, filename, digestname, binary, use_base64):
381 """BSD format output, also :command:`openssl dgst` and 415 """BSD format output, also :command:`openssl dgst` and
382 :command:`b2sum --tag" format output 416 :command:`b2sum --tag" format output
383 417
384 """ 418 """
419 if use_base64:
420 digest = base64.b64encode(digest).decode("ascii")
421 else:
422 digest = binascii.hexlify(digest).decode("ascii")
385 if filename is None: 423 if filename is None:
386 print(digest, file=dest) 424 print(digest, file=dest)
387 else: 425 else:
388 print("{} ({}) = {}".format(digestname, 426 print("{} ({}) = {}".format(digestname,
389 normalize_filename(filename), 427 normalize_filename(filename),
390 digest), 428 digest),
391 file=dest) 429 file=dest)
392 430
393 431
394 def out_std(dest, digest, filename, digestname, binary): 432 def out_std(dest, digest, filename, digestname, binary, use_base64):
395 """Coreutils format (:command:`shasum` et al.) 433 """Coreutils format (:command:`shasum` et al.)
396 434
397 """ 435 """
436 if use_base64:
437 digest = base64.b64encode(digest).decode("ascii")
438 else:
439 digest = binascii.hexlify(digest).decode("ascii")
398 print("{} {}{}".format( 440 print("{} {}{}".format(
399 digest, 441 digest,
400 '*' if binary else ' ', 442 '*' if binary else ' ',
401 '-' if filename is None else normalize_filename(filename)), 443 '-' if filename is None else normalize_filename(filename)),
402 file=dest) 444 file=dest)
452 mapoffset += mapsize 494 mapoffset += mapsize
453 if rest < mapsize: 495 if rest < mapsize:
454 mapsize = rest 496 mapsize = rest
455 finally: 497 finally:
456 os.close(fd) 498 os.close(fd)
457 return h.hexdigest() 499 return h.digest()
458 500
459 501
460 def compute_digest_stream(hashobj, instream): 502 def compute_digest_stream(hashobj, instream):
461 """ 503 """
462 504
471 buf = instream.read(CHUNK_SIZE) 513 buf = instream.read(CHUNK_SIZE)
472 if buf is not None: 514 if buf is not None:
473 if len(buf) == 0: 515 if len(buf) == 0:
474 break 516 break
475 h.update(buf) 517 h.update(buf)
476 return h.hexdigest() 518 return h.digest()
477 519
478 520
479 def normalize_filename(filename, strip_leading_dot_slash=False): 521 def normalize_filename(filename, strip_leading_dot_slash=False):
480 filename = filename.replace("\\", "/") 522 filename = filename.replace("\\", "/")
481 if strip_leading_dot_slash: 523 if strip_leading_dot_slash: