Purl to v3 - #51
Merged
Merged
Conversation
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
marked this pull request as ready for review
July 7, 2026 18:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR backports the purl module (PURL →
Locatorconversion) 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.rsand its 16 ecosystem submodules, withEcosystemrenamed toFetcher(v3's name for the same concept):purl.rs:ecosystem()is renamedfetcher()with a one-to-one variant mapping, plus the sharedrevisionhelper and its regression tests.You can see that in this diff:
Almost all of the changes are renaming Ecosystem -> Fetcher.
There is one largish behaviour change. v4 builds revisions with a fallible parse that returns
Nonefor empty input. v3 has no fallible parse, and its infallibleRevision::fromwould instead produceSome(Opaque("")).For example,
pkg:apk/alpine/curlhas no version and noarchqualifier, so the apk/deb/rpm revision (built by joining whichever ofarch/versionare present with#) comes out as the empty string: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()insrc/purl.rs— which trims and maps empty toNone, reproducing v4's observable behaviour. This also matches Core, whoseLocatorclass 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 leadingvbefore comparing revisions, sov1.2.3 == 1.2.3. v3 classifiesv1.2.3asOpaque, which never equalsSemver(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 v4HashMap<Locator, _>but different keys in v3: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,v2and2can be different refs. Consumers migrating off v4 inherit strict string equality.Fetcher JSON names
Equivalence testing caught
Maven/SourceForge/StackOverflowserializing 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 neededrename.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
Fetcherand 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.
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
ficus/src/strategy/vendetta/solver.rs(parse_locator)Checklist