Skip to content

Commit 706c446

Browse files
committed
pylock: validate hashes
1 parent 9a18628 commit 706c446

File tree

2 files changed

+52
-1
lines changed

2 files changed

+52
-1
lines changed

src/pip/_internal/models/pylock.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import dataclasses
2+
import hashlib
23
import logging
34
import re
45
import sys
@@ -213,6 +214,17 @@ def _exactly_one(iterable: Iterable[object]) -> bool:
213214
return found
214215

215216

217+
def _validate_hashes(hashes: Dict[str, Any]) -> None:
218+
if not hashes:
219+
raise PylockValidationError("At least one hash must be provided")
220+
if not any(algo in hashlib.algorithms_guaranteed for algo in hashes):
221+
raise PylockValidationError(
222+
"At least one hash algorithm must be in haslib.algorithms_guaranteed"
223+
)
224+
if not all(isinstance(hash, str) for hash in hashes.values()):
225+
raise PylockValidationError("Hash values must be strings")
226+
227+
216228
class PylockValidationError(Exception):
217229
pass
218230

@@ -236,7 +248,6 @@ class PackageVcs:
236248
subdirectory: Optional[str]
237249

238250
def __post_init__(self) -> None:
239-
# TODO validate supported vcs type
240251
if not self.path and not self.url:
241252
raise PylockValidationError("No path nor url set for vcs package")
242253

@@ -279,6 +290,7 @@ class PackageArchive:
279290
def __post_init__(self) -> None:
280291
if not self.path and not self.url:
281292
raise PylockValidationError("No path nor url set for archive package")
293+
_validate_hashes(self.hashes)
282294

283295
@classmethod
284296
def from_dict(cls, d: Dict[str, Any]) -> "Self":
@@ -304,6 +316,7 @@ class PackageSdist:
304316
def __post_init__(self) -> None:
305317
if not self.path and not self.url:
306318
raise PylockValidationError("No path nor url set for sdist package")
319+
_validate_hashes(self.hashes)
307320

308321
@classmethod
309322
def from_dict(cls, d: Dict[str, Any]) -> "Self":
@@ -329,6 +342,7 @@ class PackageWheel:
329342
def __post_init__(self) -> None:
330343
if not self.path and not self.url:
331344
raise PylockValidationError("No path nor url set for wheel package")
345+
_validate_hashes(self.hashes)
332346

333347
@classmethod
334348
def from_dict(cls, d: Dict[str, Any]) -> "Self":

tests/unit/test_pylock.py

+37
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime
22
from pathlib import Path
3+
from typing import Any, Dict
34

45
import pytest
56

@@ -8,6 +9,7 @@
89
from pip._vendor.packaging.version import Version
910

1011
from pip._internal.models.pylock import (
12+
PackageWheel,
1113
Pylock,
1214
PylockRequiredKeyError,
1315
PylockUnsupportedVersionError,
@@ -252,3 +254,38 @@ def test_pylock_tool() -> None:
252254
assert pylock.tool == {"pip": {"version": "25.2"}}
253255
package = pylock.packages[0]
254256
assert package.tool == {"pip": {"foo": "bar"}}
257+
258+
259+
@pytest.mark.parametrize(
260+
"hashes,expected_error",
261+
[
262+
(
263+
{
264+
"sha2": "f" * 40,
265+
},
266+
"At least one hash algorithm must be in haslib.algorithms_guaranteed",
267+
),
268+
(
269+
{
270+
"sha256": "f" * 40,
271+
"md5": 1,
272+
},
273+
"Hash values must be strings",
274+
),
275+
(
276+
{},
277+
"At least one hash must be provided",
278+
),
279+
],
280+
)
281+
def test_hash_validation(hashes: Dict[str, Any], expected_error: str) -> None:
282+
with pytest.raises(PylockValidationError) as exc_info:
283+
PackageWheel(
284+
name="example-1.0-py3-none-any.whl",
285+
upload_time=None,
286+
url="https://example.com/example-1.0-py3-none-any.whl",
287+
path=None,
288+
size=None,
289+
hashes=hashes,
290+
)
291+
assert str(exc_info.value) == expected_error

0 commit comments

Comments
 (0)