Skip to content

Commit 2abbdc7

Browse files
Stop vendored Maven liveness attesting unpatched builds (#2006)
* Stop vendored Maven liveness attesting unpatched builds Three cases where vendored JVM (Maven reactor) liveness proved an entry "wired" and let vex attest while the build may still use the unpatched jar. Hosted mode and VEX discovery already refuse all three. J2, executable classifier copies: when the project also declares an executable classifier copy of the patched GA (tests, a native build), the reactor planner only warned `classifier_declared` and moved on, so `attestable_checked` passed. The classifier-less pin doesn't retarget the copy and the vendored tree serves only the main jar, so Maven fetches the public release and its code runs. A used (non-managed) executable classifier declaration whose version isn't the suffixed one now marks the reactor uncertain, so the entry isn't attestable. `sources` and `javadoc` still only warn. The executable-classifier rule now lives in `formats::maven::is_executable_classifier`, shared with the hosted rewriter and discovery. J8, unscoped "wired" check: `maven_reactor::wired_checked` was a substring match for the suffixed version over every reactor pom's comment-masked text. A suffixed version left only in a build plugin's dependencies, a profile, an unused property, another artifact's or a classifier copy's declaration kept the entry live. It now requires a `<dependencies>`/`<dependencyManagement>` declaration outside `<profiles>` (Doc::declarations already excludes `<build>`/`<reporting>`) naming the GA's main jar at the suffixed version, literally or through a property the checkout defines. That covers `vex`/`attestable_checked`, `vendor --check` (`check_entry`) and the plan's `refuse_unwired`. The old substring check is kept as `references_checked` and used only by the `scan --prune` GC (`entry_references`): the maven.config tail also serves plugin resolution, so a tree a `<build>` plugin or a profile still names must not be reverted out from under the build. J15, fail-open on read errors: `entry_wired` was `entry_wired_checked(..).unwrap_or(true)`, so an unreadable or unparseable pom made `Discovery::vendor_entry_live` (vendor's kept-patch rule, scan's takeover classifier and vex's ledger liveness) report a JVM entry as live. It now fails closed (`false`). The GC doesn't use it: it asks `entry_references`, which still answers "keep" on a read error, so nothing is deleted on an unknown answer. Tests (each fails on origin/main): - maven_reactor: an_executable_classifier_copy_is_not_attestable - maven_reactor: a_suffixed_version_outside_the_declarations_is_not_wired - apply: an_unreadable_pom_is_not_live_but_is_kept (also pins that entry_references / vendor_entry_in_use still keep the tree) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review findings on vendored Maven liveness Fixes all nine /code-review high findings on #2006. 1. A typed copy of the GA (`<type>test-jar</type>`, which is the `tests` jar, or `ejb`, `ejb-client`, `maven-plugin`, an unknown type) was skipped with a bare `continue`, so it still attested. A new shared rule, `formats::maven::is_executable_variant(type, classifier)`, maps types to the classifier they imply and treats every type except `pom`, `java-source` and `javadoc` as executable. The reactor now warns `classifier_declared` for such a copy and marks it uncertain unless every reader resolves it to the suffixed version. 2. With `entry_wired` failing closed, `scan`'s overlap takeover classifier read an unreadable pom, or vendored wiring left only in a plugin or profile, as a dead vendored claim. It then emitted `redirect_supersedes_vendored` ("remove <purl>") while the GC still kept the entry. New tri-state: `apply::entry_liveness`, `Discovery::vendor_entry_liveness` and `LedgerLiveness:: vendor_entry_state`. It returns `None` when the claim is undecided (read error, or still referenced without attestable wiring), and the classifier stays silent on `None`. 3. A classifier copy's version now goes through the same override check as the main jar's (`version_moved`, generalized from `version_override`). An inheriting pom or a profile that moves the property off the suffixed version makes it uncertain. 4. A version-less declaration now reads its version from the nearest local `<dependencyManagement>` on its chain, interpolated as the declaring pom reads it. Anything other than the suffixed version, including no local management (a local root left unpinned), is uncertain. One pinned root no longer proves an unpinned sibling root. 5. `vendor --check` now agrees with `vex`. `check_entry` returns `Ok(Some(why))` when the entry is intact and wired but not attestable. `vendor --check` keeps the entry `verified` and adds the new run warning `vendor_jvm_unattested` naming the declarations (documented in CLI_CONTRACT.md). It is a warning rather than a failure because the contract's unattested-references rule says liveness gates keep such wiring live, and re-vendoring cannot clear it. 6. `Reactor::wires` now interpolates each declaration in the declaring pom and in every pom that inherits it, as Maven interpolates after inheritance. A parent's managed `${ct.version}` that each module defines as the pin now counts as wired again, so `refuse_unwired` no longer refuses that plan. 7. The classifier version check uses `Doc::coordinate`. 8. The main-jar predicate is `formats::maven::is_main_jar`, used through `Doc::is_main_jar` by `wires`, `rewrite_declarations`, `warn_unresolved_coordinates` and `jar_declaration`. 9. Removed the uncalled `maven_reactor::wired`. Tests (each fails with its fix neutralized): - plugin_dependencies_exclusions_and_other_types_are_untouched: updated. It pinned the silent `test-jar` pass-through; executable types now warn and don't attest, while `pom` and `java-source` still pass silently. - a_classifier_copy_a_reader_moves_back_to_the_base_is_not_attestable - a_version_less_declaration_under_an_unpinned_root_is_not_attestable - a_parent_declaration_its_modules_resolve_to_the_pin_is_wired - vendor_check_reports_an_unattestable_entry - maven_takeover_stays_silent_on_an_undecided_vendored_claim (CLI) - an_unreadable_pom_is_not_live_but_is_kept also covers the tri-state Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Check profile overrides for version-less Maven declarations A version-less declaration was treated as reading the suffixed version whenever the nearest local management resolved to it, but that check interpolated with no profile overlay and ignored profile management. A profile that redefines the managed property, or manages g:a itself at the base, puts the unpatched jar back on the classpath while liveness still attested. Apply the same version_moved check versioned ${...} declarations get, and reject profile management of g:a at another version. Addresses the Bugbot High finding on maven_reactor.rs. Co-Authored-By: Claude <noreply@anthropic.com> * One executable-variant rule for hosted, discovery and vendored Maven Second review round on vendored Maven liveness. - The vendored reactor judged a `<type>` that implies an executable copy (`test-jar` is the `tests` jar, `ejb-client`, `ejb`, `bundle`, an unknown type) like a classifier, but the hosted rewriter and VEX discovery looked only at `<classifier>`: a `test-jar` declaration at the base release was pinned (hosted) and attested (discovery) while Maven put the unpatched tests jar on the classpath, so the two modes answered differently for one project. `formats::maven::is_variant` / `variant_label` now define the variant both share with the reactor, and both use `is_executable_variant`; discovery's main-jar version grouping skips such variants too, and keys management by type as well as classifier. - `keep_older_vendored_patch` asked the attestation question (`vendor_entry_live`), so an older JVM vendoring that is still wired but not attestable (an executable classifier copy beside it) gave way and every re-run failed `vendor_prebuilt_required`. It now keeps the older patch unless its liveness is provably dead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f43bef2 commit 2abbdc7

