Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/digest/__init__.py: 53%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# ---------------------------------------------------------------------------
4"""
5Calculate a cryptohash on a file or standard input.
7The *digest* utility calculates message digests of files or, if no file
8is specified, standard input. The set of supported digests depends on the
9current Python interpreter and the version of OpenSSL present on the system.
10However, at a minimum, *digest* supports the following algorithms:
12 +-------------+--------------------------------------+
13 | Argument | Algorithm |
14 +=============+======================================+
15 | md5 | The MD5 algorithm |
16 +-------------+--------------------------------------+
17 | sha1 | The SHA-1 algorithm |
18 +-------------+--------------------------------------+
19 | sha224 | The SHA-224 algorithm |
20 +-------------+--------------------------------------+
21 | sha256 | The SHA-256 algorithm |
22 +-------------+--------------------------------------+
23 | sha384 | The SHA-384 algorithm |
24 +-------------+--------------------------------------+
25 | sha512 | The SHA-512 algorithm |
26 +-------------+--------------------------------------+
28For usage information, the algorithms supported by your version of Python,
29and other information, run:
31 digest --help
33For additional information, see the README (README.md) or visit
34https://github.com/bmc/digest
35"""
37__docformat__ = "restructuredtext"
39# Info about the module
40__version__ = "1.2.1"
41__author__ = "Brian M. Clapper"
42__email__ = "bmc@clapper.org"
43__url__ = "https://github.com/bmc/digest"
44__copyright__ = "2008-2026 Brian M. Clapper"
45__license__ = "Apache Software License Version 2.0"
47# Package stuff
49__all__ = ["digest", "main"]
51# ---------------------------------------------------------------------------
52# Imports
53# ---------------------------------------------------------------------------
55import hashlib
56import os
57import sys
58from dataclasses import dataclass, replace
59from typing import BinaryIO
60from typing import Sequence as Seq
62import click
64# ---------------------------------------------------------------------------
65# Constants
66# ---------------------------------------------------------------------------
68ALGORITHMS = sorted(hashlib.algorithms_available)
69BUFSIZE = 1024 * 16
70DIGEST_LENGTH_REQUIRED = {"shake_128", "shake_256"}
72# ---------------------------------------------------------------------------
73# Classes
74# ---------------------------------------------------------------------------
77@dataclass(frozen=True)
78class Params:
79 """
80 Parsed command-line parameters.
81 """
83 buffer_size: int
84 digest_length: int | None
85 algorithm: str
86 paths: Seq[str]
89class DigestError(Exception):
90 """
91 Thrown to indicate an error in processing.
92 """
95# ---------------------------------------------------------------------------
96# Functions
97# ---------------------------------------------------------------------------
100def digest(
101 f: BinaryIO,
102 algorithm: str,
103 bufsize: int,
104 digest_length: int | None = None,
105) -> str:
106 """
107 Calculate a digest of the contents of a file. If an error occurs, this
108 function raises a DigestError.
110 :param f: The file to read.
111 :param algorithm: The algorithm to use.
112 :param bufsize: The buffer size to use when reading.
113 :param digest_length: The length of the digest, for variable-length
114 algorithms.
115 """
116 try:
117 h = hashlib.new(algorithm)
118 buf = bytearray(bufsize)
119 while True:
120 # Pyright can't grok BinaryIO.readinto(), so just disable it for
121 # this line.
122 n = f.readinto(buf) # pyright: ignore
123 if n <= 0:
124 break
125 h.update(buf[:n])
127 # Some algorithms (e.g., the SHAKE algorithms) are variable length, and
128 # their hexdigest() functions take a length parameter. But the generic
129 # hexdigest() function doesn't, and the typing doesn't capture this
130 # difference. So, type-checkers like pyright complain about the first
131 # call, below. For now, we just disable pyright for that line.
132 if digest_length is not None:
133 return h.hexdigest(digest_length) # pyright: ignore
135 return h.hexdigest()
136 except Exception as ex:
137 raise DigestError(f"{algorithm}: {ex}") from ex
140def positive_integer(
141 ctx: click.Context, param: str, value: int | None
142) -> int | None:
143 """
144 Ensure that a command line parameter is a positive integer.
145 """
146 if value is not None and value <= 0:
147 raise click.BadParameter(f"{value} is not a positive integer.")
149 return value
152@click.command(context_settings={"help_option_names": ["-h", "--help"]})
153@click.option(
154 "-b",
155 "--bufsize",
156 type=int,
157 default=BUFSIZE,
158 show_default=True,
159 callback=positive_integer,
160 help="Buffer size to use when reading files.",
161)
162@click.option(
163 "-l",
164 "--digest-length",
165 type=int,
166 callback=positive_integer,
167 show_default=True,
168 help="Length of the digest for variable-length algorithms.",
169)
170@click.option(
171 "-a",
172 "--algorithm",
173 type=click.Choice(ALGORITHMS, case_sensitive=False),
174 required=True,
175 help="Digest algorithm to use.",
176)
177@click.version_option(version=__version__)
178@click.argument(
179 "paths", nargs=-1, metavar="[PATH]...", type=click.Path(exists=True)
180)
181def main(
182 bufsize: int, digest_length: int | None, algorithm: str, paths: list[str]
183) -> int:
184 """
185 Generate a message digest (cryptohash) of one or more files, or of standard
186 input. Files are read as binary data, even if they're text files. Files are
187 read BUFSIZE bytes at a time.
188 """
189 params = Params(
190 buffer_size=bufsize,
191 digest_length=digest_length,
192 algorithm=algorithm,
193 paths=paths,
194 )
196 if (params.algorithm in DIGEST_LENGTH_REQUIRED) and (
197 params.digest_length is None
198 ):
199 raise click.BadParameter(
200 f"Digest length is required for algorithm {params.algorithm}."
201 )
203 if (params.algorithm not in DIGEST_LENGTH_REQUIRED) and (
204 params.digest_length is not None
205 ):
206 print(
207 f"WARNING: Digest length (-l) is ignored for {params.algorithm}",
208 file=sys.stderr,
209 )
210 params = replace(params, digest_length=None)
212 try:
213 if len(params.paths) == 0:
214 # Standard input.
215 print(
216 digest(
217 f=sys.stdin.buffer,
218 algorithm=params.algorithm,
219 bufsize=params.buffer_size,
220 digest_length=params.digest_length,
221 )
222 )
224 else:
225 u_algorithm = params.algorithm.upper()
226 for path in params.paths:
227 if not os.path.isfile(path):
228 print(f'*** Skipping non-file "{path}".')
229 continue
231 with open(path, mode="rb") as f:
232 d = digest(
233 f=f,
234 algorithm=params.algorithm,
235 bufsize=params.buffer_size,
236 digest_length=params.digest_length,
237 )
238 print(f"{u_algorithm} ({path}): {d}")
239 except DigestError as ex:
240 print(f"Error: {ex}", file=sys.stderr)
241 return 1
243 return 0
246# ---------------------------------------------------------------------------
247# Main
248# ---------------------------------------------------------------------------
250if __name__ == "__main__":
251 sys.exit(main())