changeset 12:5e2c9123f93f

Implemented digest verification: -c or --check option
author Franz Glasner <fzglas.hg@dom66.de>
date Fri, 04 Dec 2020 17:37:56 +0100
parents 15c3416d3677
children db64e282b049
files shasum.py
diffstat 1 files changed, 105 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/shasum.py	Fri Dec 04 16:13:02 2020 +0100
+++ b/shasum.py	Fri Dec 04 17:37:56 2020 +0100
@@ -14,6 +14,8 @@
 
 import argparse
 import hashlib
+import io
+import re
 import sys
 
 
@@ -36,6 +38,9 @@
         "--bsd", "-B", action="store_true", dest="bsd", default=False,
         help="write BSD style output; also :command:`openssl dgst` style")
     aparser.add_argument(
+        "--check", "-c", action="store_true",
+        help="read digests from FILEs and check them")
+    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(
@@ -58,7 +63,10 @@
 
     if not opts.files:
         opts.files.append('-')
-    generate_digests(opts)
+    if opts.check:
+        return verify_digests_from_files(opts)
+    else:
+        return generate_digests(opts)
 
 
 def generate_digests(opts):
@@ -87,11 +95,68 @@
                     fn,
                     opts.algorithm[1],
                     True)
+    return 0
+
+
+def verify_digests_from_files(opts):
+    exit_code = 0
+    if len(opts.files) == 1 and opts.files[0] == '-':
+        for checkline in sys.stdin:
+            if not checkline:
+                continue
+            r, fn = handle_checkline(opts, checkline)
+            print("{}: {}".format(fn, r.upper()))
+            if r != "ok" and exit_code == 0:
+                exit_code = 1
+    else:
+        for fn in opts.files:
+            with io.open(fn, "rt", encoding="utf-8") as checkfile:
+                for checkline in checkfile:
+                    if not checkline:
+                        continue
+                    r, fn = handle_checkline(opts, checkline)
+                    print("{}: {}".format(fn, r.upper()))
+                    if r != "ok" and exit_code == 0:
+                        exit_code = 1
+    return exit_code
+
+
+def handle_checkline(opts, line):
+    """
+    :return: a tuple with static "ok", "missing", or "failed" and the filename
+    :rtype: tuple(str, str)
+
+    """
+    # determine checkfile format (BSD or coreutils)
+    # BSD?
+    mo = re.search(r"\A(\S+)\s*\((.*)\)\s*=\s*(.+)\n?\Z", line)
+    if mo:
+        algo = algotag2algotype(mo.group(1))
+        fn = mo.group(2)
+        digest = mo.group(3)
+    else:
+        mo = re.search(r"([^\ ]+) [\*\ ]?(.+)\n?\Z", line)
+        if mo:
+            algo = opts.algorithm[0]
+            fn = mo.group(2)
+            digest = mo.group(1)
+        else:
+            raise ValueError(
+                "improperly formatted digest line: {}".format(line))
+    try:
+        with open(fn, "rb") as input:
+            d = compute_digest(algo, input)
+            if d.lower() == digest.lower():
+                return ("ok", fn)
+            else:
+                return ("failed", fn)
+    except EnvironmentError:
+        return ("missing", fn)
 
 
 def argv2algo(s):
     """Convert a commane line algorithm specifier into a tuple with the
-    type/factory of the digest and the algorithms tag for output purposes
+    type/factory of the digest and the algorithms tag for output purposes.
 
     :param str s: the specifier from the commane line
     :return: the internal digest specification
@@ -130,6 +195,44 @@
             "`{}' is not a recognized algorithm".format(s))
 
 
+def algotag2algotype(s):
+    """Convert the algorithm specifier in a BSD-style digest file to the
+    type/factory of the corresponding algorithm.
+
+    :param str s: the tag (i.e. normalized name) or the algorithm
+    :return: the digest type or factory for `s`
+
+    All string comparisons are case-sensitive.
+
+    """
+    if s == "SHA1":
+        return hashlib.sha1
+    elif s == "SHA224":
+        return hashlib.sha224
+    elif s == "SHA256":
+        return hashlib.sha256
+    elif s == "SHA384":
+        return hashlib.sha384
+    elif s == "SHA512":
+        return hashlib.sha512
+    elif s == "SHA3-224":
+        return hashlib.sha3_224
+    elif s == "SHA3-256":
+        return hashlib.sha3_256
+    elif s == "SHA3-384":
+        return hashlib.sha3_384
+    elif s == "SHA3-512":
+        return hashlib.sha3_512
+    elif s == "BLAKE2b":
+        return hashlib.blake2b
+    elif s == "BLAKE2s":
+        return hashlib.blake2s
+    elif s == "MD5":
+        return hashlib.md5
+    else:
+        raise ValueError("unknown algorithm: {}".format(s))
+
+
 def out_bsd(dest, digest, filename, digestname, binary):
     """BSD format output, also :command:`openssl dgst` and
     :command:`b2sum --tag" format output