Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/digest/__init__.py: 43%
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"""
37from __future__ import print_function
39__docformat__ = "restructuredtext"
41# Info about the module
42__version__ = "1.1.2"
43__author__ = "Brian M. Clapper"
44__email__ = "bmc@clapper.org"
45__url__ = "http://software.clapper.org/digest/"
46__copyright__ = "2008-2023 Brian M. Clapper"
47__license__ = "Apache Software License Version 2.0"
49# Package stuff
51__all__ = ["digest", "main"]
53# ---------------------------------------------------------------------------
54# Imports
55# ---------------------------------------------------------------------------
57import argparse
58import hashlib
59import os
60import sys
61from dataclasses import dataclass
62from typing import BinaryIO, NoReturn, Optional
63from typing import Sequence as Seq
65# ---------------------------------------------------------------------------
66# Constants
67# ---------------------------------------------------------------------------
69ALGORITHMS = sorted(hashlib.algorithms_available)
70BUFSIZE = 1024 * 16
71DIGEST_LENGTH_REQUIRED = {"shake_128", "shake_256"}
73# ---------------------------------------------------------------------------
74# Classes
75# ---------------------------------------------------------------------------
78@dataclass(frozen=True)
79class Params:
80 """
81 Parsed command-line parameters.
82 """
83 buffer_size: int
84 digest_length: Optional[int]
85 algorithm: str
86 paths: Seq[str]
89class DigestError(Exception):
90 """
91 Thrown to indicate an error in processing.
92 """
94# ---------------------------------------------------------------------------
95# Functions
96# ---------------------------------------------------------------------------
99def parse_params() -> Params:
100 """
101 Parse command-line parameters, returning a Params object.
102 """
103 def positive_number(s: str) -> int:
104 """
105 Ensure that a string is a positive number and, if it is, return
106 the number as an integer. Otherwise, raise a ValueError.
107 """
108 n = int(s)
109 if n <= 0:
110 raise ValueError(f'"{s}" is not a positive number.')
112 return n
114 parser = argparse.ArgumentParser(
115 description="Generate a message digest (cryptohash) of one or more "
116 "files, or of standard input. Files are read as binary "
117 "data, even if they're text files. Files are read "
118 f"{BUFSIZE:,} bytes at a time, by default. Use -b to "
119 "change that buffer size."
120 )
121 parser.add_argument(
122 "-b",
123 "--bufsize",
124 metavar="N",
125 type=positive_number,
126 default=BUFSIZE,
127 help="Buffer size (in bytes) to use when reading. "
128 "Defaults to %(default)d.",
129 )
130 length_required = ", ".join(sorted(DIGEST_LENGTH_REQUIRED))
131 parser.add_argument(
132 "-l",
133 "--digest-length",
134 type=positive_number,
135 help="Length to use, for variable-length digests. "
136 f"Required for: {length_required}",
137 )
138 parser.add_argument(
139 "-v", "--version", action="version", version=f"%(prog)s {__version__}"
140 )
141 parser.add_argument(
142 "algorithm",
143 action="store",
144 metavar="algorithm",
145 choices=ALGORITHMS,
146 help="The digest algorithm to use, one of: " + ", ".join(ALGORITHMS),
147 )
148 parser.add_argument(
149 "path",
150 action="store",
151 nargs="*",
152 help="Input file(s) to process. If not specified, "
153 "standard input is read.",
154 )
156 args = parser.parse_args()
157 if (args.algorithm in DIGEST_LENGTH_REQUIRED) and (
158 args.digest_length is None
159 ):
160 raise DigestError(
161 f"Digest algorithm {args.algorithm} requires that you specify a "
162 "digest length via -l or --digest-length."
163 )
165 if (args.algorithm not in DIGEST_LENGTH_REQUIRED) and (
166 args.digest_length is not None
167 ):
168 print(
169 f"WARNING: Digest length (-l) is ignored for {args.algorithm}.",
170 file=sys.stderr,
171 )
172 args.digest_length = None
174 return Params(
175 buffer_size=args.bufsize,
176 digest_length=args.digest_length,
177 algorithm=args.algorithm,
178 paths=args.path,
179 )
182def digest(
183 f: BinaryIO,
184 algorithm: str,
185 bufsize: int,
186 digest_length: Optional[int] = None,
187) -> str:
188 """
189 Calculate a digest of the contents of a file. If an error occurs, this
190 function raises a DigestError.
192 :param f: The file to read.
193 :param algorithm: The algorithm to use.
194 :param bufsize: The buffer size to use when reading.
195 :param digest_length: The length of the digest, for variable-length
196 algorithms.
197 """
198 try:
199 h = hashlib.new(algorithm)
200 buf = bytearray(bufsize)
201 while True:
202 # Pyright can't grok BinaryIO.readinto(), so just disable it for
203 # this line.
204 n = f.readinto(buf) # pyright: ignore
205 if n <= 0:
206 break
207 h.update(buf[:n])
209 # Some algorithms (e.g., the SHAKE algorithms) are variable length, and
210 # their hexdigest() functions take a length parameter. But the generic
211 # hexdigest() function doesn't, and the typing doesn't capture this
212 # difference. So, type-checkers like pyright complain about the first
213 # call, below. For now, we just disable pyright for that line.
214 if digest_length is not None:
215 return h.hexdigest(digest_length) # pyright: ignore
217 return h.hexdigest()
218 except Exception as ex:
219 # pylint: disable=raise-missing-from
220 raise DigestError(f"{algorithm}: {ex}")
223def main() -> int:
224 """
225 Main program.
226 """
227 params: Params = parse_params()
229 try:
230 if len(params.paths) == 0:
231 # Standard input.
232 print(
233 digest(
234 f=sys.stdin.buffer,
235 algorithm=params.algorithm,
236 bufsize=params.buffer_size,
237 digest_length=params.digest_length,
238 )
239 )
241 else:
242 u_algorithm = params.algorithm.upper()
243 for path in params.paths:
244 if not os.path.isfile(path):
245 print(f'*** Skipping non-file "{path}".')
246 continue
248 with open(path, mode="rb") as f:
249 d = digest(
250 f=f,
251 algorithm=params.algorithm,
252 bufsize=params.buffer_size,
253 digest_length=params.digest_length,
254 )
255 print(f"{u_algorithm} ({path}): {d}")
256 except DigestError as ex:
257 print(f"Error: {ex}", file=sys.stderr)
258 return 1
260 return 0
263# ---------------------------------------------------------------------------
264# Main
265# ---------------------------------------------------------------------------
267if __name__ == "__main__":
268 sys.exit(main())