diff --git a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs index bd75dd126..18b749a51 100644 --- a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs +++ b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs @@ -1153,6 +1153,90 @@ fn gradle_vendor_533_unpatched_classifier_copy_withholds_vex() { assert!(!vex.exists(), "{env}"); } +/// #513 / #459: a reactor `${prop}` version that can't be pinned with +/// certainty (undefined in the checkout, or raised by a profile) is left +/// as is with a warning, and `vex` does not attest the package the build +/// still takes unpatched. +#[test] +fn maven_reactor_uncertain_property_version_withholds_vex() { + let cases = [ + ("", "property_unresolved"), + ( + "1.0\ + new1.1\ + ", + "conflicting_literal_version", + ), + ]; + // With a sibling module at the literal base, that module is rewritten + // and the patch is wired, yet `a` still builds the base. + for ((extra, reason), sibling) in cases + .into_iter() + .flat_map(|case| [(case, false), (case, true)]) + { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + fixture(root, Shape::Reactor, &[("foo", FOO_UUID)]); + let pristine = std::fs::read_to_string(root.join("proj/a/pom.xml")).unwrap(); + let a = pristine + .replace( + "1.0", + "${foo.version}", + ) + .replace( + "a", + &format!("a{extra}"), + ); + write(root, "proj/a/pom.xml", a.as_bytes()); + if sibling { + let agg = std::fs::read_to_string(root.join("proj/pom.xml")).unwrap(); + let agg = agg.replace( + "a", + "a\n b", + ); + write(root, "proj/pom.xml", agg.as_bytes()); + std::fs::create_dir_all(root.join("proj/b")).unwrap(); + let b = pristine.replace("a", "b"); + write(root, "proj/b/pom.xml", b.as_bytes()); + } + let env = ok(root, &["vendor"]); + assert!( + env.to_string().contains(&format!("reason: {reason}")), + "{env}" + ); + assert_eq!( + std::fs::read_to_string(root.join("proj/a/pom.xml")).unwrap(), + a + ); + assert_eq!( + wiring_text(root).contains("-socket."), + sibling, + "{reason}: wired" + ); + let vex = root.join("vex.json"); + let (_, env) = socket( + root, + &[ + "vex", + "-O", + vex.to_str().unwrap(), + "--product", + "pkg:generic/x@1", + ], + ); + assert_eq!( + events(&env), + [( + purl("foo"), + "skipped".to_string(), + "vendor_unwired".to_string() + )], + "{reason} (sibling: {sibling}): {env}" + ); + assert!(!vex.exists(), "{env}"); + } +} + /// #429 repair: a deleted owned `.gitattributes` (and nothing else) is /// rewritten by `repair` (no download), so the next autocrlf clone keeps the /// script and index LF. diff --git a/crates/socket-patch-core/src/vendor/jvm/apply.rs b/crates/socket-patch-core/src/vendor/jvm/apply.rs index e9d17f762..212a39a50 100644 --- a/crates/socket-patch-core/src/vendor/jvm/apply.rs +++ b/crates/socket-patch-core/src/vendor/jvm/apply.rs @@ -830,10 +830,11 @@ pub fn entry_references(root: &Path, entry: &VendorEntry) -> bool { referenced || reader.read_error.borrow().is_some() } -/// The liveness proof `vex` needs: every half of the entry is wired, and -/// a Gradle half's script is intact and re-plans with no refusal and no +/// The liveness proof `vex` needs: every half of the entry is wired, a +/// Gradle half's script is intact and re-plans with no refusal and no /// degraded warning (a conflicting rule or unchecked build logic added -/// since vendoring withholds attestation). +/// since vendoring withholds attestation), and no Maven declaration is +/// left at a version that may still read the base. pub fn entry_wired_checked(root: &Path, entry: &VendorEntry) -> Result { let (g, a, v) = entry_gav(entry)?; let c = Coords { @@ -856,7 +857,7 @@ pub fn entry_wired_checked(root: &Path, entry: &VendorEntry) -> Result, c: &Coords<'_>) -> Result, c: &Coords<'_>) -> Result { + let sv = c.suffixed_version(); + let reactor = Reactor::discover(read)?; + if !reactor + .scope + .iter() + .any(|rel| reactor.poms[rel].doc.masked.contains(&sv)) + { + return Ok(false); + } + let patch = JvmPatch { + group_id: c.group_id, + artifact_id: c.artifact_id, + version: c.version, + uuid: c.uuid, + jar: &[], + upstream_pom: &[], + upstream_module: None, + extra_artifacts: &[], + patched_members: &[], + }; + let mut uncertain = BTreeSet::new(); + for rel in &reactor.scope { + reactor.rewrite_declarations( + rel, + &patch, + &sv, + &mut BTreeMap::new(), + &mut Vec::new(), + &mut BTreeSet::new(), + &mut BTreeSet::new(), + &mut uncertain, + ); + } + Ok(uncertain.is_empty()) +} + /// `