Skip to content

Commit fc37c70

Browse files
committed
Fix Go package PURL mapping by normalizing percent-encoding in plain PURL comparison
Signed-off-by: Ajay Rajera <newajay.11r@gmail.com>
1 parent 0e6951a commit fc37c70

4 files changed

Lines changed: 78 additions & 5 deletions

File tree

component_catalog/tests/test_models.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2628,7 +2628,7 @@ def test_package_model_get_purldb_entries(self, mock_find_packages):
26282628

26292629
mock_find_packages.return_value = [purldb_entry1, purldb_entry2, purldb_entry3]
26302630
purldb_entries = package1.get_purldb_entries(user=self.user)
2631-
# The purldb_entry2 is excluded as the PURL differs
2631+
# The purldb_entry3 is excluded as its plain PURL differs (no version)
26322632
self.assertEqual([purldb_entry1, purldb_entry2], purldb_entries)
26332633

26342634
@mock.patch("dejacode_toolkit.purldb.PurlDB.find_packages")
@@ -2645,6 +2645,42 @@ def test_package_model_get_purldb_entries_plain_purls_equal(self, mock_find_pack
26452645
purldb_entries = package1.get_purldb_entries(user=self.user)
26462646
self.assertEqual([purldb_entry1, purldb_entry2], purldb_entries)
26472647

2648+
@mock.patch("dejacode_toolkit.purldb.PurlDB.find_packages")
2649+
def test_package_model_get_purldb_entries_fallback_to_purl(self, mock_find_packages):
2650+
"""
2651+
Test that get_purldb_entries falls through to PURL lookup when an earlier
2652+
strategy (e.g. download_url) returns no results.
2653+
2654+
This covers the Go package mapping bug (issue #462): packages imported from an
2655+
SBOM may have an inferred download_url that PurlDB does not recognise, while
2656+
their PURL *is* indexed in PurlDB. Without the fallback the PurlDB tab was
2657+
showing "No entries found" even though "Improve Packages from PurlDB" worked
2658+
(because that code path does not limit the number of requests).
2659+
"""
2660+
go_purl = "pkg:golang/github.com/gin-gonic/gin@v1.9.0"
2661+
inferred_download_url = (
2662+
"https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.0.zip"
2663+
)
2664+
package1 = make_package(
2665+
self.dataspace,
2666+
package_url=go_purl,
2667+
download_url=inferred_download_url,
2668+
)
2669+
purldb_entry = {
2670+
"purl": go_purl,
2671+
"type": "golang",
2672+
"namespace": "github.com/gin-gonic",
2673+
"name": "gin",
2674+
"version": "v1.9.0",
2675+
}
2676+
2677+
# Simulate: download_url lookup fails (None), PURL lookup succeeds.
2678+
mock_find_packages.side_effect = [None, [purldb_entry]]
2679+
purldb_entries = package1.get_purldb_entries(user=self.user)
2680+
self.assertEqual([purldb_entry], purldb_entries)
2681+
# Ensure both payloads were tried (download_url first, then purl).
2682+
self.assertEqual(2, mock_find_packages.call_count)
2683+
26482684
@mock.patch("component_catalog.models.Package.get_purldb_entries")
26492685
def test_package_model_update_from_purldb(self, mock_get_purldb_entries):
26502686
purldb_entry = {

component_catalog/views.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2546,7 +2546,6 @@ def get_tab_fields(self):
25462546

25472547
purldb_entries = self.object.get_purldb_entries(
25482548
user=self.request.user,
2549-
max_request_call=1,
25502549
timeout=5,
25512550
)
25522551
if not purldb_entries:

dje/tests/test_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,12 +531,19 @@ def test_utils_is_purl_fragment(self):
531531
def test_utils_get_plain_purl(self):
532532
self.assertEqual("", get_plain_purl(None))
533533
self.assertEqual("", get_plain_purl(""))
534+
# Invalid PURLs fall back to simple string split (no exception raised)
534535
self.assertEqual("not:a/purl", get_plain_purl("not:a/purl"))
535536
self.assertEqual("not:a/purl", get_plain_purl("not:a/purl"))
536537
self.assertEqual("pkg:npm/is-npm@1.0.0", get_plain_purl("pkg:npm/is-npm@1.0.0"))
537538
self.assertEqual(
538539
"pkg:npm/is-npm@1.0.0", get_plain_purl("pkg:npm/is-npm@1.0.0?qualifier=1#frament")
539540
)
541+
# Percent-encoded and literal forms of the same version compare equal.
542+
# "+" in a version is encoded as "%2B" by PackageURL.to_string().
543+
self.assertEqual(
544+
get_plain_purl("pkg:golang/github.com/foo/bar@v1.0.0%2Bincompatible"),
545+
get_plain_purl("pkg:golang/github.com/foo/bar@v1.0.0+incompatible"),
546+
)
540547

541548
def test_utils_plain_purls_equal(self):
542549
purl1 = "pkg:npm/is-npm@1.0.0"
@@ -555,6 +562,17 @@ def test_utils_plain_purls_equal(self):
555562
purl2 = "pkg:npm/is-npm@2.0.0"
556563
self.assertFalse(plain_purls_equal(purl1, purl2))
557564

565+
# Go packages: percent-encoded "+" vs literal "+" in +incompatible versions
566+
# must be treated as equal (issue #462).
567+
purl1 = "pkg:golang/github.com/docker/docker@v19.03.15%2Bincompatible"
568+
purl2 = "pkg:golang/github.com/docker/docker@v19.03.15+incompatible"
569+
self.assertTrue(plain_purls_equal(purl1, purl2))
570+
571+
# Different versions must still be not-equal.
572+
purl1 = "pkg:golang/github.com/docker/docker@v19.03.15+incompatible"
573+
purl2 = "pkg:golang/github.com/docker/docker@v20.10.0+incompatible"
574+
self.assertFalse(plain_purls_equal(purl1, purl2))
575+
558576
def test_utils_localized_datetime(self):
559577
self.assertIsNone(localized_datetime(None))
560578

dje/utils.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -663,17 +663,37 @@ def is_purl_fragment(string):
663663

664664

665665
def get_plain_purl(purl_str):
666-
"""Remove the qualifiers and subpath from the `purl_str```."""
666+
"""
667+
Remove the qualifiers and subpath from the ``purl_str``.
668+
669+
The comparison is normalised through ``PackageURL.from_string`` so that
670+
percent-encoded variants of the same PURL are treated as equal
671+
(e.g. ``@v1.0.0%2Bincompatible`` and ``@v1.0.0+incompatible``).
672+
673+
Falls back to a simple ``split("?")`` strip when the string is not a
674+
valid PURL (so callers never get an unexpected exception).
675+
"""
667676
if not purl_str:
668677
return ""
669-
return purl_str.split("?")[0]
678+
try:
679+
purl = PackageURL.from_string(str(purl_str))
680+
# Re-serialise without qualifiers or subpath for a canonical plain PURL.
681+
return purl.to_string().split("?")[0].split("#")[0]
682+
except ValueError:
683+
return str(purl_str).split("?")[0]
670684

671685

672686
def plain_purls_equal(purl1, purl2):
673-
"""Check if two PURLs are equal, ignoring qualifiers and subpath."""
687+
"""Check if two PURLs are equal, ignoring qualifiers and subpath.
688+
689+
Percent-encoding differences (e.g. ``+`` vs ``%2B``) are normalised
690+
before the comparison so semantically identical PURLs are always
691+
considered equal.
692+
"""
674693
return get_plain_purl(purl1) == get_plain_purl(purl2)
675694

676695

696+
677697
def remove_empty_values(input_dict):
678698
"""
679699
Return a new dict not including empty value entries from `input_dict`.

0 commit comments

Comments
 (0)