Skip to content

fix(purl): encode opam, swift and conan PURL components - #1350

Merged
mstykow merged 3 commits into
mainfrom
fix/encode-opam-and-conan-purls
Aug 11, 2026
Merged

fix(purl): encode opam, swift and conan PURL components#1350
mstykow merged 3 commits into
mainfrom
fix/encode-opam-and-conan-purls

Conversation

@mstykow

@mstykow mstykow commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Three parsers assembled PURLs with format!, splicing unvalidated names straight in. The results either fail to parse or silently change meaning.
  • opam is the worst: names come from a quoted field, so anything but a quote reaches the PURL. ocaml/evil was reinterpreted as namespace ocaml + name evil; sharp#frag gained a subpath; conf gmp kept a raw space; and an already-encoded pct%20 decoded to pct rather than the literal name.
  • These reach dependencies[].purl and dependency_uid, which is the output surface nothing downstream re-encodes — assembled packages[] identities are rebuilt through the crate regardless of type, so package-level sites were already masked. Dependency-level sites are not.

Scope and exclusions

  • Included: opam (package identity + dependencies), swift show-dependencies (root + dependency), conan references. Two shared helpers, simple_purl and namespaced_purl, so the next conversion is not another hand-rolled string.
  • Explicit exclusions: types the crate rewrites rather than merely encodes — it lowercases names for bitbucket, deb, github, hex, npm and pypi. Converting those (notably .gitmodules, which emits pkg:github/…) is a case-sensitivity decision, not a mechanical swap, so it is deliberately left out.

How to verify

printf 'opam-version: "2.0"\nname: "my pkg"\nversion: "1.0 beta"\ndepends: [\n "conf gmp" {>= "3"}\n "ocaml/evil"\n "sharp#frag"\n]\n' > /tmp/o/demo.opam
provenant --package --json-pp - /tmp/o | jq '[.dependencies[].purl]'

Before: pkg:opam/conf gmp, pkg:opam/ocaml/evil, pkg:opam/sharp#frag. After: pkg:opam/conf%20gmp, pkg:opam/ocaml%2Fevil, pkg:opam/sharp%23frag — each parses, round-trips byte-identically, and decodes back to the declared name.

The pct%20 case is the interesting one: already-percent-encoded text is data, so the real name is the six literal characters and the correct PURL is pkg:opam/pct%2520.

Intentional differences from Python

  • None; ScanCode builds these through packageurl-python, which encodes on construction.

Expected-output fixture changes

  • None across all golden suites. No fixture used a name needing encoding, which is why this went unnoticed; covered by new unit tests that assert the encoded form, parse it back, and check the decoded components and round trip.

mstykow and others added 3 commits August 11, 2026 23:49
opam names come from a quoted field, so anything other than a quote reached the
PURL verbatim. Splicing them in with `format!` produced strings that either fail
to parse or silently change meaning: `ocaml/evil` was reinterpreted as namespace
`ocaml` plus name `evil`, `sharp#frag` gained a subpath, `conf gmp` kept a raw
space, and an already-encoded `pct%20` decoded to `pct ` instead of the literal
name.

Build both the package identity and the dependency PURLs through the crate's
encoder via a shared `simple_purl` helper for namespace-free types. Every
component now survives a round trip and decodes back to the declared text.

The helper deliberately excludes the types the crate rewrites — it lowercases
names for bitbucket, deb, github, hex, npm and pypi — so those conversions need a
decision about case rather than a mechanical swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Maxim Stykow <maxim.stykow@gmail.com>
Package and repository names were spliced into `pkg:swift/{namespace}/{name}`
with `format!`, so a name containing a space or any other reserved character
produced a PURL that did not survive a round trip.

Build through the crate's encoder via a `namespaced_purl` helper, which keeps the
namespace's `/` separators — its segments are path parts — while encoding the
name and version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Maxim Stykow <maxim.stykow@gmail.com>
A reference carrying a range constraint yields no PURL version, so it took the
hand-formatted branch and spliced the name in unencoded — `my pkg/[>=1.0]`
produced `pkg:conan/my pkg`, which does not survive a round trip.

Route both the versioned and name-only forms through the shared encoder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Maxim Stykow <maxim.stykow@gmail.com>
@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces hand-formatted opam, Conan, and Swift PURLs with shared packageurl-backed constructors so names, namespaces, and versions are encoded without changing their meaning.

  • Adds shared constructors for simple and namespaced PURLs.
  • Routes opam package and dependency identities through the encoder.
  • Encodes Conan references, including name-only references produced for version ranges.
  • Encodes Swift root and dependency PURL components.
  • Adds round-trip regression coverage for special characters and literal percent-encoded text.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code failure identified.

The new constructors preserve the existing parser component choices while applying packageurl validation and encoding, and the investigated omission and identity risks were either pre-existing, invalid-output cases, or lacked a realistic reachable trigger.

Important Files Changed

Filename Overview
src/parsers/utils.rs Adds centralized packageurl-backed helpers that trim required components, encode values, and return no PURL when construction fails.
src/parsers/opam.rs Replaces manual package and dependency PURL formatting with encoded construction and adds component round-trip tests.
src/parsers/conan.rs Routes exact, ranged, and name-only Conan references through the shared encoder.
src/parsers/conan_test.rs Adds regression coverage for encoded ranged-reference names and canonical PURL round trips.
src/parsers/swift_show_dependencies.rs Uses the namespaced helper for Swift root and dependency PURLs while preserving existing URL-derived identity behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Parser component values] --> B{PURL shape}
    B -->|No namespace| C[simple_purl]
    B -->|Namespace required| D[namespaced_purl]
    C --> E[packageurl validation and encoding]
    D --> E
    E --> F[Canonical dependency or package PURL]
Loading

Reviews (1): Last reviewed commit: "fix(conan): encode reference PURL compon..." | Re-trigger Greptile

@mstykow
mstykow merged commit a2b4a28 into main Aug 11, 2026
13 checks passed
@mstykow
mstykow deleted the fix/encode-opam-and-conan-purls branch August 11, 2026 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant