Skip to content

Commit 0dd4ffb

Browse files
committed
pylock: validate hashes
1 parent ba086a8 commit 0dd4ffb

File tree

2 files changed

+52
-1
lines changed

2 files changed

+52
-1
lines changed

src/pip/_internal/models/pylock.py

Lines changed: 15 additions & 1 deletion
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 hashlib.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

Lines changed: 37 additions & 0 deletions
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,
@@ -253,3 +255,38 @@ def test_pylock_tool() -> None:
253255
assert pylock.tool == {"pip": {"version": "25.2"}}
254256
package = pylock.packages[0]
255257
assert package.tool == {"pip": {"foo": "bar"}}
258+
259+
260+
@pytest.mark.parametrize(
261+
"hashes,expected_error",
262+
[
263+
(
264+
{
265+
"sha2": "f" * 40,
266+
},
267+
"At least one hash algorithm must be in hashlib.algorithms_guaranteed",
268+
),
269+
(
270+
{
271+
"sha256": "f" * 40,
272+
"md5": 1,
273+
},
274+
"Hash values must be strings",
275+
),
276+
(
277+
{},
278+
"At least one hash must be provided",
279+
),
280+
],
281+
)
282+
def test_hash_validation(hashes: Dict[str, Any], expected_error: str) -> None:
283+
with pytest.raises(PylockValidationError) as exc_info:
284+
PackageWheel(
285+
name="example-1.0-py3-none-any.whl",
286+
upload_time=None,
287+
url="https://example.com/example-1.0-py3-none-any.whl",
288+
path=None,
289+
size=None,
290+
hashes=hashes,
291+
)
292+
assert str(exc_info.value) == expected_error

0 commit comments

Comments
 (0)