Skip to content

Purl to v3 - #51

Merged
spatten merged 10 commits into
v3-maintenancefrom
purl-to-v3
Jul 7, 2026
Merged

Purl to v3#51
spatten merged 10 commits into
v3-maintenancefrom
purl-to-v3

Conversation

@spatten

@spatten spatten commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR backports the purl module (PURL → Locator conversion) from the v4 line to v3. The v4 line is being retired: its only active consumer is ficus, and purl conversion (#36) is the only v4-specific functionality ficus uses. This backport is the last piece ficus needs to migrate (fossas/ficus#180), after which nothing depends on v4.

This releases as v5.0.0 on main. It's purely additive over v3, but backwards-incompatible with v4, so we'll bump the main version to v5.

This is a direct copy of v4's locator/src/purl.rs and its 16 ecosystem submodules, with Ecosystem renamed to Fetcher (v3's name for the same concept):

  • Each submodule differs from the version on v4 by 3 lines: the import, the builder call, and the revision line (below).
  • purl.rs: ecosystem() is renamed fetcher() with a one-to-one variant mapping, plus the shared revision helper and its regression tests.
  • The v4 purl test suite is ported along with the code.

You can see that in this diff:

git fetch origin v4-maintenance purl-to-v3
diff <(git show origin/v4-maintenance:locator/src/purl.rs) <(git show origin/purl-to-v3:src/purl.rs)

Almost all of the changes are renaming Ecosystem -> Fetcher.

There is one largish behaviour change. v4 builds revisions with a fallible parse that returns None for empty input. v3 has no fallible parse, and its infallible Revision::from would instead produce Some(Opaque("")).

