fix: Handle relative paths properly in _absolute_url (#2153)

This updates the simpleapi parser to handle indexes where wheel and
sdist may be an index_url relative path. It also organises the
conditionals with fewer negations so they're easier to read

Fixes: #2150
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e59564..59dfbe4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,7 @@
 * (gazelle): Fix incorrect use of `t.Fatal`/`t.Fatalf` in tests.
 * (toolchain) Omit third-party python packages from coverage reports from
   stage2 bootstrap template.
+* (bzlmod) Properly handle relative path URLs in parse_simpleapi_html.bzl
 
 ### Added
 * Nothing yet
diff --git a/examples/bzlmod/MODULE.bazel.lock b/examples/bzlmod/MODULE.bazel.lock
index 4deef01..8d02256 100644
--- a/examples/bzlmod/MODULE.bazel.lock
+++ b/examples/bzlmod/MODULE.bazel.lock
@@ -1231,7 +1231,7 @@
     },
     "@@rules_python~//python/extensions:pip.bzl%pip": {
       "general": {
-        "bzlTransitiveDigest": "eWzi4IV0AzQ+cpVkMkdU/wOc7BUQdo0hAAQcCh8C4uU=",
+        "bzlTransitiveDigest": "9hiLCuWaaaU7Q+l2ONVr1A0NcG1JfSihv1UYeA1SpNY=",
         "usagesDigest": "MChlcSw99EuW3K7OOoMcXQIdcJnEh6YmfyjJm+9mxIg=",
         "recordedFileInputs": {
           "@@other_module~//requirements_lock_3_11.txt": "a7d0061366569043d5efcf80e34a32c732679367cb3c831c4cdc606adc36d314",
@@ -6140,7 +6140,7 @@
     },
     "@@rules_python~//python/private/pypi:pip.bzl%pip_internal": {
       "general": {
-        "bzlTransitiveDigest": "RyEJxfGmNQVzqInjjGrR29yqfFPKe9DKgODI1mxd8wA=",
+        "bzlTransitiveDigest": "VoK/T0JkBdcomCHnDIYkX+stkywdxrh1MVM16e8D4sE=",
         "usagesDigest": "Y8ihY+R57BAFhalrVLVdJFrpwlbsiKz9JPJ99ljF7HA=",
         "recordedFileInputs": {
           "@@rules_python~//tools/publish/requirements.txt": "031e35d03dde03ae6305fe4b3d1f58ad7bdad857379752deede0f93649991b8a",
diff --git a/python/private/pypi/parse_simpleapi_html.bzl b/python/private/pypi/parse_simpleapi_html.bzl
index 81ee385..b4e7dd8 100644
--- a/python/private/pypi/parse_simpleapi_html.bzl
+++ b/python/private/pypi/parse_simpleapi_html.bzl
@@ -78,7 +78,7 @@
                 url = _absolute_url(url, dist_url),
                 sha256 = sha256,
                 metadata_sha256 = metadata_sha256,
-                metadata_url = _absolute_url(url, metadata_url),
+                metadata_url = _absolute_url(url, metadata_url) if metadata_url else "",
                 yanked = yanked,
             )
         else:
@@ -109,18 +109,33 @@
 
     return "{}://{}".format(scheme, host)
 
+def _is_downloadable(url):
+    """Checks if the URL would be accepted by the Bazel downloader.
+
+    This is based on Bazel's HttpUtils::isUrlSupportedByDownloader
+    """
+    return url.startswith("http://") or url.startswith("https://") or url.startswith("file://")
+
 def _absolute_url(index_url, candidate):
+    if candidate == "":
+        return candidate
+
+    if _is_downloadable(candidate):
+        return candidate
+
     if candidate.startswith("/"):
-        # absolute url
+        # absolute path
         root_directory = _get_root_directory(index_url)
         return "{}{}".format(root_directory, candidate)
 
-    if not candidate.startswith(".."):
-        return candidate
+    if candidate.startswith(".."):
+        # relative path with up references
+        candidate_parts = candidate.split("..")
+        last = candidate_parts[-1]
+        for _ in range(len(candidate_parts) - 1):
+            index_url, _, _ = index_url.rstrip("/").rpartition("/")
 
-    candidate_parts = candidate.split("..")
-    last = candidate_parts[-1]
-    for _ in range(len(candidate_parts) - 1):
-        index_url, _, _ = index_url.rstrip("/").rpartition("/")
+        return "{}/{}".format(index_url, last.strip("/"))
 
-    return "{}/{}".format(index_url, last.strip("/"))
+    # relative path without up-references
+    return "{}/{}".format(index_url, candidate)
diff --git a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl
index aa735b8..d3c42a8 100644
--- a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl
+++ b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl
@@ -255,6 +255,40 @@
                 yanked = False,
             ),
         ),
+        (
+            struct(
+                attrs = [
+                    'href="1.0.0/mypy_extensions-1.0.0-py3-none-any.whl#sha256=deadbeef"',
+                ],
+                filename = "mypy_extensions-1.0.0-py3-none-any.whl",
+                url = "https://example.org/simple/mypy_extensions",
+            ),
+            struct(
+                filename = "mypy_extensions-1.0.0-py3-none-any.whl",
+                metadata_sha256 = "",
+                metadata_url = "",
+                sha256 = "deadbeef",
+                url = "https://example.org/simple/mypy_extensions/1.0.0/mypy_extensions-1.0.0-py3-none-any.whl",
+                yanked = False,
+            ),
+        ),
+        (
+            struct(
+                attrs = [
+                    'href="unknown://example.com/mypy_extensions-1.0.0-py3-none-any.whl#sha256=deadbeef"',
+                ],
+                filename = "mypy_extensions-1.0.0-py3-none-any.whl",
+                url = "https://example.org/simple/mypy_extensions",
+            ),
+            struct(
+                filename = "mypy_extensions-1.0.0-py3-none-any.whl",
+                metadata_sha256 = "",
+                metadata_url = "",
+                sha256 = "deadbeef",
+                url = "https://example.org/simple/mypy_extensions/unknown://example.com/mypy_extensions-1.0.0-py3-none-any.whl",
+                yanked = False,
+            ),
+        ),
     ]
 
     for (input, want) in tests: