Skip to content

Commit efbbe31

Browse files
committed
fix(cargo): refuse a non-crates.io lock source on dry runs too
The crates.io-source refusal lived only in the wet preflight, so a Cargo.lock entry from a git fork or another registry made --dry-run download from the patch service and preview success, while the real run then refused with vendor_source_unsupported. Pull the check into lock_source_refusal and run it at the top of the dry-run branch as well, skipped only while a live hosted redirect owns the entry, which the wet run restores first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEUik3Qo9HQtYGVEAMs1Di
1 parent 1c044c5 commit efbbe31

1 file changed

Lines changed: 77 additions & 12 deletions

File tree

  • crates/socket-patch-core/src/vendor

‎crates/socket-patch-core/src/vendor/cargo.rs‎

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -640,9 +640,10 @@ async fn cargo_prelude(
640640
}
641641

642642
/// The wet run's refusals past the dry-run branch and before the first
643-
/// service call: a live hosted redirect nothing can revert, and a lock entry
644-
/// the vendored copy's version tag cannot be written into. `Ok` is the lock
645-
/// probe the hot path routes on.
643+
/// service call: a live hosted redirect nothing can revert, a lock entry
644+
/// resolved outside crates.io, and a lock entry the vendored copy's version
645+
/// tag cannot be written into. `Ok` is the lock probe the hot path routes
646+
/// on.
646647
async fn cargo_wet_preflight(
647648
project_root: &Path,
648649
name: &str,
@@ -670,19 +671,34 @@ async fn cargo_wet_preflight(
670671
),
671672
));
672673
}
674+
if let Some(refusal) = lock_source_refusal(project_root, name, version).await {
675+
return Err(refusal);
676+
}
677+
match lock_tag_preflight(project_root, name, version, uuid).await {
678+
(_, Some(refusal)) => Err(refusal),
679+
(probe, None) => Ok(probe),
680+
}
681+
}
682+
683+
/// A lock entry resolved from anywhere but crates.io (a git fork, another
684+
/// registry): server patches require the crates.io package. The dry run
685+
/// raises it too, unless a live hosted redirect owns the entry (the wet run
686+
/// restores that first).
687+
async fn lock_source_refusal(
688+
project_root: &Path,
689+
name: &str,
690+
version: &str,
691+
) -> Option<VendorOutcome> {
673692
if let cargo_lock::LockEntryProbe::Source(source) =
674693
cargo_lock::probe_lock_entry(project_root, name, version).await
675694
{
676695
if source != "registry+https://github.com/rust-lang/crates.io-index"
677696
&& source != "sparse+https://index.crates.io/"
678697
{
679-
return Err(refused("vendor_source_unsupported", format!("{name}@{version} resolves from {source}; server patches require the crates.io package")));
698+
return Some(refused("vendor_source_unsupported", format!("{name}@{version} resolves from {source}; server patches require the crates.io package")));
680699
}
681700
}
682-
match lock_tag_preflight(project_root, name, version, uuid).await {
683-
(_, Some(refusal)) => Err(refusal),
684-
(probe, None) => Ok(probe),
685-
}
701+
None
686702
}
687703

688704
/// The cargo backend's locked-version refusal of `record` — `Cargo.lock`
@@ -780,6 +796,14 @@ pub async fn vendor_cargo_crate<'a>(
780796
let (name, version) = (name.as_str(), version.as_str());
781797

782798
if dry_run {
799+
let hosted_live = hosted_redirect_residue(project_root, name, version)
800+
.await
801+
.is_some();
802+
if !hosted_live {
803+
if let Some(refusal) = lock_source_refusal(project_root, name, version).await {
804+
return refusal;
805+
}
806+
}
783807
let mut dry_warnings = Vec::new();
784808
if !cargo_copy_matches(&copy_dir, &record.files).await {
785809
if let Err(outcome) =
@@ -798,10 +822,7 @@ pub async fn vendor_cargo_crate<'a>(
798822
// Preview the wet run's lock edit — unless a live hosted redirect
799823
// still owns the lock entry: the wet run reverts it from the
800824
// redirect ledger first, so its lock is not this one.
801-
if hosted_redirect_residue(project_root, name, version)
802-
.await
803-
.is_none()
804-
{
825+
if !hosted_live {
805826
let (lock_probe, refusal) =
806827
lock_tag_preflight(project_root, name, version, &record.uuid).await;
807828
if let Some(refusal) = refusal {
@@ -2512,6 +2533,50 @@ mod tests {
25122533
assert!(lock_text(root).await.contains(&tagged(UUID)));
25132534
}
25142535

2536+
/// A lock entry resolved from outside crates.io is refused by the dry
2537+
/// run exactly as by the wet run, before either asks the service.
2538+
#[tokio::test]
2539+
async fn dry_run_refuses_a_non_crates_io_source_like_the_wet_run() {
2540+
for source in [
2541+
"git+https://example.com/fork/cfg-if#abc123",
2542+
"registry+https://my-registry.example/index",
2543+
] {
2544+
let (dir, blobs, pristine, record) = fixture().await;
2545+
let root = dir.path();
2546+
let lock = lock_body().replace(SOURCE, source);
2547+
tokio::fs::write(root.join("Cargo.lock"), &lock)
2548+
.await
2549+
.unwrap();
2550+
let server = wiremock::MockServer::start().await;
2551+
crate::vendor::test_support::mount_no_results(&server).await;
2552+
let cfg = crate::vendor::test_support::service_cfg(
2553+
&server.uri(),
2554+
crate::vendor::VendorSource::Service,
2555+
false,
2556+
);
2557+
let sources = PatchSources::blobs_only(&blobs);
2558+
for dry_run in [true, false] {
2559+
let outcome = crate::vendor::test_support::vendor_cargo_crate(
2560+
PURL,
2561+
pristine.as_path(),
2562+
root,
2563+
&record,
2564+
&sources,
2565+
"2026-06-09T00:00:00Z",
2566+
dry_run,
2567+
false,
2568+
Some(&cfg),
2569+
)
2570+
.await;
2571+
let detail = expect_refused(outcome, "vendor_source_unsupported");
2572+
assert!(detail.contains(source), "{dry_run}: {detail}");
2573+
}
2574+
assert_eq!(crate::vendor::test_support::request_count(&server).await, 0);
2575+
assert_eq!(lock_text(root).await, lock);
2576+
assert!(!root.join(format!(".socket/vendor/cargo/{UUID}")).exists());
2577+
}
2578+
}
2579+
25152580
#[tokio::test]
25162581
async fn test_refuses_locked_version_mismatch() {
25172582
let (dir, blobs, pristine, record) = fixture().await;

0 commit comments

Comments
 (0)