Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_internal/utils/hashes.py: 35%

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

74 statements  

1from __future__ import annotations 

2 

3import hashlib 

4from collections.abc import Iterable 

5from typing import TYPE_CHECKING, BinaryIO, NoReturn 

6 

7from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError 

8from pip._internal.utils.misc import read_chunks 

9 

10if TYPE_CHECKING: 

11 from hashlib import _Hash 

12 

13 

14# The recommended hash algo of the moment. Change this whenever the state of 

15# the art changes; it won't hurt backward compatibility. 

16FAVORITE_HASH = "sha256" 

17 

18 

19# Names of hashlib algorithms allowed by the --hash option and ``pip hash`` 

20# Currently, those are the ones at least as collision-resistant as sha256. 

21STRONG_HASHES = ["sha256", "sha384", "sha512"] 

22 

23 

24class Hashes: 

25 """A wrapper that builds multiple hashes at once and checks them against 

26 known-good values 

27 

28 """ 

29 

30 def __init__(self, hashes: dict[str, list[str]] | None = None) -> None: 

31 """ 

32 :param hashes: A dict of algorithm names pointing to lists of allowed 

33 hex digests 

34 """ 

35 allowed = {} 

36 if hashes is not None: 

37 for alg, keys in hashes.items(): 

38 # Make sure values are always sorted (to ease equality checks) 

39 allowed[alg] = [k.lower() for k in sorted(keys)] 

40 self._allowed = allowed 

41 

42 def __and__(self, other: Hashes) -> Hashes: 

43 if not isinstance(other, Hashes): 

44 return NotImplemented 

45 

46 # If either of the Hashes object is entirely empty (i.e. no hash 

47 # specified at all), all hashes from the other object are allowed. 

48 if not other: 

49 return self 

50 if not self: 

51 return other 

52 

53 # Otherwise only hashes that present in both objects are allowed. 

54 new = {} 

55 for alg, values in other._allowed.items(): 

56 if alg not in self._allowed: 

57 continue 

58 new[alg] = [v for v in values if v in self._allowed[alg]] 

59 return Hashes(new) 

60 

61 @property 

62 def digest_count(self) -> int: 

63 return sum(len(digests) for digests in self._allowed.values()) 

64 

65 def is_hash_allowed(self, hash_name: str, hex_digest: str) -> bool: 

66 """Return whether the given hex digest is allowed.""" 

67 return hex_digest in self._allowed.get(hash_name, []) 

68 

69 def check_against_chunks(self, chunks: Iterable[bytes]) -> None: 

70 """Check good hashes against ones built from iterable of chunks of 

71 data. 

72 

73 Raise HashMismatch if none match. 

74 

75 """ 

76 gots = {} 

77 for hash_name in self._allowed.keys(): 

78 try: 

79 gots[hash_name] = hashlib.new(hash_name) 

80 except (ValueError, TypeError): 

81 raise InstallationError(f"Unknown hash name: {hash_name}") 

82 

83 for chunk in chunks: 

84 for hash in gots.values(): 

85 hash.update(chunk) 

86 

87 for hash_name, got in gots.items(): 

88 if got.hexdigest() in self._allowed[hash_name]: 

89 return 

90 self._raise(gots) 

91 

92 def _raise(self, gots: dict[str, _Hash]) -> NoReturn: 

93 raise HashMismatch(self._allowed, gots) 

94 

95 def check_against_file(self, file: BinaryIO) -> None: 

96 """Check good hashes against a file-like object 

97 

98 Raise HashMismatch if none match. 

99 

100 """ 

101 return self.check_against_chunks(read_chunks(file)) 

102 

103 def check_against_path(self, path: str) -> None: 

104 with open(path, "rb") as file: 

105 return self.check_against_file(file) 

106 

107 def has_one_of(self, hashes: dict[str, str]) -> bool: 

108 """Return whether any of the given hashes are allowed.""" 

109 for hash_name, hex_digest in hashes.items(): 

110 if self.is_hash_allowed(hash_name, hex_digest): 

111 return True 

112 return False 

113 

114 def __bool__(self) -> bool: 

115 """Return whether I know any known-good hashes.""" 

116 return bool(self._allowed) 

117 

118 def __eq__(self, other: object) -> bool: 

119 if not isinstance(other, Hashes): 

120 return NotImplemented 

121 return self._allowed == other._allowed 

122 

123 def __hash__(self) -> int: 

124 return hash( 

125 ",".join( 

126 sorted( 

127 ":".join((alg, digest)) 

128 for alg, digest_list in self._allowed.items() 

129 for digest in digest_list 

130 ) 

131 ) 

132 ) 

133 

134 

135class MissingHashes(Hashes): 

136 """A workalike for Hashes used when we're missing a hash for a requirement 

137 

138 It computes the actual hash of the requirement and raises a HashMissing 

139 exception showing it to the user. 

140 

141 """ 

142 

143 def __init__(self) -> None: 

144 """Don't offer the ``hashes`` kwarg.""" 

145 # Pass our favorite hash in to generate a "gotten hash". With the 

146 # empty list, it will never match, so an error will always raise. 

147 super().__init__(hashes={FAVORITE_HASH: []}) 

148 

149 def _raise(self, gots: dict[str, _Hash]) -> NoReturn: 

150 raise HashMissing(gots[FAVORITE_HASH].hexdigest())