diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index 263a3fc1095..4bd7604872e 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -31,6 +31,7 @@ from poetry.utils.env import EnvCommandError from poetry.utils.helpers import Downloader from poetry.utils.helpers import get_file_hash +from poetry.utils.helpers import get_highest_priority_hash_type from poetry.utils.helpers import pluralize from poetry.utils.helpers import remove_directory from poetry.utils.pip import pip_install @@ -792,8 +793,17 @@ def _populate_hashes_dict(self, archive: Path, package: Package) -> None: @staticmethod def _validate_archive_hash(archive: Path, package: Package) -> str: - archive_hash: str = "sha256:" + get_file_hash(archive) known_hashes = {f["hash"] for f in package.files if f["file"] == archive.name} + hash_types = {t.split(":")[0] for t in known_hashes} + hash_type = get_highest_priority_hash_type(hash_types, archive.name) + + if hash_type is None: + raise RuntimeError( + f"No usable hash type(s) for {package} from archive" + f" {archive.name} found (known hashes: {known_hashes!s})" + ) + + archive_hash = f"{hash_type}:{get_file_hash(archive, hash_type)}" if archive_hash not in known_hashes: raise RuntimeError( diff --git a/src/poetry/utils/helpers.py b/src/poetry/utils/helpers.py index e2ee03b467b..9916be7b87f 100644 --- a/src/poetry/utils/helpers.py +++ b/src/poetry/utils/helpers.py @@ -2,6 +2,7 @@ import hashlib import io +import logging import os import shutil import stat @@ -36,6 +37,29 @@ from poetry.utils.authenticator import Authenticator +logger = logging.getLogger(__name__) +prioritised_hash_types: tuple[str, ...] = tuple( + t + for t in [ + "sha3_512", + "sha3_384", + "sha3_256", + "sha3_224", + "sha512", + "sha384", + "sha256", + "sha224", + "shake_256", + "shake_128", + "blake2s", + "blake2b", + ] + if t in hashlib.algorithms_available +) +non_prioritised_available_hash_types: frozenset[str] = frozenset( + set(hashlib.algorithms_available).difference(prioritised_hash_types) +) + @contextmanager def directory(path: Path) -> Iterator[Path]: @@ -295,6 +319,28 @@ def get_file_hash(path: Path, hash_name: str = "sha256") -> str: return h.hexdigest() +def get_highest_priority_hash_type( + hash_types: set[str], archive_name: str +) -> str | None: + if not hash_types: + return None + + for prioritised_hash_type in prioritised_hash_types: + if prioritised_hash_type in hash_types: + return prioritised_hash_type + + logger.debug( + f"There are no known hash types for {archive_name} that are prioritised (known" + f" hash types: {hash_types!s})" + ) + + for available_hash_type in non_prioritised_available_hash_types: + if available_hash_type in hash_types: + return available_hash_type + + return None + + def extractall(source: Path, dest: Path, zip: bool) -> None: """Extract all members from either a zip or tar archive.""" if zip: diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index 17aeba7b8fd..d73705ff405 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -1456,3 +1456,124 @@ def test_other_error( output = io.fetch_output().strip() assert output.startswith(expected_start) assert output.endswith(expected_end) + + +@pytest.mark.parametrize( + "package_files,expected_url_reference", + [ + ( + [ + { + "file": "demo-0.1.0.tar.gz", + "hash": "sha512:766ecf369b6bdf801f6f7bbfe23923cc9793d633a55619472cd3d5763f9154711fbf57c8b6ca74e4a82fa9bd8380af831e7b8668e68e362669fc60b1d81d79ad", + }, + { + "file": "demo-0.1.0.tar.gz", + "hash": "md5:d1912c917363a64e127318655f7d1fe7", + }, + { + "file": "demo-0.1.0.whl", + "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", + }, + ], + { + "archive_info": { + "hashes": { + "sha512": "766ecf369b6bdf801f6f7bbfe23923cc9793d633a55619472cd3d5763f9154711fbf57c8b6ca74e4a82fa9bd8380af831e7b8668e68e362669fc60b1d81d79ad" + }, + }, + }, + ), + ( + [{ + "file": "demo-0.1.0.tar.gz", + "hash": "md5:d1912c917363a64e127318655f7d1fe7", + }], + { + "archive_info": { + "hashes": {"md5": "d1912c917363a64e127318655f7d1fe7"}, + }, + }, + ), + ( + [ + { + "file": "demo-0.1.0.tar.gz", + "hash": "sha3_512:196f4af9099185054ed72ca1d4c57707da5d724df0af7c3dfcc0fd018b0e0533908e790a291600c7d196fe4411b4f5f6db45213fe6e5cd5512bf18b2e9eff728", + }, + { + "file": "demo-0.1.0.tar.gz", + "hash": "sha512:766ecf369b6bdf801f6f7bbfe23923cc9793d633a55619472cd3d5763f9154711fbf57c8b6ca74e4a82fa9bd8380af831e7b8668e68e362669fc60b1d81d79ad", + }, + { + "file": "demo-0.1.0.tar.gz", + "hash": "md5:d1912c917363a64e127318655f7d1fe7", + }, + { + "file": "demo-0.1.0.whl", + "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", + }, + ], + { + "archive_info": { + "hashes": { + "sha3_512": "196f4af9099185054ed72ca1d4c57707da5d724df0af7c3dfcc0fd018b0e0533908e790a291600c7d196fe4411b4f5f6db45213fe6e5cd5512bf18b2e9eff728" + }, + }, + }, + ), + ], +) +def test_executor_known_hashes( + package_files: list[dict[str, str]], + expected_url_reference: dict[str, Any], + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + io: BufferedIO, + fixture_dir: FixtureDirGetter, +) -> None: + package_source_url: Path = ( + fixture_dir("distributions") / "demo-0.1.0.tar.gz" + ).resolve() + package = Package( + "demo", "0.1.0", source_type="file", source_url=package_source_url.as_posix() + ) + package.files = package_files + executor = Executor(tmp_venv, pool, config, io) + executor.execute([Install(package)]) + expected_url_reference["url"] = package_source_url.as_uri() + verify_installed_distribution(tmp_venv, package, expected_url_reference) + + +def test_executor_no_supported_hash_types( + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + io: BufferedIO, + fixture_dir: FixtureDirGetter, +) -> None: + url = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve() + package = Package("demo", "0.1.0", source_type="file", source_url=url.as_posix()) + # Set package.files so the executor will attempt to hash the package + package.files = [ + { + "file": "demo-0.1.0.tar.gz", + "hash": "hash_blah:1234567890abcdefghijklmnopqrstyzwxyz", + }, + { + "file": "demo-0.1.0.whl", + "hash": "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a", + }, + ] + + executor = Executor(tmp_venv, pool, config, io) + return_code = executor.execute([Install(package)]) + distributions = list(tmp_venv.site_packages.distributions(name=package.name)) + assert len(distributions) == 0 + + output = io.fetch_output() + error = io.fetch_error() + assert return_code == 1, f"\noutput: {output}\nerror: {error}\n" + assert "No usable hash type(s) for demo" in output + assert "hash_blah:1234567890abcdefghijklmnopqrstyzwxyz" in output diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index 84e1520a39b..e4713f80ff1 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -8,6 +8,7 @@ from poetry.utils.helpers import download_file from poetry.utils.helpers import get_file_hash +from poetry.utils.helpers import get_highest_priority_hash_type if TYPE_CHECKING: @@ -139,3 +140,16 @@ def test_download_file( expect_sha_256 = "9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad" assert get_file_hash(dest) == expect_sha_256 assert http.last_request().headers["Accept-Encoding"] == "Identity" + + +@pytest.mark.parametrize( + "hash_types,expected", + [ + (("sha512", "sha3_512", "md5"), "sha3_512"), + ("md5", "md5"), + (("blah", "blah_blah"), None), + ((), None), + ], +) +def test_highest_priority_hash_type(hash_types: set[str], expected: str | None) -> None: + assert get_highest_priority_hash_type(hash_types, "Blah") == expected