Repository: pypa/pip

Issue:
pip 23.0: `pip list` fails with error "ValueError: not enough values to unpack" for archives installed from URL
### Description

When a project contains installed archives installed by specifying URL (e.g. spaCy models), `pip list` does not display the list of installed packages, because it crashes.

Probable cause: https://github.com/pypa/pip/commit/38681f3d6669754c7e919f0eb051b12931bfb0f2

### Expected behavior

_No response_

### pip version

pip 23.0

### Python version

Python 3.8.16

### OS

Ubuntu 20.04.5 LTS (Focal Fossa)

### How to Reproduce

```
pyenv virtualenv 3.8.16 pip-issue
pyenv local pip-issue
```

```
cat << EOF > ./pyproject.toml
[tool.poetry]
name = "pip_issue"
version = "0.0.1"
authors = [ "Foo Bar <foo@example.com>" ]
description = "pip issue"

[tool.poetry.dependencies]
python = ">=3.8, <3.11"
en_core_web_sm = { url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.5.0/en_core_web_sm-3.5.0.tar.gz" }

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
EOF
```

```
pip install --upgrade poetry
poetry install
```

```
# ✅ previous version works fine
pip list  
```

```
# 💥 latest version crashes
python3.8 -m pip install --upgrade pip
pip list
```

### Output

```
$ pip list                              
ERROR: Exception:
Traceback (most recent call last):
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/cli/base_command.py", line 160, in exc_logging_wrapper
    status = run_func(*args)
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/commands/list.py", line 192, in run
    self.output_package_listing(packages, options)
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/commands/list.py", line 273, in output_package_listing
    data, header = format_for_columns(packages, options)
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/commands/list.py", line 316, in format_for_columns
    has_editables = any(x.editable for x in pkgs)
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/commands/list.py", line 316, in <genexpr>
    has_editables = any(x.editable for x in pkgs)
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/metadata/base.py", line 338, in editable
    return bool(self.editable_project_location)
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/metadata/base.py", line 176, in editable_project_location
    direct_url = self.direct_url
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/metadata/base.py", line 306, in direct_url
    return DirectUrl.from_json(content)
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/models/direct_url.py", line 217, in from_json
    return cls.from_dict(json.loads(s))
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/models/direct_url.py", line 200, in from_dict
    ArchiveInfo._from_dict(_get(d, dict, "archive_info")),
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/models/direct_url.py", line 124, in _from_dict
    return cls(hash=_get(d, str, "hash"), hashes=_get(d, dict, "hashes"))
  File "/home/users/foo/.pyenv/versions/pip-issue/lib/python3.8/site-packages/pip/_internal/models/direct_url.py", line 111, in __init__
    hash_name, hash_value = hash.split("=", 1)
ValueError: not enough values to unpack (expected 2, got 1)
```

### Code of Conduct

- [X] I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).

Discussion:

Patch:
diff --git a/news/11773.bugfix.rst b/news/11773.bugfix.rst
new file mode 100644
index 000000000..077bf0612
--- /dev/null
+++ b/news/11773.bugfix.rst
@@ -0,0 +1,1 @@
+Do not crash in presence of misformatted hash field in ``direct_url.json``.
diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py
index 09b540f91..c3de70a74 100644
--- a/src/pip/_internal/models/direct_url.py
+++ b/src/pip/_internal/models/direct_url.py
@@ -108,7 +108,12 @@ class ArchiveInfo:
         if hash is not None:
             # Auto-populate the hashes key to upgrade to the new format automatically.
             # We don't back-populate the legacy hash key.
-            hash_name, hash_value = hash.split("=", 1)
+            try:
+                hash_name, hash_value = hash.split("=", 1)
+            except ValueError:
+                raise DirectUrlValidationError(
+                    f"invalid archive_info.hash format: {hash!r}"
+                )
             if hashes is None:
                 hashes = {hash_name: hash_value}
             elif hash_name not in hash:


Test patch:
diff --git a/tests/unit/test_direct_url.py b/tests/unit/test_direct_url.py
index e1708ae93..3ca982b50 100644
--- a/tests/unit/test_direct_url.py
+++ b/tests/unit/test_direct_url.py
@@ -102,6 +102,13 @@ def test_parsing_validation() -> None:
         match="more than one of archive_info, dir_info, vcs_info",
     ):
         DirectUrl.from_dict({"url": "http://...", "dir_info": {}, "archive_info": {}})
+    with pytest.raises(
+        DirectUrlValidationError,
+        match="invalid archive_info.hash format",
+    ):
+        DirectUrl.from_dict(
+            {"url": "http://...", "archive_info": {"hash": "sha256:aaa"}}
+        )
 
 
 def test_redact_url() -> None: