Skip to content

Commit b389a0f

Browse files
committed
fix(vendor): rebuild a dir copy that has no inventory on re-vendor
Ledgers written before #300 record no file inventory for cargo, Go and composer copies, and a copy past the inventory cap records none either. Exact restore refuses such an entry, and vendor sent every same-uuid entry that was not healthy to that same restore, so the re-vendor the error suggested failed with vendor_redownload_failed on every run. A dir-shaped entry without an inventory now skips the exact restore and goes to the backend, which rebuilds the copy from a fresh verified download as vendor did before #300. The file-shaped entry without a SHA-256 still refuses. Both restore refusals now name the remedy that records a new fingerprint: vendor --revert and then vendor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEUik3Qo9HQtYGVEAMs1Di
1 parent 924951a commit b389a0f

3 files changed

Lines changed: 108 additions & 10 deletions

File tree

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2445,9 +2445,15 @@ pub(crate) async fn vendor_records_reusing(
24452445
}
24462446
}
24472447

2448-
if let Some(entry) =
2449-
lookup_entry(&state.entries, candidate).filter(|entry| entry.uuid == record.uuid)
2450-
{
2448+
// A dir-shaped entry with no file inventory (vendored before
2449+
// inventories were recorded, or past the inventory cap) gives an
2450+
// exact restore nothing to check a download against: the backend
2451+
// below rebuilds its copy from a fresh verified download instead.
2452+
if let Some(entry) = lookup_entry(&state.entries, candidate).filter(|entry| {
2453+
entry.uuid == record.uuid
2454+
&& (entry.artifact.file_inventory.is_some()
2455+
|| vendor::artifact_is_file_shaped(&entry.artifact.path))
2456+
}) {
24512457
if vendor::check_vendored_artifact(&common.cwd, entry, record).await
24522458
!= vendor::ArtifactHealth::Healthy
24532459
|| (entry.artifact.sha256.is_empty()

‎crates/socket-patch-cli/tests/vendor/vendor_rerun_no_network_e2e.rs‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,76 @@ fn cargo_rerun_over_a_drifted_copy_still_fetches_and_reports_it() {
689689
);
690690
}
691691

692+
/// A copy vendored before inventories were recorded (a 4.0.0 ledger entry
693+
/// has no `fileInventory`) gives an exact redownload nothing to check, so a
694+
/// re-vendor over its deleted copy rebuilds it through the backend from a
695+
/// fresh verified download instead of refusing on every run.
696+
#[test]
697+
fn cargo_rerun_over_a_missing_pre_inventory_copy_rebuilds_it() {
698+
const PURL: &str = "pkg:cargo/cfg-if@1.0.4";
699+
const UUID: &str = "2b1f6c1e-8d3a-4f6b-9c2d-7e5a9b1c3d0c";
700+
const PRISTINE: &[u8] = b"pub fn cfg() {}\n";
701+
const PATCHED: &[u8] = b"pub fn cfg() { /* patched */ }\n";
702+
let tmp = tempfile::tempdir().unwrap();
703+
let root = tmp.path().join("proj");
704+
let cargo_home = tmp.path().join("cargo-home");
705+
let krate = cargo_home.join("registry/src/index.crates.io-6f17d22bba15001f/cfg-if-1.0.4");
706+
std::fs::create_dir_all(krate.join("src")).unwrap();
707+
std::fs::write(krate.join("src/lib.rs"), PRISTINE).unwrap();
708+
std::fs::write(
709+
krate.join("Cargo.toml"),
710+
"[package]\nname = \"cfg-if\"\nversion = \"1.0.4\"\n",
711+
)
712+
.unwrap();
713+
std::fs::create_dir_all(&root).unwrap();
714+
std::fs::write(
715+
root.join("Cargo.toml"),
716+
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\ncfg-if = \"1\"\n",
717+
)
718+
.unwrap();
719+
std::fs::write(
720+
root.join("Cargo.lock"),
721+
format!(
722+
"version = 4\n\n\
723+
[[package]]\nname = \"app\"\nversion = \"0.1.0\"\n\
724+
dependencies = [\n \"cfg-if\",\n]\n\n\
725+
[[package]]\nname = \"cfg-if\"\nversion = \"1.0.4\"\n\
726+
source = \"registry+https://github.com/rust-lang/crates.io-index\"\n\
727+
checksum = \"{}\"\n",
728+
"9".repeat(64)
729+
),
730+
)
731+
.unwrap();
732+
write_manifest(&root, PURL, UUID, "package/src/lib.rs", PRISTINE, PATCHED);
733+
let home = cargo_home.to_string_lossy().into_owned();
734+
let env = [("CARGO_HOME", home.as_str())];
735+
let dead = dead_endpoint();
736+
737+
let (code, v, stderr) = run_vendor(&root, &dead, &[], &env);
738+
assert_eq!(code, 0, "{v:#}\n{stderr}");
739+
std::fs::remove_dir_all(&krate).unwrap();
740+
let state_path = root.join(".socket/vendor/state.json");
741+
let mut state: serde_json::Value =
742+
serde_json::from_slice(&std::fs::read(&state_path).unwrap()).unwrap();
743+
let artifact = state["entries"][PURL]["artifact"]
744+
.as_object_mut()
745+
.expect("the ledger records the crate");
746+
assert!(artifact.remove("fileInventory").is_some(), "{v:#}");
747+
std::fs::write(&state_path, serde_json::to_vec_pretty(&state).unwrap()).unwrap();
748+
std::fs::remove_dir_all(root.join(format!(".socket/vendor/cargo/{UUID}"))).unwrap();
749+
750+
let (code, v, stderr) = run_vendor(&root, &dead, &[], &env);
751+
assert_eq!(code, 0, "{v:#}\n{stderr}");
752+
assert!(
753+
!purl_events(&v, PURL).contains(&("failed", "vendor_redownload_failed")),
754+
"{v:#}"
755+
);
756+
let copy_lib = root.join(format!(
757+
".socket/vendor/cargo/{UUID}/cfg-if-1.0.4/src/lib.rs"
758+
));
759+
assert_eq!(std::fs::read(&copy_lib).unwrap(), PATCHED);
760+
}
761+
692762
// ── cargo: the service path needs no pristine source ─────────────────────
693763

694764
/// A `.crate`: a tar.gz with a single `{prefix}/` top-level dir.

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ async fn download_archive(
4747
.or_else(used)
4848
}
4949

50+
// Without the ledger's fingerprint no download can be proven to be the
51+
// recorded artifact. A revert drops the entry, so the following `vendor`
52+
// downloads afresh and records a new fingerprint.
53+
const NO_ARCHIVE_SHA256: &str = "the ledger has no archive SHA-256; restore it from version \
54+
control, or run `socket-patch vendor --revert` and then `socket-patch vendor` to vendor \
55+
it again";
56+
const NO_FILE_INVENTORY: &str = "the ledger has no complete file inventory; restore it from \
57+
version control, or run `socket-patch vendor --revert` and then `socket-patch vendor` to \
58+
vendor it again";
59+
5060
/// Restore only the recorded artifact. Project wiring and ledger are never written.
5161
pub async fn restore(
5262
root: &Path,
@@ -93,10 +103,10 @@ pub async fn restore(
93103
let file_shaped = !super::verify::is_vlt_dir_entry(entry)
94104
&& super::verify::artifact_is_file_shaped(&entry.artifact.path);
95105
if file_shaped && entry.artifact.sha256.is_empty() {
96-
return Err("the ledger has no archive SHA-256; restore from version control or explicitly re-vendor".into());
106+
return Err(NO_ARCHIVE_SHA256.into());
97107
}
98108
if !file_shaped && entry.artifact.file_inventory.is_none() {
99-
return Err("the ledger has no complete file inventory; restore from version control or explicitly re-vendor".into());
109+
return Err(NO_FILE_INVENTORY.into());
100110
}
101111
let socket = root.join(".socket");
102112
tokio::fs::create_dir_all(&socket)
@@ -278,7 +288,11 @@ pub async fn restore(
278288
}
279289
}
280290
{
281-
let inventory = entry.artifact.file_inventory.as_ref().ok_or("the ledger has no complete file inventory; restore from version control or explicitly re-vendor")?;
291+
let inventory = entry
292+
.artifact
293+
.file_inventory
294+
.as_ref()
295+
.ok_or(NO_FILE_INVENTORY)?;
282296
let uuid = (entry.ecosystem == "cargo").then_some(entry.uuid.as_str());
283297
super::verify::verify_dir_inventory(&stage, inventory, uuid).await?;
284298
}
@@ -566,6 +580,10 @@ mod tests {
566580
.await
567581
.unwrap_err();
568582
assert!(error.contains("no archive SHA-256"), "{error}");
583+
assert!(
584+
error.contains("`socket-patch vendor --revert` and then `socket-patch vendor`"),
585+
"the refusal names the remedy that records a new fingerprint: {error}"
586+
);
569587
assert!(server.received_requests().await.unwrap().is_empty());
570588
assert_eq!(tree_snapshot(root.path()), before);
571589
}
@@ -685,15 +703,19 @@ mod tests {
685703
let before = tree_snapshot(root.path());
686704
entry.artifact.file_inventory = None;
687705
server.reset().await;
688-
assert!(restore(
706+
let error = restore(
689707
root.path(),
690708
&entry,
691709
&record(),
692-
&service_cfg(&server.uri(), VendorSource::Service, false)
710+
&service_cfg(&server.uri(), VendorSource::Service, false),
693711
)
694712
.await
695-
.unwrap_err()
696-
.contains("no complete file inventory"));
713+
.unwrap_err();
714+
assert!(error.contains("no complete file inventory"), "{error}");
715+
assert!(
716+
error.contains("`socket-patch vendor --revert` and then `socket-patch vendor`"),
717+
"the refusal names the remedy that records a new inventory: {error}"
718+
);
697719
assert!(server.received_requests().await.unwrap().is_empty());
698720
assert_eq!(tree_snapshot(root.path()), before);
699721
entry.ecosystem = "npm".into();

0 commit comments

Comments
 (0)