Repository navigation
Tracking: build and classify purls through one validated utils::purl API #748
Description
[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: discussion #560 register.
Kind: tracking. Source: review 6.4, 7.3; register C20.
Problem (verified on 045d7ec)
utils/purl.rs has two builder families:
- Unvalidated:
build_gem_purl…build_cargo_purl. There are 7 of them, plainformat!, with 21 production callers. - Validated:
npm_purl…maven_purl. They returnNonefor unsafe coordinates; lockfile discovery and the lock inventory use them.
On top of these, 42 production format!("pkg:…") sites outside utils/purl.rs build purls by hand. The review counted 48; corrected here. The largest groups:
vex/product.rs: 13 sites, including versionless purlsvendor/path.rs::leaf_to_purl: 10vendor/lock_inventory/recover.rs: 6vex/discover/mod.rs: 5, with their own PyPI/composer/nuget canonicalization in discover/mod.rs L1455-L1468hosted/engine.rs:622
There are also 24 inline starts_with("pkg:<type>/") checks beside Ecosystem::from_purl.
Drift already present. The families disagree on canonicalization:
composer_purllowercases, whilebuild_composer_purl,leaf_to_purlandrecover.rsdon't.pypi_purlcanonicalizes the name, whileleaf_to_purl,recover.rsand the hosted skip purl (hosted/engine.rs:622) don't.vex::discoverre-canonicalizes afterwards.vlt.rs:290alone percent-encodes the npm scope@.
Every consumer that compares purls therefore needs purl_eq/normalize_purl to paper over the differences.
Target design
utils::purlexposes one validated constructor per ecosystem (orPurl::new(Ecosystem, ns, name, version) -> Option<String>) that owns name canonicalization (PyPI PEP 503, composer and nuget lowercase), plus one versionlessbase_purl.build_*becomes private or is deleted.- Type checks go through
Ecosystem::from_purl.
Checklist (one PR each, in order)
- Route purl ecosystem checks through Ecosystem::from_purl instead of 24 inline starts_with("pkg:<type>/") tests #747: purl type checks through
Ecosystem::from_purl(mechanical; can start now). - Vendored ledger keys (
vendor/{gem,maven_repo,nuget_feed,composer_lock}.rs) andredirect/golang_local.rsmove to the validated builders. An unsafe coordinate becomes a refusal instead of a ledger key. Owner: ecosystems area. -
vendor/path.rs::leaf_to_purlandlock_inventory/recover.rsbuild through the validated builders, canonicalizing PyPI and composer once. Delete the re-canonicalization invex::discover. -
vex/product.rsversionless/product purls through onebase_purlbuilder. - Delete the
build_*family andpurl_name_version, which only has a test caller.
Dependencies
- Children 2–4 touch the ecosystem area (
audit-ecosystems), so coordinate with E-rows onvendor/andvex/. - Related: Move the crawler coordinate guards and composer's leading-v rule into utils and delete the copies #630 (crawler coordinate guards into
formats).
Consolidated work — backlog review, 2026-10-08
The following standalone issues are now tracked here. Their closure consolidates scheduling; it does not mean their implementation is complete. Original reports and discussion remain linked below.
#630: Move the crawler coordinate guards and composer's leading-v rule into utils and delete the copies
- Track the remaining scope from Move the crawler coordinate guards and composer's leading-v rule into utils and delete the copies #630 in this issue. Coordinate/purl helper consolidation belongs under the existing validated-purl tracker.
Preserved scope and acceptance criteria from #630
Proposed change
- Add
path_safety::is_safe_name_version(name, version)and moveis_safe_maven_coordinateintoutils::path_safety. - Call them from the three crawlers,
simple_purl,maven_purlandvendor/maven_repo.rs. - Make
utils::composer_version::strip_leading_vpub(crate)and point everynormalize_versioncaller at it: the composer crawler and its oracle,formats::composer,lock_inventory::composerandupstream::composer.
Delete: is_safe_{cargo,gem,nuget}_coordinate and their three test copies (keep one table test on the shared function), plus composer_crawler::normalize_version.
Size and scope
About 40 production lines deleted and about 10 added, in crawlers/{cargo,ruby,nuget,maven,composer}_crawler.rs, utils/{path_safety,purl,composer_version}.rs, formats/composer/mod.rs, patch/redirect/upstream/composer.rs and vendor/maven_repo.rs. There is no behavior change. Out of scope: the go and deno guards, which use multi-segment and JSR-component rules, and the purl builder families (C20).
Acceptance criteria
- One single-segment name/version guard and one Maven guard, both in
utils::path_safety. -
formats/,utils/andpatch/redirect/no longer importcrawlers::composer_crawler::normalize_versionorcrawlers::maven_crawler::is_safe_maven_coordinate. - One table test covers the traversal, colon, NUL, backslash and empty cases now spread over the three crawler tests.
purl_builders_validate_coordinatesand the composertest_normalize_versioncases, moved tostrip_leading_v, stay green. -
cargo test -p socket-patch-core --lib crawlersandcargo clippy --workspace --all-features -- -D warningsstay green.
Activity
audit-core: C23 filed (#746), C20 tracking (#748, #747), new C43 (#745)
refactor register: re-rank at capacity
mikolalysenko commentedon Oct 4, 2026
[agent] Triaged:
priority:p3(tracking/refactor). Child work starts with #747. Related: #630.Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Claiming this issue for the architecture refactor routine (highest leverage: the #630 item's three byte-identical crawler coordinate guards and
simple_purl's inline copy are the best item in files no open PR touches). Branch: arch-refactor/748-name-version-guard. Claim-ID: 2026-10-08T17:56:10Z-d68635Slice: one
path_safety::is_safe_name_versionfor the cargo, gem and NuGet crawlers andpurl::simple_purl, deletingis_safe_{cargo,gem,nuget}_coordinateand their three test copies. The composer leading-vhalf waits for #1108 (it touchesupstream/composer.rs); the Maven guard already lives informats::maven.Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Draft PR for the #630 item (coordinate guards slice): #1153.
Generated by Claude Code
Guard name/version pairs through path_safety
mikolalysenko commentedon Oct 13, 2026
[agent] Janitor: released the stale claim
Claim-ID: 2026-10-08T17. 56:10Z-d68635:#1153 (merged 2026-10-08) and #1045 slices merged; no open PR for the rest, and the claimer has posted nothing here in more than 48h. The issue stays open and can be claimed again.Generated by Claude Code
mikolalysenko commentedon Oct 29, 2026
[agent] The architecture audit (audit-core, register row A14) found that this tracker's scope also covers the parsing half: the
pkg:<type>/<name>@<version>split. It is hand-written about 12 times atc07af61.utils/purl.rsalone has three splitters, and they disagree on empty parts:purl_name_versionL147-L152 accepts an empty version, although its doc says it returnsNonewhen either part is missing.purl_partsL160-L169 accepts an empty name and an empty version.split_name_versionL181-L190 rejects both.The other copies:
npm_crawlerL4342-L4365npm_commonL630-L645lock_inventory/recoverL276-L290,``` which never strips qualifiershosted/memory/discoverL166-L171vex/discover/modL2602-L2612purl_keyL182-L206 and L224-L246lock_inventory/pypiL489,target.rsL294 and CLIscan/discovery.rsL138-L145Suggested child: one
purl::split(&str) -> Option<Parts { ty, name, version }>. It would split on the encoded text after stripping qualifiers and decode each component afterwards. Route every copy above through it.Generated by Claude Code