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

72 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-02-26 06:33 +0000

1import hashlib 

2from typing import TYPE_CHECKING, BinaryIO, Dict, Iterable, List, NoReturn, Optional 

3 

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

5from pip._internal.utils.misc import read_chunks 

6 

7if TYPE_CHECKING: 

8 from hashlib import _Hash 

9 

10 

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

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

13FAVORITE_HASH = "sha256" 

14 

15 

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

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

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

19 

20 

21class Hashes: 

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

23 known-good values 

24 

25 """ 

26 

27 def __init__(self, hashes: Optional[Dict[str, List[str]]] = None) -> None: 

28 """ 

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

30 hex digests 

31 """ 

32 allowed = {} 

33 if hashes is not None: 

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

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

36 allowed[alg] = sorted(keys) 

37 self._allowed = allowed 

38 

39 def __and__(self, other: "Hashes") -> "Hashes": 

40 if not isinstance(other, Hashes): 

41 return NotImplemented 

42 

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

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

45 if not other: 

46 return self 

47 if not self: 

48 return other 

49 

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

51 new = {} 

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

53 if alg not in self._allowed: 

54 continue 

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

56 return Hashes(new) 

57 

58 @property 

59 def digest_count(self) -> int: 

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

61 

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

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

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

65 

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

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

68 data. 

69 

70 Raise HashMismatch if none match. 

71 

72 """ 

73 gots = {} 

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

75 try: 

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

77 except (ValueError, TypeError): 

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

79 

80 for chunk in chunks: 

81 for hash in gots.values(): 

82 hash.update(chunk) 

83 

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

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

86 return 

87 self._raise(gots) 

88 

89 def _raise(self, gots: Dict[str, "_Hash"]) -> "NoReturn": 

90 raise HashMismatch(self._allowed, gots) 

91 

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

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

94 

95 Raise HashMismatch if none match. 

96 

97 """ 

98 return self.check_against_chunks(read_chunks(file)) 

99 

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

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

102 return self.check_against_file(file) 

103 

104 def has_one_of(self, hashes: Dict[str, str]) -> bool: 

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

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

107 if self.is_hash_allowed(hash_name, hex_digest): 

108 return True 

109 return False 

110 

111 def __bool__(self) -> bool: 

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

113 return bool(self._allowed) 

114 

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

116 if not isinstance(other, Hashes): 

117 return NotImplemented 

118 return self._allowed == other._allowed 

119 

120 def __hash__(self) -> int: 

121 return hash( 

122 ",".join( 

123 sorted( 

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

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

126 for digest in digest_list 

127 ) 

128 ) 

129 ) 

130 

131 

132class MissingHashes(Hashes): 

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

134 

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

136 exception showing it to the user. 

137 

138 """ 

139 

140 def __init__(self) -> None: 

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

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

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

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

145 

146 def _raise(self, gots: Dict[str, "_Hash"]) -> "NoReturn": 

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