9 files changed

Lines changed: 965 additions & 101 deletions

File tree

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2280,7 +2280,14 @@ reference to `.socket/vendor/<eco>/<uuid>/` that no ledger entry owns (the
22802280
artifact-level event repair emits: `uuid` plus `details.{ecosystem,path}`, no
22812281
purl), so a checkout whose ledger was ignored or dropped from the commit no
22822282
longer passes with nothing to check. Offline upstream metadata is reported as the run warning
2283-
`vendor_jvm_upstream_unverified`. The check never starts an API client or writes
2283+
`vendor_jvm_upstream_unverified`. A Maven reactor entry that is intact and wired
2284+
but that `vex` does not attest — a declaration may still build the base release:
2285+
an executable classifier or type copy of the patched GA (`tests`, `test-jar`, a
2286+
native build), a version a property, a profile or an unpinned local root may
2287+
move, or a base declaration added since vendoring that the re-plan cannot take
2288+
— stays `verified` and adds the run warning `vendor_jvm_unattested`, whose
2289+
detail names the entry and the declarations. Like the other unattested
2290+
references, re-vendoring cannot clear it, so it is not drift. The check never starts an API client or writes
22842291
lock/recovery files. `--check` conflicts with `--revert`.
22852292

22862293
`vendor --check --local-repo <path>` additionally checks existing suffixed Maven
@@ -2636,6 +2643,7 @@ with `reason: <reason>: `:
26362643
| `vendor_jvm_degraded` | applied, VEX withheld | `gradle_unscanned_build_logic`, `unwired_build_logic` (a nonliteral included build), `settings_plugins_unwired`, `classifier_unpatched_copy` (a classifier jar carries an unpatched copy of a patched member), `verification_parent_chain_unhandled`, `gradle_lock_overrides_vendored` (a dependency lock of the build, buildscript and settings classpath locks included, pins the module at a version no vendored row serves), `gradle_settings_lock_unrouted` (a `settings-gradle.lockfile` locks the module but that build's settings script does not hold the in-block repository entry for it), `gradle_lock_unreadable` (a lock file, or a followed custom `lockFile`, is missing, unreadable, outside the checkout or not UTF-8), `gradle_custom_lock_file` (a `lockFile` location that cannot be followed statically: anything but a literal `file(…)` in a project's own build script), `gradle_wiring_uncommitted` (VEX liveness only, never reported by `vendor`: the committed settings files, index, derived `maven-metadata.xml`, script or verification file lack wiring a re-plan would write, such as a removed apply line or in-block entry; the git hygiene files do not count), `conflicting_managed_version` / `management_unresolved` (Maven reactor: an imported BOM or an out-of-checkout parent manages the artifact at another version, or its POM is unavailable; that local root is not pinned) |
26372644
| `vendor_jvm_note` | applied, informational | `range_declared` (a range, prefix or rich selector lists versions from the derived `maven-metadata.xml`), `ide_sources_unavailable` |
26382645
| `vendor_jvm_upstream_unverified` | warning | Upstream metadata was taken offline and not authenticated against registry checksums. |
2646+
| `vendor_jvm_unattested` | warning (`vendor --check`) | A Maven reactor entry is intact and wired, but a declaration may still build the base release (an executable classifier or type copy, a version a property, profile or unpinned local root may move), so `vex` does not attest it. The entry stays `verified`; the detail names the purl and the declarations (`<pom>:<line>`). |
26392647

