Skip to content

Commit 4e7f273

Browse files
committed
Fix resolve_relative_url to handle multi-level relative paths
Signed-off-by: Kai Hodžić <hodzic.e.k@outlook.com>
1 parent 7b61dde commit 4e7f273

2 files changed

Lines changed: 37 additions & 15 deletions

File tree

src/python_inspector/utils_pypi.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from typing import Union
2727
from urllib.parse import quote_plus
2828
from urllib.parse import unquote
29+
from urllib.parse import urljoin
2930
from urllib.parse import urlparse
3031
from urllib.parse import urlunparse
3132

@@ -1631,26 +1632,21 @@ async def fetch_links(
16311632

16321633
def resolve_relative_url(package_url, url):
16331634
"""
1634-
Return the resolved `url` URLstring given a `package_url` base URL string
1635+
Return the resolved `url` URL string given a `package_url` base URL string
16351636
of a package.
16361637
1638+
Per PEP 503, links in the simple index may be relative. Use stdlib urljoin
1639+
which correctly handles multi-level '../' traversal and path normalization.
1640+
16371641
For example:
1638-
>>> resolve_relative_url("https://example.com/package", "../path/file.txt")
1642+
>>> resolve_relative_url("https://example.com/package/", "../path/file.txt")
16391643
'https://example.com/path/file.txt'
1644+
>>> resolve_relative_url("https://example.com/simple/pkg/", "../../packages/file.whl")
1645+
'https://example.com/packages/file.whl'
1646+
>>> resolve_relative_url("https://example.com/a/b/c/", "https://other.com/file.whl")
1647+
'https://other.com/file.whl'
16401648
"""
1641-
if not url.startswith(("http://", "https://")):
1642-
base_url_parts = urlparse(package_url)
1643-
url_parts = urlparse(url)
1644-
# If the relative URL starts with '..', remove the last directory from the base URL
1645-
if url_parts.path.startswith(".."):
1646-
path = base_url_parts.path.rstrip("/").rsplit("/", 1)[0] + url_parts.path[2:]
1647-
else:
1648-
path = urlunparse(
1649-
("", "", url_parts.path, url_parts.params, url_parts.query, url_parts.fragment)
1650-
)
1651-
resolved_url_parts = base_url_parts._replace(path=path)
1652-
url = urlunparse(resolved_url_parts)
1653-
return url
1649+
return urljoin(package_url, url)
16541650

16551651

16561652
################################################################################

tests/test_utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from python_inspector.resolution import fetch_and_extract_sdist
2424
from python_inspector.utils import get_netrc_auth
2525
from python_inspector.utils_pypi import PypiSimpleRepository
26+
from python_inspector.utils_pypi import resolve_relative_url
2627
from python_inspector.utils_pypi import valid_python_version
2728

2829
test_env = FileDrivenTesting()
@@ -164,3 +165,28 @@ def test_parse_reqs_with_setup_requires_and_python_requires():
164165
def test_valid_python_version():
165166
assert valid_python_version("3.8", ">3.1")
166167
assert not valid_python_version("3.8.1", ">3.9")
168+
169+
170+
def test_resolve_relative_url_multi_level():
171+
base = "https://example.com/api/pypi/repo/simple/pkg/"
172+
rel = "../../packages/packages/d9/0b/hash/file-1.0-cp310-linux.whl"
173+
result = resolve_relative_url(base, rel)
174+
assert (
175+
result
176+
== "https://example.com/api/pypi/repo/packages/packages/d9/0b/hash/file-1.0-cp310-linux.whl"
177+
)
178+
assert "/../" not in result
179+
180+
181+
def test_resolve_relative_url_single_level():
182+
base = "https://example.com/simple/pkg/"
183+
rel = "../other/file.whl"
184+
result = resolve_relative_url(base, rel)
185+
assert result == "https://example.com/simple/other/file.whl"
186+
187+
188+
def test_resolve_relative_url_absolute():
189+
base = "https://example.com/simple/pkg/"
190+
rel = "https://files.pythonhosted.org/packages/file.whl"
191+
result = resolve_relative_url(base, rel)
192+
assert result == "https://files.pythonhosted.org/packages/file.whl"

0 commit comments

Comments
 (0)