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

81 statements  

1#!/usr/bin/env python 

2# --------------------------------------------------------------------------- 

3 

4""" 

5Calculate a cryptohash on a file or standard input. 

6 

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: 

11 

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 +-------------+--------------------------------------+ 

27 

28For usage information, the algorithms supported by your version of Python, 

29and other information, run: 

30 

31 digest --help 

32 

33For additional information, see the README (README.md) or visit 

34https://github.com/bmc/digest 

35""" 

36 

37from __future__ import print_function 

38 

39__docformat__ = "restructuredtext" 

40 

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" 

48 

49# Package stuff 

50 

51__all__ = ["digest", "main"] 

52 

53# --------------------------------------------------------------------------- 

54# Imports 

55# --------------------------------------------------------------------------- 

56 

57import argparse 

58import hashlib 

59import os 

60import sys 

61from dataclasses import dataclass 

62from typing import BinaryIO, NoReturn, Optional 

63from typing import Sequence as Seq 

64 

65# --------------------------------------------------------------------------- 

66# Constants 

67# --------------------------------------------------------------------------- 

68 

69ALGORITHMS = sorted(hashlib.algorithms_available) 

70BUFSIZE = 1024 * 16 

71DIGEST_LENGTH_REQUIRED = {"shake_128", "shake_256"} 

72 

73# --------------------------------------------------------------------------- 

74# Classes 

75# --------------------------------------------------------------------------- 

76 

77 

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] 

87 

88 

89class DigestError(Exception): 

90 """ 

91 Thrown to indicate an error in processing. 

92 """ 

93 

94# --------------------------------------------------------------------------- 

95# Functions 

96# --------------------------------------------------------------------------- 

97 

98 

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.') 

111 

112 return n 

113 

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 ) 

155 

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 ) 

164 

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 

173 

174 return Params( 

175 buffer_size=args.bufsize, 

176 digest_length=args.digest_length, 

177 algorithm=args.algorithm, 

178 paths=args.path, 

179 ) 

180 

181 

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. 

191 

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]) 

208 

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 

216 

217 return h.hexdigest() 

218 except Exception as ex: 

219 # pylint: disable=raise-missing-from 

220 raise DigestError(f"{algorithm}: {ex}") 

221 

222 

223def main() -> int: 

224 """ 

225 Main program. 

226 """ 

227 params: Params = parse_params() 

228 

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 ) 

240 

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 

247 

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 

259 

260 return 0 

261 

262 

263# --------------------------------------------------------------------------- 

264# Main 

265# --------------------------------------------------------------------------- 

266 

267if __name__ == "__main__": 

268 sys.exit(main())