changeset 22:6bdfc5ad4656

Implemented OpenBSD's -C (aka --checklist) option for shasum
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 05 Dec 2020 15:08:46 +0100
parents f2d634270e1c
children 232063b73e45
files shasum.py
diffstat 1 files changed, 77 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/shasum.py	Sat Dec 05 13:32:00 2020 +0100
+++ b/shasum.py	Sat Dec 05 15:08:46 2020 +0100
@@ -51,6 +51,12 @@
 the comparison. This will validate any of the supported checksums.
 If no file is given, stdin is used.""")
     aparser.add_argument(
+        "--checklist", "-C", metavar="CHECKLIST",
+        help="""Compare the checksum of each FILE against the checksums in
+the CHECKLIST. Any specified FILE that is not listed in the CHECKLIST will
+generate an error.""")
+
+    aparser.add_argument(
         "--reverse", "-r", action="store_false", dest="bsd", default=False,
         help="explicitely select normal coreutils style output (to be option compatible with BSD style commands and :command:`openssl dgst -r`)")
     aparser.add_argument(
@@ -68,13 +74,20 @@
 
     if opts.text_mode:
         print("ERROR: text mode not supported", file=sys.stderr)
-        sys.exit(78)   # :manpage:`sysexits(3)` EX_CONFIG
+        sys.exit(78)   # :manpage:`sysexits(3)`  EX_CONFIG
+
+    if opts.check and opts.checklist:
+        print("ERROR: only one of --check or --checklist allowed",
+              file=sys.stderr)
+        sys.exit(64)   # :manpage:`sysexits(3)`  EX_USAGE
 
     if not opts.algorithm:
         opts.algorithm = argv2algo("1")
 
     if opts.check:
         return verify_digests_from_files(opts)
+    elif opts.checklist:
+        return verify_digests_with_checklist(opts)
     else:
         return generate_digests(opts)
 
@@ -108,6 +121,48 @@
     return 0
 
 
+def verify_digests_with_checklist(opts):
+    exit_code = 0
+    if not opts.files or (len(opts.files) == 1 and opts.files[0] == '-'):
+        if PY2:
+            if sys.platform == "win32":
+                import os, msvcrt   # noqa: E401
+                msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
+            source = sys.stdin
+        else:
+            source = sys.stdin.buffer
+        pl = get_parsed_digest_line_from_checklist(opts.checklist, opts, None)
+        if pl is None:
+            exit_code = 1
+            print("-: MISSING")
+        else:
+            tag, algo, cl_filename, cl_digest = pl
+            computed_digest = compute_digest(algo, source)
+            if cl_digest.lower() == computed_digest.lower():
+                res = "OK"
+            else:
+                res = "FAILED"
+                exit_code = 1
+            print("{}: {}: {}".format(tag, "-", res))
+    else:
+        for fn in opts.files:
+            pl = get_parsed_digest_line_from_checklist(opts.checklist, opts, fn)
+            if pl is None:
+                print("{}: MISSING".format(fn))
+                exit_code = 1
+            else:
+                tag, algo, cl_filename, cl_digest = pl
+                with open(fn, "rb") as source:
+                    computed_digest = compute_digest(algo, source)
+                if cl_digest.lower() == computed_digest.lower():
+                    res = "OK"
+                else:
+                    exit_code = 1
+                    res = "FAILED"
+                print("{}: {}: {}".format(tag, fn, res))
+    return exit_code
+
+
 def verify_digests_from_files(opts):
     exit_code = 0
     if not opts.files or (len(opts.files) == 1 and opts.files[0] == '-'):
@@ -154,6 +209,27 @@
         return ("missing", fn, tag)
 
 
+def get_parsed_digest_line_from_checklist(checklist, opts, filename):
+    if filename is None:
+        filenames = ("-", "stdin", "", )
+    else:
+        filenames = (
+            normalize_filename(filename, strip_leading_dot_slash=True),)
+    with io.open(checklist, "rt", encoding="utf-8") as clf:
+        for checkline in clf:
+            if not checkline:
+                continue
+            parts = parse_digest_line(opts, checkline)
+            if not parts:
+                raise ValueError(
+                    "improperly formatted digest line: {}".format(checkline))
+            fn = normalize_filename(parts[2], strip_leading_dot_slash=True)
+            if fn in filenames:
+                return parts
+        else:
+            return None
+
+
 def parse_digest_line(opts, line):
     """Parse a `line` of a digest file and return its parts.