{"id":"55c70406a7bd8c92","repo":"pypa/pip","slug":"invalid-hash-format-expected-algorithm-hash","errorCode":null,"errorMessage":"Invalid hash format (expected '<algorithm>=<hash>')","messagePattern":"Invalid hash format \\(expected '<algorithm>=<hash>'\\)","errorType":"validation","errorClass":"DirectUrlValidationError","httpStatus":null,"severity":"error","filePath":"src/pip/_vendor/packaging/direct_url.py","lineNumber":195,"sourceCode":"\n    def __init__(\n        self,\n        *,\n        hashes: Mapping[str, str] | None = None,\n    ) -> None:\n        object.__setattr__(self, \"hashes\", hashes)\n\n    @classmethod\n    def _from_dict(cls, d: Mapping[str, Any]) -> Self:\n        hashes = _get(d, Mapping, \"hashes\")  # type: ignore[type-abstract]\n        if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):\n            raise DirectUrlValidationError(\n                \"Hash values must be strings\", context=\"hashes\"\n            )\n        legacy_hash = _get(d, str, \"hash\")\n        if legacy_hash is not None:\n            if \"=\" not in legacy_hash:\n                raise DirectUrlValidationError(\n                    \"Invalid hash format (expected '<algorithm>=<hash>')\",\n                    context=\"hash\",\n                )\n            hash_algorithm, hash_value = legacy_hash.split(\"=\", 1)\n            if hashes is None:\n                # if `hashes` are not present, we can derive it from the legacy `hash`\n                hashes = {hash_algorithm: hash_value}\n            else:\n                # if `hashes` are present, the legacy `hash` must match one of them\n                if hash_algorithm not in hashes:\n                    raise DirectUrlValidationError(\n                        f\"Algorithm {hash_algorithm!r} used in hash field \"\n                        f\"is not present in hashes field\",\n                        context=\"hashes\",\n                    )\n                if hashes[hash_algorithm] != hash_value:\n                    raise DirectUrlValidationError(\n                        f\"Algorithm {hash_algorithm!r} used in hash field \"","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/pypa/pip/blob/d7d0d0a39494e28ec1c407bd0680e4a4d1067791/src/pip/_vendor/packaging/direct_url.py#L177-L213","documentation":"Raised by `ArchiveInfo._from_dict` when the legacy `hash` field is present but does not contain an `=` separator. PEP 610's legacy `hash` field must follow the form `<algorithm>=<hexdigest>` (e.g. `sha256=abc123...`). Without the separator, the algorithm and digest cannot be split.","triggerScenarios":"A `direct_url.json` with `archive_info.hash` set to a bare hex digest like `\"abc123def456\"` or `\"sha256:abc123\"` (colon instead of equals).","commonSituations":"Older tooling that wrote the digest without the algorithm prefix; copy-paste from a `--hash` line that used a different separator; hand-crafted install records.","solutions":["Reformat the value as `algorithm=digest`, e.g. `sha256=<hexdigest>`","Prefer the modern `hashes` mapping (`{\"sha256\": \"<hexdigest>\"}`) over the legacy single `hash` field","Reinstall the package to regenerate a correct record"],"exampleFix":"// before\n{'archive_info': {'hash': 'abc123def456'}}\n// after\n{'archive_info': {'hash': 'sha256=abc123def456'}}","handlingStrategy":"validation","validationCode":"def format_legacy_hash(algorithm: str, digest: str) -> str:\n    if '=' in algorithm or '=' in digest:\n        raise ValueError('algorithm/digest must not contain =')\n    return f'{algorithm}={digest}'","typeGuard":"import re\ndef is_valid_legacy_hash(s: object) -> bool:\n    return isinstance(s, str) and bool(re.match(r'^[A-Za-z0-9_-]+=[0-9a-fA-F]+$', s))","tryCatchPattern":"from packaging.direct_url import ArchiveInfo, DirectUrlValidationError\ntry:\n    ArchiveInfo._from_dict(d)\nexcept DirectUrlValidationError as e:\n    if 'Invalid hash format' in str(e):\n        h = d['archive_info'].pop('hash')\n        d['archive_info']['hash'] = f'sha256={h}'  # repair if algorithm known\n    raise","preventionTips":["Always write hashes as 'algorithm=digest'","Prefer the modern hashes mapping over the legacy hash field","Validate legacy hash strings with an '=' check before persisting"],"tags":["packaging","direct-url","pep610","hashes"],"analyzedSha":"d7d0d0a39494e28ec1c407bd0680e4a4d1067791","analyzedAt":"2026-08-04T20:55:04.259Z","schemaVersion":2}