26402648
The Maven half of a root (single-module, reactor or mixed) also reports
26412649
`vendor_jvm_degraded` with `reason: maven_f_outside_root` (Maven 3.9.2–3.9.8

‎crates/socket-patch-cli/src/commands/scan/mod.rs‎

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,15 +1070,18 @@ pub(super) async fn classify_overlap_takeover_with(
10701070
Some(uuid) => liveness.redirect_record(&purl, uuid).await,
10711071
None => discovery.wires_package(&purl, WiringMode::Hosted),
10721072
};
1073+
// `None`: undecided (a JVM pom that cannot be read, or a tree still
1074+
// referenced without attestable wiring) — neither live nor dead.
10731075
let vendored_live = match vendor_by_purl.get(&PurlKey::new(&purl)) {
1074-
Some(entry) => liveness.vendor_entry(entry).await,
1075-
None => false,
1076+
Some(entry) => liveness.vendor_entry_state(entry).await,
1077+
None => Some(false),
10761078
};
10771079
match (hosted_live, vendored_live) {
1078-
(true, false) => out.redirect.push(purl),
1079-
(false, true) => out.vendored.push(purl),
1080-
// Both (a half-migrated lock naming both) or neither (no rewire /
1081-
// unreadable) does not prove a single direction — stay silent.
1080+
(true, Some(false)) => out.redirect.push(purl),
1081+
(false, Some(true)) => out.vendored.push(purl),
1082+
// Both (a half-migrated lock naming both), neither (no rewire)
1083+
// or an undecided vendored side (unreadable) does not prove a
1084+
// single direction — stay silent.
10821085
_ => {}
10831086
}
10841087
}
@@ -4671,6 +4674,118 @@ mod tests {
46714674
);
46724675
}
46734676

