# SPDX-FileCopyrightText: 2026 Jeff Epler # # SPDX-License-Identifier: GPL-3.0-only import io import binascii import getopt import hashlib import queue import sys import typing import collections.abc import shlex import threading try: use_concurrency: bool = not sys._is_gil_enabled() except AttributeError: use_concurrency = False chunksize = 262144 class Hashlike(typing.Protocol): @property def name(self) -> str: ... def __init__(self) -> None: ... def update(self, data: collections.abc.Buffer, /) -> None: ... def hexdigest(self) -> str: ... class Crc32: name = "crc32" def __init__(self) -> None: self.value = binascii.crc32(b"") def update(self, b: collections.abc.Buffer) -> None: self.value = binascii.crc32(b, self.value) def hexdigest(self) -> str: return f"{self.value:08x}" def chunked(f: io.RawIOBase, chunksize: int) -> typing.Iterator[bytes]: while chunk := f.read(chunksize): yield chunk def hash_new(name: str) -> Hashlike: if name == "crc32": return Crc32() return hashlib.new(name) def hash_list(): algorithms_guaranteed = hashlib.algorithms_guaranteed | {"crc32"} algorithms_available = hashlib.algorithms_available | {"crc32"} print("Hashes guaranteed available:", " ".join(sorted(algorithms_guaranteed))) print( "Additional hashes available:", " ".join(sorted(algorithms_available - algorithms_guaranteed)), ) def opener(filename): if filename == "-": return sys.stdin.buffer return open(filename, "rb") def hash_do(h: Hashlike, q: queue.Queue) -> None: while chunk := q.get(): h.update(chunk) def multihash(filename, hashes): hashers = [hash_new(h) for h in hashes] with opener(filename) as f: if use_concurrency: queues = [queue.Queue(3) for _ in hashers] threads = [ threading.Thread(target=hash_do, args=args) for args in zip(hashers, queues) ] for t in threads: t.start() for chunk in chunked(f, chunksize): for q in queues: q.put(chunk) for q in queues: q.put(None) for t in threads: t.join() else: for chunk in chunked(f, chunksize): for h in hashers: h.update(chunk) return [h.hexdigest() for h in hashers] def print_header(hashes): for h in hashes: hashlen = len(hash_new(h).hexdigest()) print(f"{h:{hashlen}} ", end="") print("", "filename") def check(fn: str, quiet: bool) -> bool: success = True with open(fn, "r") as f: header = f.readline() header_words = header.split() if not header_words or header_words[-1] != "filename": print(f"{fn}:1: Bad header", file=sys.stderr) return False hashes = header_words[:-1] for i, row in enumerate(f, start=2): words = row.split(None, maxsplit=1 + len(hashes)) if len(words) != len(header_words): print("{fn}:{i}: Bad syntax", file=sys.stderr) success = False continue parsed = shlex.split(words[-1]) if len(parsed) != 1: print("{fn}:{i}: Bad syntax (filename)", file=sys.stderr) success = False continue filename = parsed[0] expected = words[:-1] try: hashed = multihash(filename, hashes) except Exception as e: print(f"{fn}:{i}: Failed to hash: {e}", file=sys.stderr) success = False continue file_success = True for hashname, want, got in zip(hashes, expected, hashed): if want != got: print( f"{fn}:{i}: {hashname}: Expected {want}, got {got}", file=sys.stderr, ) success = False file_success = False if file_success and not quiet: print(f"{fn}:{i}: {filename}: All hashes match") return success def main(): opts, files = getopt.getopt(sys.argv[1:], "ch:q", ["list-hashes"]) hashes = [] do_check = False quiet = False files = files or ["-"] for o in opts: if o[0] == "--list-hashes": hash_list() raise SystemExit(0) if o[0] == "-h": hashes.extend(o[1].split(",")) if o[0] == "-c": do_check = True if o[0] == "-q": quiet = True if not hashes: hashes = ["sha256"] if not quiet: print(f"Note: {use_concurrency=}", file=sys.stderr) if do_check: status = 0 for fn in files: if not check(fn, quiet): status = 1 raise SystemExit(status) else: print_header(hashes) for fn in files: hashers = multihash(fn, hashes) for h in hashers: print(h, end=" ") print("", shlex.quote(fn)) def hash(fn): hashers = [hash_new(h) for h in hashes] with opener(fn) as f: while chunk := f.read(chunksize): for h in hashers: h.update(chunk) hashes = [] if __name__ == "__main__": main()