Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/digest/__init__.py: 56%
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 buffer_size: int
81 digest_length: Optional[int]
82 algorithm: str
83 paths: Seq[str]
86# ---------------------------------------------------------------------------
87# Functions
88# ---------------------------------------------------------------------------
91def die(msg: str) -> NoReturn:
92 print(msg, file=sys.stderr)
93 sys.exit(1)
96def parse_params() -> Params:
97 def positive_number(s: str) -> int:
98 n = int(s)
99 if n <= 0:
100 raise ValueError(f'"{s}" is not a positive number.')
102 return n
104 parser = argparse.ArgumentParser(
105 description="Generate a message digest (cryptohash) of one or more "
106 "files, or of standard input. Files are read as binary "
107 "data, even if they're text files. Files are read "
108 f"{BUFSIZE:,} bytes at a time, by default. Use -b to "
109 "change that buffer size."
110 )
111 parser.add_argument(
112 "-b",
113 "--bufsize",
114 metavar="N",
115 type=positive_number,
116 default=BUFSIZE,
117 help="Buffer size (in bytes) to use when reading. "
118 "Defaults to %(default)d.",
119 )
120 length_required = ", ".join(sorted(DIGEST_LENGTH_REQUIRED))
121 parser.add_argument(
122 "-l",
123 "--digest-length",
124 type=positive_number,
125 help="Length to use, for variable-length digests. "
126 f"Required for: {length_required}",
127 )
128 parser.add_argument(
129 "-v", "--version", action="version", version=f"%(prog)s {__version__}"
130 )
131 parser.add_argument(
132 "algorithm",
133 action="store",
134 metavar="algorithm",
135 choices=ALGORITHMS,
136 help="The digest algorithm to use, one of: " + ", ".join(ALGORITHMS),
137 )
138 parser.add_argument(
139 "path",
140 action="store",
141 nargs="*",
142 help="Input file(s) to process. If not specified, "
143 "standard input is read.",
144 )
146 args = parser.parse_args()
147 if (args.algorithm in DIGEST_LENGTH_REQUIRED) and (
148 args.digest_length is None
149 ):
150 die(
151 f"Digest algorithm {args.algorithm} requires that you specify a "
152 "digest length via -l or --digest-length."
153 )
154 elif (args.algorithm not in DIGEST_LENGTH_REQUIRED) and (
155 args.digest_length is not None
156 ):
157 print(
158 f"WARNING: Digest length (-l) is ignored for {args.algorithm}.",
159 file=sys.stderr,
160 )
161 args.digest_length = None
163 return Params(
164 buffer_size=args.bufsize,
165 digest_length=args.digest_length,
166 algorithm=args.algorithm,
167 paths=args.path,
168 )
171def digest(
172 f: BinaryIO,
173 algorithm: str,
174 bufsize: int,
175 digest_length: Optional[int] = None,
176) -> str:
177 try:
178 h = hashlib.new(algorithm)
179 except ValueError as ex:
180 die("%s: %s" % (algorithm, str(ex)))
182 buf = bytearray(bufsize)
183 while True:
184 # Pyright can't grok BinaryIO.readinto(), so just disable it for
185 # this line.
186 n = f.readinto(buf) # pyright: ignore
187 if n <= 0:
188 break
189 h.update(buf[:n])
191 # Some algorithms (e.g., the SHAKE algorithms) are variable length, and
192 # their hexdigest() functions take a length parameter. But the generic
193 # hexdigest() function doesn't, and the typing doesn't capture this
194 # difference. So, type-checkers like pyright complain about the first
195 # call, below. For now, we just disable pyright for that line.
196 if digest_length is not None:
197 return h.hexdigest(digest_length) # pyright: ignore
198 else:
199 return h.hexdigest()
202def main():
203 params: Params = parse_params()
205 if len(params.paths) == 0:
206 # Standard input.
207 print(
208 digest(
209 f=sys.stdin.buffer,
210 algorithm=params.algorithm,
211 bufsize=params.buffer_size,
212 digest_length=params.digest_length,
213 )
214 )
216 else:
217 u_algorithm = params.algorithm.upper()
218 for path in params.paths:
219 if not os.path.isfile(path):
220 print(f'*** Skipping non-file "{path}".')
221 continue
223 with open(path, mode="rb") as f:
224 d = digest(
225 f=f,
226 algorithm=params.algorithm,
227 bufsize=params.buffer_size,
228 digest_length=params.digest_length,
229 )
230 print(f"{u_algorithm} ({path}): {d}")
232 return 0
235# ---------------------------------------------------------------------------
236# Main
237# ---------------------------------------------------------------------------
239if __name__ == "__main__":
240 sys.exit(main())