4677+
// ---- maven: an undecided vendored claim is not a dead one -------------
4678+
4679+
const MAVEN_PURL: &str = "pkg:maven/org.slf4j/slf4j-api@1.7.36";
4680+
/// The vendored patch: another uuid than the hosted pin's, so its
4681+
/// suffixed version tells the two wirings apart.
4682+
const MAVEN_VENDORED_UUID: &str = "5c0ffee0-1d3a-4f6b-8c2d-7e5a9b1c3d5f";
4683+
4684+
fn maven_dep(version: &str) -> String {
4685+
format!(
4686+
"<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId>\
4687+
<version>{version}</version></dependency>"
4688+
)
4689+
}
4690+
4691+
/// A root pom pinning slf4j-api to the hosted [`TAKEOVER_UUID`] patch,
4692+
/// with `extra` (a `<build>` section) inside the project.
4693+
async fn write_maven_hosted_pom(root: &Path, extra: &str) {
4694+
let pom = format!(
4695+
"<?xml version=\"1.0\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n \
4696+
<modelVersion>4.0.0</modelVersion>\n <groupId>com.example</groupId>\n \
4697+
<artifactId>app</artifactId>\n <version>1.0.0</version>\n \
4698+
<dependencies>\n {}\n </dependencies>\n <repositories>\n <repository>\n \
4699+
<id>socket-patch-{TAKEOVER_UUID}</id>\n \
4700+
<url>https://patch.socket.dev/patch-registry/maven/11111111-2222-4333-8444-555555555555/{TAKEOVER_UUID}/maven2</url>\n \
4701+
<releases>\n <enabled>true</enabled>\n \
4702+
<checksumPolicy>fail</checksumPolicy>\n </releases>\n </repository>\n \
4703+
</repositories>\n {extra}\n</project>\n",
4704+
maven_dep(&format!("1.7.36-socket.{}", &TAKEOVER_UUID[..8])),
4705+
);
4706+
tokio::fs::write(root.join("pom.xml"), pom).await.unwrap();
4707+
}
4708+
4709+
/// A vendored ledger whose one entry is a Maven reactor vendoring of
4710+
/// slf4j-api at [`MAVEN_VENDORED_UUID`].
4711+
async fn write_maven_vendor_ledger(root: &Path) {
4712+
let sv = format!("1.7.36-socket.{}", &MAVEN_VENDORED_UUID[..8]);
4713+
let state = serde_json::json!({
4714+
"version": 1,
4715+
"entries": {
4716+
MAVEN_PURL: {
4717+
"ecosystem": "maven",
4718+
"basePurl": MAVEN_PURL,
4719+
"uuid": MAVEN_VENDORED_UUID,
4720+
"artifact": {
4721+
"path": format!(
4722+
".socket/vendor/maven2/org/slf4j/slf4j-api/{sv}/slf4j-api-{sv}.jar"
4723+
),
4724+
},
4725+
"wiring": [{
4726+
"file": "pom.xml",
4727+
"kind": "maven_pom_fragment",
4728+
"action": "added",
4729+
}],
4730+
},
4731+
},
4732+
});
4733+
let dir = root.join(".socket/vendor");
4734+
tokio::fs::create_dir_all(&dir).await.unwrap();
4735+
tokio::fs::write(
4736+
dir.join("state.json"),
4737+
serde_json::to_string_pretty(&state).unwrap(),
4738+
)
4739+
.await
4740+
.unwrap();
4741+
}
4742+
4743+
/// A vendored Maven claim the reader cannot decide — a project file it
4744+
/// cannot read, or the vendored version left only where the build may
4745+
/// still resolve it from the tree (a build plugin's dependencies) —
4746+
/// is neither live nor dead: the `scan --prune` GC keeps it, so the
4747+
/// takeover classifier must not report it superseded (and tell the
4748+
/// user to remove it). Only a claim nothing references any more is.
4749+
#[tokio::test]
4750+
async fn maven_takeover_stays_silent_on_an_undecided_vendored_claim() {
4751+
let vendored = format!("1.7.36-socket.{}", &MAVEN_VENDORED_UUID[..8]);
4752+
let plugin = format!(
4753+
"<build><plugins><plugin><groupId>p</groupId><artifactId>p</artifactId>\
4754+
<dependencies>{}</dependencies></plugin></plugins></build>",
4755+
maven_dep(&vendored)
4756+
);
4757+
// Control: nothing references the vendored version — superseded.
4758+
let tmp = tempfile::tempdir().unwrap();
4759+
let root = tmp.path();
4760+
write_maven_vendor_ledger(root).await;
4761+
write_maven_hosted_pom(root, "").await;
4762+
let takeover = classify_overlap_takeover(&common_at(root), root).await;
4763+
assert_eq!(
4764+
takeover.redirect,
4765+
vec![MAVEN_PURL.to_string()],
4766+
"{takeover:?}"
4767+
);
4768+
4769+
// Left only in a build plugin's dependencies.
4770+
let tmp = tempfile::tempdir().unwrap();
4771+
let root = tmp.path();
4772+
write_maven_vendor_ledger(root).await;
4773+
write_maven_hosted_pom(root, &plugin).await;
4774+
let takeover = classify_overlap_takeover(&common_at(root), root).await;
4775+
assert_eq!(takeover, OverlapTakeover::default());
4776+
4777+
// A Maven config the reader cannot read (a directory).
4778+
let tmp = tempfile::tempdir().unwrap();
4779+
let root = tmp.path();
4780+
write_maven_vendor_ledger(root).await;
4781+
write_maven_hosted_pom(root, "").await;
4782+
tokio::fs::create_dir_all(root.join(".mvn/maven.config"))
4783+
.await
4784+
.unwrap();
4785+
let takeover = classify_overlap_takeover(&common_at(root), root).await;
4786+
assert_eq!(takeover, OverlapTakeover::default());
4787+
}
4788+
46744789
// ---- takeover DIRECTION follows the live lock, not the command ---------
46754790

