Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/socket-patch-cli/src/commands/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1254,10 +1254,10 @@ async fn run_check(args: &VendorArgs) -> i32 {
for (key, entry) in entries {
let record = entry.record.as_ref().or_else(|| manifest.patches.get(key));
let mut failure = match record {
Some(record) => match vendor::check_vendored_artifact(root, entry, record).await {
vendor::ArtifactHealth::Healthy => None,
health => Some(format!("artifact verification failed: {health:?}")),
},
Some(record) => {
let health = vendor::check_vendored_artifact(root, entry, record).await;
vendor::describe_artifact_failure(root, entry, &health).await
}
None => Some("patch record missing; restore the manifest or vendor ledger".to_string()),
};
if failure.is_none() && vendor::jvm::apply::is_jvm_entry(entry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -760,8 +760,7 @@ impl VendoredBackend<'_> {
}

fn workspace_copy_issue(health: &ArtifactHealth) -> bool {
matches!(health, ArtifactHealth::Corrupt { reason }
if reason == "vendor_workspace_artifact_missing" || reason == "vendor_workspace_artifact_corrupt")
health.is_workspace_copy_issue()
}

/// Preserve package originals while adopting/rebuilding every member-relative
Expand Down
51 changes: 51 additions & 0 deletions crates/socket-patch-cli/tests/in_process_vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4780,6 +4780,57 @@ async fn vendor_check_fails_when_lock_no_longer_wires_artifact() {
);
}

/// REGRESSION (#1467): a missing or changed committed artifact failed
/// `vendor --check` with the Rust Debug text of the health state
/// (`Missing`, `Corrupt { reason: "vendor_sha256_mismatch" }`): no path,
/// no remedy. The reason must name the artifact and point at `repair`;
/// restoring the committed bytes turns the check green again.
#[tokio::test]
async fn vendor_check_names_damaged_artifact_and_repair_remedy() {
let fx = npm_fixture();
assert_eq!(vendor_run(vendor_args(fx.root())).await, 0, "vendor");
let rel = fx
.tgz_path()
.strip_prefix(fx.root())
.unwrap()
.to_string_lossy()
.replace('\\', "/");
let good = std::fs::read(fx.tgz_path()).unwrap();

let mut changed = good.clone();
changed.extend_from_slice(b"drift");
std::fs::write(fx.tgz_path(), &changed).unwrap();
let (code, env) = vendor_cli(fx.root(), &["--check"]);
assert_eq!(code, 1, "{env:#}");
let event = find_event(&env, "failed", Some("vendor_check_failed"));
assert_eq!(
event["reason"],
format!(
"committed artifact {rel} does not match the sha256 in the vendor ledger \
(vendor_sha256_mismatch); run `socket-patch repair` to restore it, then commit it"
),
"{env:#}"
);

std::fs::remove_file(fx.tgz_path()).unwrap();
let (code, env) = vendor_cli(fx.root(), &["--check"]);
assert_eq!(code, 1, "{env:#}");
let event = find_event(&env, "failed", Some("vendor_check_failed"));
assert_eq!(
event["reason"],
format!(
"committed artifact {rel} is missing (vendor_artifact_missing); run \
`socket-patch repair` to restore it, then commit it"
),
"{env:#}"
);

std::fs::write(fx.tgz_path(), &good).unwrap();
let (code, env) = vendor_cli(fx.root(), &["--check"]);
assert_eq!(code, 0, "{env:#}");
find_event(&env, "verified", Some("vendor_check_ok"));
}

/// REGRESSION (#969): npm >= 11.14 refuses a vendored `file:` tarball
/// (EALLOWFILE) under a project `allow-file=none`, so every install of the
/// lock fails — but `vendor --check` verified it. It must fail the entry,
Expand Down
28 changes: 22 additions & 6 deletions crates/socket-patch-core/src/vendor/bun_workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,35 @@ fn required_mirrors(

/// Check every location an older Bun workspace reader can install from.
pub(super) async fn verify(root: &Path, entry: &VendorEntry) -> Result<(), String> {
let mirrors = required_mirrors(root, entry).map_err(|_| "vendor_workspace_artifact_invalid")?;
check(root, entry).await.map_err(|(tag, _)| tag.to_string())
}

/// The project-relative path of the first workspace copy [`verify`] rejects,
/// so a failure can name the member copy that is off.
pub(super) async fn failing_mirror(root: &Path, entry: &VendorEntry) -> Option<String> {
check(root, entry).await.err().and_then(|(_, rel)| rel)
}

/// Why the workspace copies cannot be listed at all (the detail behind
/// `vendor_workspace_artifact_invalid`).
pub(super) fn mirror_list_error(root: &Path, entry: &VendorEntry) -> Option<String> {
required_mirrors(root, entry).err()
}

async fn check(root: &Path, entry: &VendorEntry) -> Result<(), (&'static str, Option<String>)> {
let mirrors =
required_mirrors(root, entry).map_err(|_| ("vendor_workspace_artifact_invalid", None))?;
if mirrors.is_empty() {
return Ok(());
}
let expected = super::verify::file_sha256_hex(&root.join(&entry.artifact.path))
.await
.ok_or("vendor_artifact_unreadable")?;
for (_, _, path) in mirrors {
.ok_or(("vendor_artifact_unreadable", None))?;
for (_, rel, path) in mirrors {
match super::verify::file_sha256_hex(&path).await {
Some(actual) if actual == expected => {}
Some(_) => return Err("vendor_workspace_artifact_corrupt".into()),
None if !path.exists() => return Err("vendor_workspace_artifact_missing".into()),
None => return Err("vendor_workspace_artifact_corrupt".into()),
None if !path.exists() => return Err(("vendor_workspace_artifact_missing", Some(rel))),
_ => return Err(("vendor_workspace_artifact_corrupt", Some(rel))),
}
}
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/vendor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ pub use state::{
};
pub use verify::{
artifact_is_file_shaped, check_vendored_artifact, compute_dir_inventory,
compute_package_dir_inventory, file_sha256_hex, ArtifactHealth,
compute_package_dir_inventory, describe_artifact_failure, file_sha256_hex, ArtifactHealth,
};
// The hosted→vendored takeover refuses a berry project the backend would
// refuse BEFORE it reverts the hosted redirect.
Expand Down
23 changes: 17 additions & 6 deletions crates/socket-patch-core/src/vendor/redownload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ const NO_FILE_INVENTORY: &str = "the ledger has no complete file inventory; rest
version control, or re-run the vendoring command (`socket-patch vendor`, or `scan --mode \
vendored`) to rebuild it from a verified download";

/// Why [`restore`] refuses this entry before downloading anything: the
/// ledger records nothing to check a fresh download against.
pub(super) fn ledger_blocks_restore(entry: &VendorEntry) -> Option<String> {
let file_shaped = !super::verify::is_vlt_dir_entry(entry)
&& super::verify::artifact_is_file_shaped(&entry.artifact.path);
if file_shaped && entry.artifact.sha256.is_empty() {
return Some(no_archive_sha256());
}
if !file_shaped && entry.artifact.file_inventory.is_none() {
return Some(NO_FILE_INVENTORY.into());
}
None
}

/// Restore only the recorded artifact. Project wiring and ledger are never written.
pub async fn restore(
root: &Path,
Expand Down Expand Up @@ -101,14 +115,11 @@ pub async fn restore(
if crate::utils::containment::linked_level(root, &root.join(&entry.artifact.path)).is_some() {
return Err("vendor_path_unsafe: artifact path contains a symlink".into());
}
if let Some(detail) = ledger_blocks_restore(entry) {
return Err(detail);
}
let file_shaped = !super::verify::is_vlt_dir_entry(entry)
&& super::verify::artifact_is_file_shaped(&entry.artifact.path);
if file_shaped && entry.artifact.sha256.is_empty() {
return Err(no_archive_sha256());
}
if !file_shaped && entry.artifact.file_inventory.is_none() {
return Err(NO_FILE_INVENTORY.into());
}
let socket = root.join(".socket");
tokio::fs::create_dir_all(&socket)
.await
Expand Down
Loading
Loading