Mercurial > hgrepos > Python > apps > py-cutils
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: |
