Skip to content

Commit 2a50e2a

Browse files
committed
feat: fix parsing of names and namespaces with colons
This attempts to comply with the specification. Closes package-url#152 (I think) See discussion at package-url#152 Signed-off-by: Joshua Kugler <[email protected]>
1 parent 6f38e3e commit 2a50e2a

File tree

2 files changed

+71
-14
lines changed

2 files changed

+71
-14
lines changed

src/packageurl/__init__.py

+27-14
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ def unquote(s: AnyStr) -> str:
7777

7878

7979
@overload
80-
def get_quoter(encode: bool = True) -> Callable[[AnyStr], str]: ...
80+
def get_quoter(encode: bool = True) -> Callable[[AnyStr], str]:
81+
...
8182

8283

8384
@overload
84-
def get_quoter(encode: None) -> Callable[[str], str]: ...
85+
def get_quoter(encode: None) -> Callable[[str], str]:
86+
...
8587

8688

8789
def get_quoter(encode: bool | None = True) -> Callable[[AnyStr], str] | Callable[[str], str]:
@@ -151,19 +153,22 @@ def normalize_version(version: AnyStr | None, encode: bool | None = True) -> str
151153
@overload
152154
def normalize_qualifiers(
153155
qualifiers: AnyStr | dict[str, str] | None, encode: Literal[True] = ...
154-
) -> str | None: ...
156+
) -> str | None:
157+
...
155158

156159

157160
@overload
158161
def normalize_qualifiers(
159162
qualifiers: AnyStr | dict[str, str] | None, encode: Literal[False] | None
160-
) -> dict[str, str]: ...
163+
) -> dict[str, str]:
164+
...
161165

162166

163167
@overload
164168
def normalize_qualifiers(
165169
qualifiers: AnyStr | dict[str, str] | None, encode: bool | None = ...
166-
) -> str | dict[str, str] | None: ...
170+
) -> str | dict[str, str] | None:
171+
...
167172

168173

169174
def normalize_qualifiers(
@@ -251,7 +256,8 @@ def normalize(
251256
qualifiers: AnyStr | dict[str, str] | None,
252257
subpath: AnyStr | None,
253258
encode: Literal[True] = ...,
254-
) -> tuple[str, str | None, str, str | None, str | None, str | None]: ...
259+
) -> tuple[str, str | None, str, str | None, str | None, str | None]:
260+
...
255261

256262

257263
@overload
@@ -263,7 +269,8 @@ def normalize(
263269
qualifiers: AnyStr | dict[str, str] | None,
264270
subpath: AnyStr | None,
265271
encode: Literal[False] | None,
266-
) -> tuple[str, str | None, str, str | None, dict[str, str], str | None]: ...
272+
) -> tuple[str, str | None, str, str | None, dict[str, str], str | None]:
273+
...
267274

268275

269276
@overload
@@ -275,7 +282,8 @@ def normalize(
275282
qualifiers: AnyStr | dict[str, str] | None,
276283
subpath: AnyStr | None,
277284
encode: bool | None = ...,
278-
) -> tuple[str, str | None, str, str | None, str | dict[str, str] | None, str | None]: ...
285+
) -> tuple[str, str | None, str, str | None, str | dict[str, str] | None, str | None]:
286+
...
279287

280288

281289
def normalize(
@@ -459,12 +467,17 @@ def from_string(cls, purl: str) -> Self:
459467
url=remainder, scheme="", allow_fragments=True
460468
)
461469

462-
if scheme or authority:
463-
msg = (
464-
f'Invalid purl {purl!r} cannot contain a "user:pass@host:port" '
465-
f"URL Authority component: {authority!r}."
466-
)
467-
raise ValueError(msg)
470+
# The spec (seems) to allow colons in the name and namespace.
471+
# urllib.urlsplit splits on : considers them parts of scheme
472+
# and authority.
473+
# Other libraries do no care about this.
474+
# See https://github.com/package-url/packageurl-python/issues/152#issuecomment-2637692538
475+
# We do + ":" + to put the colon back that urlsplit removed.
476+
if authority:
477+
path = authority + ":" + path
478+
479+
if scheme:
480+
path = scheme + ":" + path
468481

469482
path = path.lstrip("/")
470483

tests/test_packageurl.py

+44
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,47 @@ def test_to_dict_custom_empty_value(self):
330330
def test_purl_is_hashable():
331331
s = {PackageURL(name="hashable", type="pypi")}
332332
assert len(s) == 1
333+
334+
335+
def test_colons_in_name_are_handled_correctly() -> None:
336+
p = PackageURL.from_string(
337+
"pkg:nuget/libiconv:%20character%20set%20conversion%[email protected]?package-id=e11a609df352e292"
338+
)
339+
340+
assert p.type == "nuget"
341+
assert p.namespace is None
342+
assert p.name == "libiconv: character set conversion library"
343+
assert p.version == "1.9"
344+
assert p.qualifiers == {"package-id": "e11a609df352e292"}
345+
assert p.subpath == None
346+
347+
assert PackageURL.from_string(p.to_string()).to_string() == p.to_string()
348+
349+
350+
def test_colons_in_namespace_are_handled_correctly() -> None:
351+
p = PackageURL.from_string(
352+
"pkg:nuget/an:odd:space/libiconv:%20character%20set%20conversion%[email protected]?package-id=e11a609df352e292"
353+
)
354+
355+
assert p.type == "nuget"
356+
assert p.namespace == "an:odd:space"
357+
assert p.name == "libiconv: character set conversion library"
358+
assert p.version == "1.9"
359+
assert p.qualifiers == {"package-id": "e11a609df352e292"}
360+
assert p.subpath == None
361+
362+
assert PackageURL.from_string(p.to_string()).to_string() == p.to_string()
363+
364+
365+
def test_encoding_stuff_with_colons_correctly() -> None:
366+
p = PackageURL(
367+
type="nuget",
368+
namespace="an:odd:space",
369+
name="libiconv: character set conversion library",
370+
version="1.9",
371+
qualifiers={"package-id": "e11a609df352e292"},
372+
)
373+
assert (
374+
p.to_string()
375+
== "pkg:nuget/an:odd:space/libiconv:%20character%20set%20conversion%[email protected]?package-id=e11a609df352e292"
376+
)

0 commit comments

Comments
 (0)