Repository: package-url/packageurl-python

Issue:
Creation PackageURL objects for invalid PURLs / Possible improper encoding of colons (":") in PURL fields
It is possible to create PackageURL objects that contain invalid fields, specifically by using the `PackageURL` kwarg constructor and passing in values that contain colons.

Simple example:
```
>>> from packageurl import PackageURL
>>> p = PackageURL(type="generic", name="Foo: <Bar>", version="1.2.3")
>>> p.to_string()
'pkg:generic/Foo:%20%3CBar%3E@1.2.3'
>>> PackageURL.from_string(p.to_string())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/vossn/finitestate/finite-state-sip/venv/lib/python3.10/site-packages/packageurl/__init__.py", line 514, in from_string
    raise ValueError(msg)
ValueError: Invalid purl 'pkg:generic/Foo:%20%3CBar%3E@1.2.3' cannot contain a "user:pass@host:port" URL Authority component: ''.
```

On closer inspection, it looks like the problem might be that colons (`:`) are not being percent-encoded correctly? I would expect the colon in the name to be encoded to `%3A`, but it looks like it is being left as a literal `:` in the `to_string()` function:
```
>>> p = PackageURL(type="generic", name="Foo: <Bar>", version="1.2.3")
>>> p
PackageURL(type='generic', namespace=None, name='Foo: <Bar>', version='1.2.3', qualifiers={}, subpath=None)
>>> p.to_string()
'pkg:generic/Foo:%20%3CBar%3E@1.2.3'
```

I'm not sure I'm interpreting the [PURL spec](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst) correctly with regards to the treatment of colon characters, but on the surface it sounds like any colon appearing within an individual field value should simply be percent-encoded if the field itself calls for it.

Discussion hints:

Solution patch:
diff --git a/src/packageurl/__init__.py b/src/packageurl/__init__.py
index 8199e39..9677b73 100644
--- a/src/packageurl/__init__.py
+++ b/src/packageurl/__init__.py
@@ -459,12 +459,17 @@ class PackageURL(
             url=remainder, scheme="", allow_fragments=True
         )
 
-        if scheme or authority:
-            msg = (
-                f'Invalid purl {purl!r} cannot contain a "user:pass@host:port" '
-                f"URL Authority component: {authority!r}."
-            )
-            raise ValueError(msg)
+        # The spec (seems) to allow colons in the name and namespace.
+        # urllib.urlsplit splits on : considers them parts of scheme
+        # and authority.
+        # Other libraries do not care about this.
+        # See https://github.com/package-url/packageurl-python/issues/152#issuecomment-2637692538
+        # We do + ":" + to put the colon back that urlsplit removed.
+        if authority:
+            path = authority + ":" + path
+
+        if scheme:
+            path = scheme + ":" + path
 
         path = path.lstrip("/")
 


Test patch:
diff --git a/tests/test_packageurl.py b/tests/test_packageurl.py
index cb419e2..5d14220 100644
--- a/tests/test_packageurl.py
+++ b/tests/test_packageurl.py
@@ -330,3 +330,47 @@ class NormalizePurlTest(unittest.TestCase):
 def test_purl_is_hashable():
     s = {PackageURL(name="hashable", type="pypi")}
     assert len(s) == 1
+
+
+def test_colons_in_name_are_handled_correctly() -> None:
+    p = PackageURL.from_string(
+        "pkg:nuget/libiconv:%20character%20set%20conversion%20library@1.9?package-id=e11a609df352e292"
+    )
+
+    assert p.type == "nuget"
+    assert p.namespace is None
+    assert p.name == "libiconv: character set conversion library"
+    assert p.version == "1.9"
+    assert p.qualifiers == {"package-id": "e11a609df352e292"}
+    assert p.subpath is None
+
+    assert PackageURL.from_string(p.to_string()).to_string() == p.to_string()
+
+
+def test_colons_in_namespace_are_handled_correctly() -> None:
+    p = PackageURL.from_string(
+        "pkg:nuget/an:odd:space/libiconv:%20character%20set%20conversion%20library@1.9?package-id=e11a609df352e292"
+    )
+
+    assert p.type == "nuget"
+    assert p.namespace == "an:odd:space"
+    assert p.name == "libiconv: character set conversion library"
+    assert p.version == "1.9"
+    assert p.qualifiers == {"package-id": "e11a609df352e292"}
+    assert p.subpath is None
+
+    assert PackageURL.from_string(p.to_string()).to_string() == p.to_string()
+
+
+def test_encoding_stuff_with_colons_correctly() -> None:
+    p = PackageURL(
+        type="nuget",
+        namespace="an:odd:space",
+        name="libiconv: character set conversion library",
+        version="1.9",
+        qualifiers={"package-id": "e11a609df352e292"},
+    )
+    assert (
+        p.to_string()
+        == "pkg:nuget/an:odd:space/libiconv:%20character%20set%20conversion%20library@1.9?package-id=e11a609df352e292"
+    )