For example, pkg:apk/alpine/curl has no version and no arch qualifier, so the apk/deb/rpm revision (built by joining whichever of arch/version are present with #) comes out as the empty string:

pkg:apk/alpine/curl  →  apk+curl#alpine   (v4, Core, and this PR)
                     →  apk+curl#alpine$  (naive port: trailing $, doesn't round-trip through Locator::parse)

A purl whose version is only whitespace (pkg:npm/foobar@%20, version = " ") has the same problem.

To handle this, every conversion routes through one shared helper — revision() in src/purl.rs — which trims and maps empty to None, reproducing v4's observable behaviour. This also matches Core, whose Locator class coerces an empty revision to null.

Risks

v-prefixed revisions This is a pre-existing difference between the v3 and v4 branches. v4 strips a leading v before comparing revisions, so v1.2.3 == 1.2.3. v3 classifies v1.2.3 as Opaque, which never equals Semver(1.2.3) — v3 equality is effectively string equality for v-prefixed versions.

Rendered locator strings are identical on both branches; the difference is only observable in in-memory comparison (Eq/Ord/Hash). For example, these two locators are the same key in a v4 HashMap<Locator, _> but different keys in v3:

git+github.com/foo/bar$v1.2.3
git+github.com/foo/bar$1.2.3

This stays as-is, for two reasons: v3's string-equality semantics match how Core compares locators, and v-stripping is unsafe exactly where prefixes occur — for git and go, v2 and 2 can be different refs. Consumers migrating off v4 inherit strict string equality.

Fetcher JSON names

Equivalence testing caught Maven/SourceForge/StackOverflow serializing to JSON as their snake_case variant names ("maven", "source_forge", "stack_overflow") instead of their locator-string names ("mvn", "sourceforge", "stackoverflow"). They had #[serde(alias)] — which only affects deserialization — where they needed rename.

This PR renames them. The old spellings remain as deserialization aliases so existing payloads still parse, and a regression test pins every Fetcher's serde name to its strum name.

This is a safe change because Sparkle never serializes a Fetcher and already parses both spellings; ficus's serialized output becomes identical to what it emits on v4 today.

Testing plan

To test, run against something that Vendetta finds vendored deps in. I used the deps directory of github.com/redis/redis. The test passes if the v3 and v4 versions give the same result.

export FOSSA_API_TOKEN=<token>
cd ~/fossa/ficus
git checkout main && cargo build --release && cp target/release/ficus /tmp/ficus-v4
git checkout scott/locator-purl-to-v3 && cargo build --release && cp target/release/ficus /tmp/ficus-v3

git clone --depth 1 https://github.com/redis/redis /tmp/redis && cd /tmp/redis/deps

/tmp/ficus-v4 analyze --strategy vendetta --locator 'custom+equiv-test$v4' > /tmp/v4.ndjson 2> /tmp/v4.log
/tmp/ficus-v3 analyze --strategy vendetta --locator 'custom+equiv-test$v3' > /tmp/v3.ndjson 2> /tmp/v3.log

jq -c 'select(.observation.strategy=="vendetta" and .observation.kind=="finding") | .observation.payload' /tmp/v4.ndjson | sort > /tmp/v4-findings.txt
jq -c 'select(.observation.strategy=="vendetta" and .observation.kind=="finding") | .observation.payload' /tmp/v3.ndjson | sort > /tmp/v3-findings.txt

There should be no difference between the results for v4 and v3, so /tmp/v4-findings.txt and /tmp/v3-findings.txt should be exactly the same.

Metrics

Not applicable — this is a library change with no runtime surface of its own; consumers pin tags.

References

  • Add PURL support #36: original purl support on the v4 line
  • fossas/ficus#180: the ficus migration that consumes this backport
  • Ficus's usage of this API: ficus/src/strategy/vendetta/solver.rs (parse_locator)

Checklist

  • I added tests for this PR's change (the v4 purl test suite is ported alongside the module).

spatten and others added 10 commits July 2, 2026 16:25
First step of backporting v4's purl module to v3: pull in the same
`purl = { version = "0.1.6", features = ["serde"] }` dep that v4 uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports locator/src/purl.rs from v4.2.1, mechanically renaming
`Ecosystem`→`Fetcher`, `.ecosystem(...)`→`.fetcher(...)`, and replacing
`Revision::parse(v).ok()` with `Revision::from(v)` (v3's Revision
conversion is infallible). Tests are included but will not compile until
the submodule ports land in the next task. Not added to `lib.rs` yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports the 16 per-ecosystem purl submodules from v4.2.1. Each file
replaces `crate::ecosystems::<ZST>` imports with `crate::Fetcher::<variant>`
at the builder call site, and swaps `Revision::parse(v).ok()` for
`Revision::from(v)` since v3's Revision conversion is infallible.

Verified: all 52 `purl::tests` pass when `pub mod purl;` is added to
lib.rs; that declaration is deferred to the next task per the plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Maven, SourceForge, and StackOverflow serialized to JSON as their
snake_case variant names (maven, source_forge, stack_overflow) instead
of their locator-string names (mvn, sourceforge, stackoverflow), because
they used serde alias (deserialize-only) instead of rename. Consumers
serializing a Fetcher would emit names that other tooling, expecting
locator names, does not recognize.

Rename them to the locator-string names, keep the old spellings as
deserialization aliases so existing payloads still parse, and add a
regression test asserting every Fetcher's serde name matches its strum
name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v4 built revisions with the fallible Revision::parse, which fails only on
input that is empty after trimming. The port's infallible Revision::from
diverged on that edge: a whitespace-only purl version, or an apk/deb/rpm
purl with neither version nor arch (whose joined revision string is empty),
produced Some(Opaque("")) — rendering a trailing '$' that does not
round-trip through Locator::parse — where v4 produced no revision.

Replace the per-file construction with one shared helper that trims and
maps empty to None, restoring v4's observable behavior at all 17 sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Released as a new major from the v3 line: the API is additive over v3,
but the version supersedes the retired v4 line so future major versions
never collide with the abandoned v4 tags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@spatten
spatten requested a review from nficca July 7, 2026 18:46
@spatten
spatten marked this pull request as ready for review July 7, 2026 18:46

@nficca nficca left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

@spatten
spatten merged commit 01538b6 into v3-maintenance Jul 7, 2026
10 checks passed
@spatten
spatten deleted the purl-to-v3 branch July 7, 2026 21:46
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.

2 participants