{"record":{"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":207,"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":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/pypa/pip/blob/f399c3718970b1b0e2478dac5296eb62679a9b86/src/pip/_vendor/packaging/direct_url.py#L189-L225","documentation":"The legacy hash field in archive_info (from older PEP 610 implementations) must follow the format <algorithm>=<hash_digest>, e.g. sha256=abc123.... If the string lacks an = sign, DirectUrlValidationError is raised. This is separate from the newer hashes mapping field.","triggerScenarios":"Parsing an archive_info block that has a legacy hash field without an = separator: e.g. {'hash': 'abc123'} or {'hash': 'sha256:'} (colon instead of equals).","commonSituations":"Migrating from older metadata formats. Custom build scripts that write hash fields with the wrong separator. Tooling that uses a colon instead of equals sign.","solutions":["Format the legacy hash as <algorithm>=<digest> with an equals sign separator","Prefer the newer hashes mapping field over the legacy hash string","Remove the legacy hash field if hashes is already present and correct"],"exampleFix":"# before\ndata = {\n    \"url\": \"https://example.com/pkg.tar.gz\",\n    \"archive_info\": {\"hash\": \"abc123def456\"}  # missing algorithm=\n}\n\n# after\ndata = {\n    \"url\": \"https://example.com/pkg.tar.gz\",\n    \"archive_info\": {\"hashes\": {\"sha256\": \"abc123def456...\"}}\n}","handlingStrategy":"validation","validationCode":"def validate_legacy_hash(archive_info: dict) -> None:\n    legacy = archive_info.get(\"hash\")\n    if legacy is not None:\n        if \"=\" not in legacy:\n            raise ValueError(f\"Legacy hash must be 'algorithm=digest', got: {legacy!r}\")\n        algo, digest = legacy.split(\"=\", 1)\n        if not algo or not digest:\n            raise ValueError(f\"Invalid legacy hash: {legacy!r}\")","typeGuard":"def is_valid_legacy_hash(h) -> bool:\n    if not isinstance(h, str):\n        return False\n    parts = h.split(\"=\", 1)\n    return len(parts) == 2 and all(parts)","tryCatchPattern":"from packaging.direct_url import DirectUrl, DirectUrlValidationError\n\ntry:\n    du = DirectUrl.from_dict(data)\nexcept DirectUrlValidationError as e:\n    if \"Invalid hash format\" in str(e):\n        data[\"archive_info\"].pop(\"hash\", None)\n        du = DirectUrl.from_dict(data)","preventionTips":["Use the hashes mapping field instead of legacy hash","Format legacy hashes as 'algorithm=digest' with equals sign","Validate metadata before parsing"],"tags":["packaging","pep610","json-validation","hashes","vendored"],"backgroundTag":null,"analyzedSha":"f399c3718970b1b0e2478dac5296eb62679a9b86","analyzedAt":"2026-08-08T23:01:42.227Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}