46764791
/// Like [`write_vendor_ledger`] but each entry records wiring the

‎crates/socket-patch-cli/src/commands/vendor.rs‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,14 @@ async fn keep_older_vendored_patch(
174174
};
175175
if let Some(kept) = lookup_entry(&state.entries, purl).filter(|e| e.uuid != uuid) {
176176
let root = common.project_root();
177+
// Still in force, not attestable: a JVM tree that is still wired
178+
// or referenced (an executable classifier copy beside it, say) is
179+
// kept; only a provably dead one gives way.
177180
if crate::commands::discover_wiring(common, &root)
178181
.await
179-
.vendor_entry_live(&root, kept)
182+
.vendor_entry_liveness(&root, kept)
180183
.await
184+
!= Some(false)
181185
{
182186
return Some(VendorOutcome::Refused {
183187
code,
@@ -1334,7 +1338,18 @@ async fn run_check(args: &VendorArgs) -> i32 {
13341338
None => Some("patch record missing; restore the manifest or vendor ledger".to_string()),
13351339
};
13361340
if failure.is_none() && vendor::jvm::apply::is_jvm_entry(entry) {
1337-
failure = vendor::jvm::apply::check_entry(root, entry, local_repo.as_deref()).err();
1341+
match vendor::jvm::apply::check_entry(root, entry, local_repo.as_deref()) {
1342+
Ok(None) => {}
1343+
// Intact and wired, but `vex` will not attest it: a
1344+
// declaration may still build the base. Re-vendoring cannot
1345+
// clear that, so it warns rather than fails (the unattested
1346+
// references rule).
1347+
Ok(Some(why)) => env.warnings.push(RunWarning {
1348+
code: "vendor_jvm_unattested".into(),
1349+
detail: format!("{key}: {why}; vex does not attest it"),
1350+
}),
1351+
Err(e) => failure = Some(e),
1352+
}
13381353
}
13391354
// The npm check names the exact unwired lock entry, so it runs
13401355
// before the generic liveness rule below.

‎crates/socket-patch-core/src/formats/maven/mod.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,69 @@ pub(crate) fn may_shadow(
563563
&& may_name(dep_artifact, artifact)
564564
}
565565

566+
/// The classifiers whose jars never run on a classpath: `sources` and
567+
/// `javadoc`. Every other classifier copy of a patched GA (tests, a native
568+
/// or JDK-specific build) carries code, so it must resolve the patched
569+
/// release too. The hosted rewriter, VEX discovery and the vendored
570+
/// reactor's attestation share this rule.
571+
pub(crate) const NON_EXECUTABLE_CLASSIFIERS: &[&str] = &["sources", "javadoc"];
572+
573+
/// Whether a `<classifier>` copy of a GA carries code onto a classpath
574+
/// (see [`NON_EXECUTABLE_CLASSIFIERS`]).
575+
pub(crate) fn is_executable_classifier(classifier: &str) -> bool {
576+
!NON_EXECUTABLE_CLASSIFIERS.contains(&classifier)
577+
}
578+
579+
/// Whether a declaration of a GA is a sibling copy of its main jar that
580+
/// Maven resolves apart from it (and that the main-jar pin does not
581+
/// reach): a classifier, or a type other than `jar` / `pom`. The `<type>`
582+
/// a `${...}` leaves unknown counts. The hosted rewriter, VEX discovery and
583+
/// the vendored reactor share this rule with [`is_executable_variant`].
584+
pub(crate) fn is_variant(type_text: Option<&str>, classifier: Option<&str>) -> bool {
585+
classifier.is_some_and(|c| !c.is_empty())
586+
|| type_text.is_some_and(|t| !t.is_empty() && t != "jar" && t != "pom")
587+
}
588+
589+
/// The label a variant ([`is_variant`]) is reported by: its classifier,
590+
/// else its type.
591+
pub(crate) fn variant_label<'a>(
592+
type_text: Option<&'a str>,
593+
classifier: Option<&'a str>,
594+
) -> &'a str {
595+
classifier
596+
.filter(|c| !c.is_empty())
597+
.or(type_text)
598+
.unwrap_or_default()
599+
}
600+
601+
/// Whether a declaration of a GA's main jar: untyped or `jar`-typed, with
602+
/// no classifier.
603+
pub(crate) fn is_main_jar(type_text: Option<&str>, classifier: Option<&str>) -> bool {
604+
type_text.is_none_or(|t| t.is_empty() || t == "jar") && classifier.is_none_or(str::is_empty)
605+
}
606+
607+
/// Whether a declaration of a GA that is not its main jar
608+
/// ([`is_main_jar`]) is a copy that carries code onto a classpath. Maven
609+
/// derives a classifier from some types (`test-jar` is the `tests` jar,
610+
/// `ejb-client` the `client` one, `java-source` / `javadoc` the
611+
/// non-executable ones), so a type counts like the classifier it implies.
612+
/// Of the rest only `pom` holds no code; every other type (`ejb`,
613+
/// `maven-plugin`, `bundle`, an unknown or `${...}` one) is executable.
614+
pub(crate) fn is_executable_variant(type_text: Option<&str>, classifier: Option<&str>) -> bool {
615+
let type_text = type_text.filter(|t| !t.is_empty()).unwrap_or("jar");
616+
let implied = match type_text {
617+
"test-jar" => Some("tests"),
618+
"ejb-client" => Some("client"),
619+
"java-source" => Some("sources"),
620+
"javadoc" => Some("javadoc"),
621+
_ => None,
622+
};
623+
match classifier.filter(|c| !c.is_empty()).or(implied) {
624+
Some(c) => is_executable_classifier(c),
625+
None => type_text != "pom",
626+
}
627+
}
628+
566629
fn is_property_name(name: &str) -> bool {
567630
!name.is_empty()
568631
&& name

‎crates/socket-patch-core/src/patch/redirect/mod.rs‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8795,9 +8795,9 @@ fn rewrite_maven_pom(
87958795
// (sources, tests, a native build) is its own artifact — managed and
87968796
// resolved apart from the main jar — so it is left alone, and the
87978797
// main jar's literal-vs-depMgmt decision is made without it.
8798-
let (variants, live): (Vec<_>, Vec<_>) = all_matches
8799-
.into_iter()
8800-
.partition(|m| m.classifier.is_some());
8798+
let (variants, live): (Vec<_>, Vec<_>) = all_matches.into_iter().partition(|m| {
8799+
crate::formats::maven::is_variant(m.type_text.as_deref(), m.classifier.as_deref())
8800+
});
88018801
// A variant that carries code onto a classpath (tests, a native
88028802
// build, a JDK-specific jar) at the patched release would keep the
88038803
// unpatched bytes there while the GA counts as redirected and VEX
@@ -8812,8 +8812,14 @@ fn rewrite_maven_pom(
88128812
|| crate::formats::maven::split_socket_version(v)
88138813
.is_some_and(|(base, _)| base == dep.version);
88148814
if names_release {
8815-
let classifier = variant.classifier.as_deref().unwrap_or_default();
8816-
let executable = !matches!(classifier, "sources" | "javadoc");
8815+
let classifier = crate::formats::maven::variant_label(
8816+
variant.type_text.as_deref(),
8817+
variant.classifier.as_deref(),
8818+
);
8819+
let executable = crate::formats::maven::is_executable_variant(
8820+
variant.type_text.as_deref(),
8821+
variant.classifier.as_deref(),
8822+
);
88178823
executable_variant |= executable;
88188824
result.warnings.push(RewriteWarning {
88198825
code: "redirect_maven_classifier_unsupported".into(),
@@ -10371,6 +10377,16 @@ mod tests {
1037110377
);
1037210378
assert!(warning_codes(&r).contains(&"redirect_maven_classifier_unsupported"));
1037310379
}
10380+
// A type implying an executable classifier (`test-jar` is the
10381+
// `tests` jar) is the same copy, as the vendored reactor judges it.
10382+
let typed = pom_with_dep("\n <version>1.7.36</version>", "").replace(
10383+
" </dependencies>",
10384+
" <dependency>\n <groupId>org.slf4j</groupId>\n <artifactId>slf4j-api</artifactId>\n <version>1.7.36</version>\n <type>test-jar</type>\n </dependency>\n </dependencies>",
10385+
);
10386+
let files = BTreeMap::from([("pom.xml".to_string(), typed)]);
10387+
let r = rewrite_registry_redirect(&files, &[maven_override()]);
10388+
assert!(r.files.is_empty(), "test-jar: {:?}", r.files);
10389+
assert!(warning_codes(&r).contains(&"redirect_maven_classifier_unsupported"));
1037410390
}
1037510391

1037610392
/// Fail-closed literal pin: the `<version>` is rewritten to the suffixed

0 commit comments

Comments
 (0)