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

75 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 

37__docformat__ = "restructuredtext" 

38 

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" 

46 

47# Package stuff 

48 

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

50 

51# --------------------------------------------------------------------------- 

52# Imports 

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

54 

55import hashlib 

56import os 

57import sys 

58from dataclasses import dataclass, replace 

59from typing import BinaryIO 

60from typing import Sequence as Seq 

61 

62import click 

63 

64# --------------------------------------------------------------------------- 

65# Constants 

66# --------------------------------------------------------------------------- 

67 

68ALGORITHMS = sorted(hashlib.algorithms_available) 

69BUFSIZE = 1024 * 16 

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

71 

72# --------------------------------------------------------------------------- 

73# Classes 

74# --------------------------------------------------------------------------- 

75 

76 

77@dataclass(frozen=True) 

78class Params: 

79 """ 

80 Parsed command-line parameters. 

81 """ 

82 

83 buffer_size: int 

84 digest_length: int | None 

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

96# Functions 

97# --------------------------------------------------------------------------- 

98 

99 

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. 

109 

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

126 

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 

134 

135 return h.hexdigest() 

136 except Exception as ex: 

137 raise DigestError(f"{algorithm}: {ex}") from ex 

138 

139 

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

148 

149 return value 

150 

151 

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 ) 

195 

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 ) 

202 

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) 

211 

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 ) 

223 

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 

230 

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 

242 

243 return 0 

244 

245 

246# --------------------------------------------------------------------------- 

247# Main 

248# --------------------------------------------------------------------------- 

249 

250if __name__ == "__main__": 

251 sys.exit(main())