PK!]B B hb/cli.py"""Hash Brown CLI""" from glob import iglob from os.path import isfile import click from hb.main import Checksum def _is_match(checksum1: str, checksum2: str) -> bool: if checksum1.endswith(checksum2): return True return False def _algorithm_mode(algorithm: str, file: str, given: str) -> None: computed = 0 for filename in iglob(file, recursive=True): if isfile(filename): actual = Checksum(filename).get(algorithm) if given: output = Checksum.print(algorithm, filename, given) output += f" {click.style('OK', fg='green') if _is_match(actual, given) else click.style(f'ACTUAL: {actual}', fg='red')}" else: output = Checksum.print(algorithm, filename, actual) click.echo(output) computed += 1 if not computed: click.echo(f"No files matched the pattern: '{file}'") def _check_mode(file: str) -> None: try: for algorithm, filename, given in Checksum.parse(file): output = Checksum.print(algorithm, filename, given) try: actual = Checksum(filename).get(algorithm) except FileNotFoundError: output += f" {click.style('SKIP: File not found', fg='yellow')}" except OSError: output += f" {click.style('SKIP: Unable to read', fg='yellow')}" else: output += f" {click.style('OK', fg='green') if _is_match(actual, given) else click.style(f'ACTUAL: {actual}', fg='red')}" click.echo(output) except (OSError, ValueError) as error: click.echo(error) # type: ignore @click.version_option(version="1.1.1") @click.command(context_settings={"help_option_names": ["-h", "--help"]}) @click.option("-a", "--algorithm", type=click.Choice(Checksum.supported)) @click.option("-c", "--check", is_flag=True, help="Read checksums from a file.") @click.option("-g", "--given", help="See if the given checksum `TEXT` matches the computed checksum. (use with -a)") @click.argument("file") def cli(algorithm: str, check: str, given: str, file: str) -> None: """Hash Brown: Compute and verify checksums.""" if algorithm: _algorithm_mode(algorithm, file, given) elif check: _check_mode(file) else: pass if __name__ == "__main__": cli() PK!0MM hb/main.py"""Hash Brown""" import hashlib import re import zlib from dataclasses import dataclass, field from threading import Thread from time import sleep from typing import Dict, IO, List, Sequence, Tuple @dataclass class Checksum(): """Compute various checksums. Digest, hash, and checksum are all referred to as checksum for simplicity. """ path: str checksums: Dict[str, str] = field(default_factory=dict, init=False) supported: Tuple = field(default=("blake2b", "blake2s", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "adler32", "crc32"), repr=False, init=False) threshold: int = field(default=200, repr=False) @staticmethod def parse(file: str) -> List[Sequence[str]]: """Parse lines from a checksum file.""" parsed_lines = [] with open(file, "r") as lines: for line in lines: line = line.strip() if not line or line[0] == "#": # skip blank lines and comments continue match = re.match(r"(\w+)\s?\((.+)\)\s?=\s?(\w+)", line) if not match: raise ValueError(f"Bad line in checksum file: '{line}'") parsed_lines.append(match.group(1, 2, 3)) return parsed_lines @staticmethod def print(algorithm: str, file: str, checksum: str) -> str: """BSD style checksum output.""" return f"{algorithm} ({file}) = {checksum}" def _progress(self, file: IO) -> None: def _p(file: IO, fsize: int) -> None: while not file.closed: print(f"{round(file.tell() / fsize * 100)}%", end="\r") sleep(0.2) fsize, _ = file.seek(0, 2), file.seek(0) if fsize > self.threshold * 1024 * 1024: Thread(target=_p, args=(file, fsize)).start() def _hashlib_compute(self, name: str) -> str: result = hashlib.new(name) with open(self.path, "rb") as file: self._progress(file) for line in file: result.update(line) return result.hexdigest() def _zlib_compute(self, name: str) -> str: if name == "adler32": result = 1 update = zlib.adler32 elif name == "crc32": result = 0 update = zlib.crc32 with open(self.path, "rb") as file: self._progress(file) for line in file: result = update(line, result) return hex(result)[2:].zfill(8) def compute(self, algorithm: str) -> str: """Compute a checksum.""" if algorithm not in self.supported: raise ValueError(f"Unsupported algorithm: '{algorithm}'") elif algorithm in ["adler32", "crc32"]: result = self._zlib_compute(algorithm) else: result = self._hashlib_compute(algorithm) self.checksums[algorithm] = result return result def get(self, algorithm: str) -> str: """Same as `compute` but does not recalculate the checksum if it is already known.""" if algorithm in self.checksums: return self.checksums[algorithm] return self.compute(algorithm) @property def blake2b(self) -> str: """blake2b""" return self.get("blake2b") @property def blake2s(self) -> str: """blake2s""" return self.get("blake2s") @property def md5(self) -> str: """md5""" return self.get("md5") @property def sha1(self) -> str: """sha1""" return self.get("sha1") @property def sha224(self) -> str: """sha224""" return self.get("sha224") @property def sha256(self) -> str: """sha256""" return self.get("sha256") @property def sha384(self) -> str: """sha384""" return self.get("sha384") @property def sha512(self) -> str: """sha512""" return self.get("sha512") @property def adler32(self) -> str: """adler32""" return self.get("adler32") @property def crc32(self) -> str: """crc32""" return self.get("crc32") PK!H!!#hb-1.1.1.dist-info/entry_points.txtN+I/N.,()HHKɴb..PK!r++hb-1.1.1.dist-info/LICENSEMIT License Copyright (c) 2018 Ching Chow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!H_zTThb-1.1.1.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]n0H*J>mlcAPK!HO9z hb-1.1.1.dist-info/METADATAVmo6_lŲkд jw+P0E$6;Jvlg͆- 9ʩPk 0!yklfǙm19;!J m'33v9HCA҈O٘wAl(HC71L|٣XpB!nF?3q8~2D`8V0ڗT{y0i8O8x !rrآbKr֪x>l 6,V(BAa)P=Ys\f$O}>ٴ=~k{Yn,ο,λa K _>LD8Ozƀ]09js?v\wս>c`m,[ Fտ M v( 7PK!HVSsQhb-1.1.1.dist-info/RECORDm̹r@>߲()E f( PaE_p&Wffx*V)VPk.滓Ja<~ukx[E52)kjz ':<&7.9i-/]^1/Mρq!Ώ}ײF𙸉ǽ;6ҚF UnNr +(ֲԧ* <&\V^B4JiK,]|G@0wBGdݝ$6VP)3r<̇;C >a( Y$~~l;HPvY 0#uobgOc^~PK!]B B hb/cli.pyPK!0MM i hb/main.pyPK!H!!#hb-1.1.1.dist-info/entry_points.txtPK!r++@hb-1.1.1.dist-info/LICENSEPK!H_zTThb-1.1.1.dist-info/WHEELPK!HO9z -hb-1.1.1.dist-info/METADATAPK!HVSsQ$hb-1.1.1.dist-info/RECORDPK'&