#![cfg(feature = "cargo")]
use std::path::{Path, PathBuf};
use std::process::Command;
use sha2::{Digest, Sha256};
#[path = "common/mod.rs"]
mod common;
use common::{
assert_run_ok, cargo_run, has_command, parse_json_envelope, run, sha256_hex, write_blob,
write_minimal_manifest, PatchEntry,
};
const ORIGINAL_LIB_RS: &str = "pub fn hello() -> &'static str { \"world\" }\n";
const PATCHED_LIB_RS: &str = "pub fn hello() -> &'static str { \"PATCHED\" }\n";
const FIXTURE_TOML: &str = "[package]\nname = \"safety-fixture\"\nversion = \"1.0.0\"\nedition = \"2021\"\n";
const FIXTURE_PURL: &str = "pkg:cargo/safety-fixture@1.0.0";
const FIXTURE_UUID: &str = "11111111-2222-4111-8111-111111111111";
fn stage_consumer(root: &Path) -> PathBuf {
let consumer = root.join("consumer");
let vendor_fixture = consumer.join("vendor").join("safety-fixture");
std::fs::create_dir_all(consumer.join("src")).unwrap();
std::fs::create_dir_all(consumer.join(".cargo")).unwrap();
std::fs::create_dir_all(vendor_fixture.join("src")).unwrap();
std::fs::write(
consumer.join("Cargo.toml"),
r#"[package]
name = "consumer"
version = "0.1.0"
edition = "2021"
[dependencies]
safety-fixture = "1.0.0"
"#,
)
.unwrap();
std::fs::write(
consumer.join("src/main.rs"),
"fn main() { println!(\"{}\", safety_fixture::hello()); }\n",
)
.unwrap();
std::fs::write(
consumer.join(".cargo/config.toml"),
r#"[source.crates-io]
replace-with = "vendored-test"
[source.vendored-test]
directory = "vendor"
"#,
)
.unwrap();
std::fs::write(vendor_fixture.join("Cargo.toml"), FIXTURE_TOML).unwrap();
std::fs::write(vendor_fixture.join("src/lib.rs"), ORIGINAL_LIB_RS).unwrap();
write_checksum_json(&vendor_fixture);
consumer
}
fn write_checksum_json(vendor_fixture: &Path) {
let toml_hash = sha256_hex(&std::fs::read(vendor_fixture.join("Cargo.toml")).unwrap());
let lib_hash = sha256_hex(&std::fs::read(vendor_fixture.join("src/lib.rs")).unwrap());
let json = serde_json::json!({
"files": {
"Cargo.toml": toml_hash,
"src/lib.rs": lib_hash,
},
"package": "0".repeat(64),
});
std::fs::write(
vendor_fixture.join(".cargo-checksum.json"),
serde_json::to_string_pretty(&json).unwrap(),
)
.unwrap();
}
fn generate_lockfile(consumer: &Path, cargo_home: &Path) {
let out = Command::new("cargo")
.args(["generate-lockfile", "--offline"])
.current_dir(consumer)
.env("CARGO_HOME", cargo_home)
.output()
.expect("cargo generate-lockfile");
assert!(
out.status.success(),
"cargo generate-lockfile failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
fn cargo_check(consumer: &Path, cargo_home: &Path) -> std::process::Output {
let _ = std::fs::remove_dir_all(consumer.join("target"));
cargo_run(
consumer,
&["check", "--offline", "--frozen"],
&[("CARGO_HOME", cargo_home.to_str().unwrap())],
)
}
fn git_hashes() -> (String, String) {
(
git_sha256(ORIGINAL_LIB_RS.as_bytes()),
git_sha256(PATCHED_LIB_RS.as_bytes()),
)
}
fn git_sha256(content: &[u8]) -> String {
let header = format!("blob {}\0", content.len());
let mut hasher = Sha256::new();
hasher.update(header.as_bytes());
hasher.update(content);
hex::encode(hasher.finalize())
}
fn stage_socket_manifest(consumer: &Path) -> (String, String) {
let (before, after) = git_hashes();
let socket_dir = consumer.join(".socket");
write_minimal_manifest(
&socket_dir,
FIXTURE_PURL,
FIXTURE_UUID,
&[PatchEntry {
file_name: "src/lib.rs",
before_hash: &before,
after_hash: &after,
}],
);
write_blob(&socket_dir, &after, PATCHED_LIB_RS.as_bytes());
(before, after)
}
#[test]
#[ignore]
fn cargo_check_succeeds_against_unpatched_fixture() {
if !has_command("cargo") {
eprintln!("SKIP: cargo not on PATH");
return;
}
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
let cargo_home = root.path().join(".cargo-home");
generate_lockfile(&consumer, &cargo_home);
let out = cargo_check(&consumer, &cargo_home);
assert!(
out.status.success(),
"baseline cargo check should succeed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
#[test]
#[ignore]
fn cargo_check_fails_without_sidecar_fixup() {
if !has_command("cargo") {
eprintln!("SKIP: cargo not on PATH");
return;
}
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
let cargo_home = root.path().join(".cargo-home");
generate_lockfile(&consumer, &cargo_home);
assert!(cargo_check(&consumer, &cargo_home).status.success());
std::fs::write(
consumer.join("vendor/safety-fixture/src/lib.rs"),
PATCHED_LIB_RS,
)
.unwrap();
let out = cargo_check(&consumer, &cargo_home);
assert!(
!out.status.success(),
"cargo check should refuse mismatched checksum"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("checksum") && stderr.contains("changed"),
"expected 'checksum...changed' error from cargo, got:\nstderr:\n{stderr}"
);
}
#[test]
#[ignore]
fn apply_then_cargo_check_succeeds() {
if !has_command("cargo") {
eprintln!("SKIP: cargo not on PATH");
return;
}
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
let cargo_home = root.path().join(".cargo-home");
generate_lockfile(&consumer, &cargo_home);
assert!(cargo_check(&consumer, &cargo_home).status.success());
let (_before, after) = stage_socket_manifest(&consumer);
let pre_checksum: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(
consumer.join("vendor/safety-fixture/.cargo-checksum.json"),
)
.unwrap(),
)
.unwrap();
let (_stdout, _stderr) = assert_run_ok(
&consumer,
&["apply", "--cwd", consumer.to_str().unwrap()],
"socket-patch apply",
);
assert_eq!(
std::fs::read_to_string(consumer.join("vendor/safety-fixture/src/lib.rs")).unwrap(),
PATCHED_LIB_RS,
"source file should reflect the patched content"
);
let post_checksum: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(
consumer.join("vendor/safety-fixture/.cargo-checksum.json"),
)
.unwrap(),
)
.unwrap();
let expected_lib_hash = sha256_hex(PATCHED_LIB_RS.as_bytes());
assert_eq!(
post_checksum["files"]["src/lib.rs"].as_str(),
Some(expected_lib_hash.as_str()),
"sidecar should rewrite src/lib.rs entry to the new SHA256.\npost: {post_checksum}"
);
assert_eq!(
post_checksum["package"], pre_checksum["package"],
"`package` field must survive the rewrite unchanged"
);
assert_eq!(
post_checksum["files"]["Cargo.toml"], pre_checksum["files"]["Cargo.toml"],
"unpatched entries must keep their original hash"
);
let out = cargo_check(&consumer, &cargo_home);
assert!(
out.status.success(),
"cargo check should succeed after sidecar fixup.\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let _ = after;
}
#[test]
#[ignore]
fn apply_reports_cargo_checksum_in_sidecars_updated() {
if !has_command("cargo") {
eprintln!("SKIP: cargo not on PATH");
return;
}
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
let cargo_home = root.path().join(".cargo-home");
generate_lockfile(&consumer, &cargo_home);
stage_socket_manifest(&consumer);
let (_code, stdout, stderr) = run(
&consumer,
&["apply", "--json", "--cwd", consumer.to_str().unwrap()],
);
let env = parse_json_envelope(&stdout);
let sidecars = env["sidecars"]
.as_array()
.unwrap_or_else(|| panic!(
"envelope must carry `sidecars` array.\nstdout:\n{stdout}\nstderr:\n{stderr}"
));
let cargo_record = sidecars
.iter()
.find(|s| s["ecosystem"] == "cargo")
.unwrap_or_else(|| panic!(
"envelope.sidecars must contain a record with ecosystem=cargo.\nstdout:\n{stdout}"
));
let files = cargo_record["files"].as_array().expect("files array");
assert!(
files.iter().any(|f| {
f["path"] == ".cargo-checksum.json" && f["action"] == "rewritten"
}),
"expected files[] to contain {{path:.cargo-checksum.json, action:rewritten}}; got {cargo_record}"
);
assert!(
cargo_record.get("advisory").is_none()
|| cargo_record["advisory"].is_null(),
"cargo success path should not carry an advisory; got {cargo_record}"
);
assert!(
cargo_record["purl"]
.as_str()
.map(|p| p.starts_with("pkg:cargo/"))
.unwrap_or(false),
"sidecar record must carry the PURL; got {cargo_record}"
);
}
#[test]
fn apply_with_malformed_checksum_reports_sidecar_fixup_failed() {
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
let cargo_home = root.path().join(".cargo-home");
let _ = cargo_home; stage_socket_manifest(&consumer);
let checksum = consumer.join("vendor/safety-fixture/.cargo-checksum.json");
std::fs::write(&checksum, b"{this is not valid json").unwrap();
let (_code, stdout, stderr) = run(
&consumer,
&["apply", "--json", "--cwd", consumer.to_str().unwrap()],
);
assert_eq!(
std::fs::read_to_string(consumer.join("vendor/safety-fixture/src/lib.rs")).unwrap(),
PATCHED_LIB_RS,
"patch must apply even when sidecar fixup fails"
);
let env = parse_json_envelope(&stdout);
let sidecars = env["sidecars"]
.as_array()
.unwrap_or_else(|| panic!(
"envelope must carry `sidecars` array.\nstdout:\n{stdout}\nstderr:\n{stderr}"
));
let cargo_record = sidecars
.iter()
.find(|s| s["ecosystem"] == "cargo")
.unwrap_or_else(|| panic!(
"envelope.sidecars must contain a cargo record.\nstdout:\n{stdout}"
));
let advisory = cargo_record.get("advisory").unwrap_or_else(|| {
panic!(
"malformed checksum should produce an advisory.\nrecord: {cargo_record}"
)
});
assert_eq!(
advisory["code"], "sidecar_fixup_failed",
"advisory.code must be sidecar_fixup_failed; got {advisory}"
);
assert_eq!(
advisory["severity"], "error",
"boundary-converted sidecar errors are severity=error"
);
assert!(
advisory["message"]
.as_str()
.map(|s| !s.is_empty())
.unwrap_or(false),
"advisory.message must be non-empty"
);
let files = cargo_record["files"].as_array().expect("files array");
assert!(
files.is_empty(),
"failed fixup must not report any rewritten files; got {cargo_record}"
);
}
#[test]
fn apply_with_missing_files_field_reports_sidecar_fixup_failed() {
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
stage_socket_manifest(&consumer);
let checksum = consumer.join("vendor/safety-fixture/.cargo-checksum.json");
std::fs::write(&checksum, br#"{"package":"0000000000000000000000000000000000000000000000000000000000000000"}"#).unwrap();
let (_code, stdout, _stderr) = run(
&consumer,
&["apply", "--json", "--cwd", consumer.to_str().unwrap()],
);
assert_eq!(
std::fs::read_to_string(consumer.join("vendor/safety-fixture/src/lib.rs")).unwrap(),
PATCHED_LIB_RS,
);
let env = parse_json_envelope(&stdout);
let sidecars = env["sidecars"].as_array().expect("sidecars array");
let cargo = sidecars
.iter()
.find(|s| s["ecosystem"] == "cargo")
.expect("cargo record");
let advisory = cargo.get("advisory").expect("advisory");
assert_eq!(advisory["code"], "sidecar_fixup_failed");
assert_eq!(advisory["severity"], "error");
let message = advisory["message"].as_str().unwrap_or("");
assert!(
message.contains("files"),
"advisory message must mention the missing `files` field; got {message:?}"
);
}
#[cfg(unix)]
#[test]
fn apply_with_readonly_checksum_still_rewrites_it() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
stage_socket_manifest(&consumer);
let checksum = consumer.join("vendor/safety-fixture/.cargo-checksum.json");
std::fs::set_permissions(&checksum, std::fs::Permissions::from_mode(0o444)).unwrap();
let (_code, stdout, _stderr) = run(
&consumer,
&["apply", "--json", "--cwd", consumer.to_str().unwrap()],
);
assert_eq!(
std::fs::read_to_string(consumer.join("vendor/safety-fixture/src/lib.rs")).unwrap(),
PATCHED_LIB_RS,
);
let post: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&checksum).unwrap()).unwrap();
assert_eq!(
post["files"]["src/lib.rs"].as_str().unwrap(),
sha256_hex(PATCHED_LIB_RS.as_bytes()),
"checksum entry must reflect the patched source"
);
let mode = std::fs::metadata(&checksum).unwrap().permissions().mode() & 0o7777;
let _ = std::fs::set_permissions(&checksum, std::fs::Permissions::from_mode(0o644));
assert_eq!(mode, 0o444, "checksum file must stay read-only after rewrite");
let env = parse_json_envelope(&stdout);
let cargo = env["sidecars"]
.as_array()
.expect("sidecars array")
.iter()
.find(|s| s["ecosystem"] == "cargo")
.expect("cargo record");
let rewrote = cargo["files"].as_array().is_some_and(|files| {
files
.iter()
.any(|f| f["path"] == ".cargo-checksum.json" && f["action"] == "rewritten")
});
assert!(
rewrote,
"expected a rewritten .cargo-checksum.json file entry; got {cargo}"
);
assert!(
cargo.get("advisory").map(|a| a.is_null()).unwrap_or(true),
"successful rewrite must not carry a failure advisory; got {cargo}"
);
}
#[test]
fn apply_with_checksum_directory_reports_sidecar_fixup_failed() {
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
stage_socket_manifest(&consumer);
let checksum = consumer.join("vendor/safety-fixture/.cargo-checksum.json");
std::fs::remove_file(&checksum).unwrap();
std::fs::create_dir(&checksum).unwrap();
let (_code, stdout, _stderr) = run(
&consumer,
&["apply", "--json", "--cwd", consumer.to_str().unwrap()],
);
assert_eq!(
std::fs::read_to_string(consumer.join("vendor/safety-fixture/src/lib.rs")).unwrap(),
PATCHED_LIB_RS,
);
let env = parse_json_envelope(&stdout);
let cargo = env["sidecars"]
.as_array()
.expect("sidecars array")
.iter()
.find(|s| s["ecosystem"] == "cargo")
.expect("cargo record");
let advisory = cargo.get("advisory").expect("advisory");
assert_eq!(advisory["code"], "sidecar_fixup_failed");
assert_eq!(advisory["severity"], "error");
let msg = advisory["message"].as_str().unwrap_or("");
assert!(
msg.contains(".cargo-checksum.json"),
"advisory message must reference the checksum path; got {msg:?}"
);
}
#[test]
fn apply_without_cargo_checksum_emits_no_sidecar_record() {
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
stage_socket_manifest(&consumer);
std::fs::remove_file(consumer.join("vendor/safety-fixture/.cargo-checksum.json"))
.unwrap();
let (_code, stdout, _stderr) = run(
&consumer,
&["apply", "--json", "--cwd", consumer.to_str().unwrap()],
);
assert_eq!(
std::fs::read_to_string(consumer.join("vendor/safety-fixture/src/lib.rs")).unwrap(),
PATCHED_LIB_RS,
);
let env = parse_json_envelope(&stdout);
let has_cargo_record = env
.get("sidecars")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().any(|s| s["ecosystem"] == "cargo"))
.unwrap_or(false);
assert!(
!has_cargo_record,
"no checksum file => no sidecar record; got envelope:\n{env}"
);
}
#[test]
fn apply_normalizes_package_prefix_in_cargo_checksum() {
let root = tempfile::tempdir().unwrap();
let consumer = stage_consumer(root.path());
let socket_dir = consumer.join(".socket");
let (before, after) = git_hashes();
write_minimal_manifest(
&socket_dir,
FIXTURE_PURL,
FIXTURE_UUID,
&[PatchEntry {
file_name: "package/src/lib.rs",
before_hash: &before,
after_hash: &after,
}],
);
write_blob(&socket_dir, &after, PATCHED_LIB_RS.as_bytes());
let (_code, stdout, _stderr) = run(
&consumer,
&["apply", "--json", "--cwd", consumer.to_str().unwrap()],
);
assert_eq!(
std::fs::read_to_string(consumer.join("vendor/safety-fixture/src/lib.rs")).unwrap(),
PATCHED_LIB_RS,
);
let checksum: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(
consumer.join("vendor/safety-fixture/.cargo-checksum.json"),
)
.unwrap(),
)
.unwrap();
assert!(
checksum["files"]["src/lib.rs"].is_string(),
"rewriter must use the normalized cargo-relative key; got {checksum}"
);
assert!(
checksum["files"]
.get("package/src/lib.rs")
.is_none(),
"rewriter must NOT create a `package/`-prefixed key"
);
let env = parse_json_envelope(&stdout);
let sidecars = env["sidecars"].as_array().unwrap();
let cargo = sidecars.iter().find(|s| s["ecosystem"] == "cargo").unwrap();
let files = cargo["files"].as_array().unwrap();
assert!(
files.iter().any(|f| f["path"] == ".cargo-checksum.json"
&& f["action"] == "rewritten"),
"sidecar record must still report .cargo-checksum.json:rewritten; got {cargo}"
);
}
#[test]
#[ignore]
fn traitobject_real_socket_patch_round_trip() {
if !has_command("cargo") {
eprintln!("SKIP: cargo not on PATH");
return;
}
let root = tempfile::tempdir().unwrap();
let consumer = root.path().join("consumer");
let cargo_home = root.path().join(".cargo-home");
std::fs::create_dir_all(consumer.join("src")).unwrap();
std::fs::write(
consumer.join("Cargo.toml"),
r#"[package]
name = "traitobject-consumer"
version = "0.0.1"
edition = "2021"
[dependencies]
traitobject = { version = "0.0.1", features = ["allow-unmaintained"] }
"#,
)
.unwrap();
std::fs::write(
consumer.join("src/main.rs"),
"fn main() {}\n",
)
.unwrap();
let cargo_home_str = cargo_home.to_str().unwrap();
let fetch = Command::new("cargo")
.args(["fetch"])
.current_dir(&consumer)
.env("CARGO_HOME", cargo_home_str)
.output()
.expect("cargo fetch");
if !fetch.status.success() {
eprintln!(
"SKIP: cargo fetch traitobject failed (likely network):\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&fetch.stdout),
String::from_utf8_lossy(&fetch.stderr),
);
return;
}
let registry_src = cargo_home.join("registry/src");
let mut traitobject_dir: Option<std::path::PathBuf> = None;
for entry in std::fs::read_dir(®istry_src).unwrap() {
let entry = entry.unwrap();
let candidate = entry.path().join("traitobject-0.0.1");
if candidate.is_dir() {
traitobject_dir = Some(candidate);
break;
}
}
let traitobject_dir = traitobject_dir
.expect("traitobject-0.0.1 should be unpacked under cargo registry/src after cargo fetch");
let checksum_path = traitobject_dir.join(".cargo-checksum.json");
let pre_apply_checksum: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(&checksum_path)
.expect("traitobject-0.0.1 must ship .cargo-checksum.json"),
)
.unwrap();
let socket_patch_run = Command::new(env!("CARGO_BIN_EXE_socket-patch"))
.args([
"get",
"b15f2b7f-d5cb-43c9-b793-80f71682188f",
"--cwd",
consumer.to_str().unwrap(),
])
.env("CARGO_HOME", cargo_home_str)
.env_remove("SOCKET_API_TOKEN") .output()
.expect("socket-patch get");
if !socket_patch_run.status.success() {
eprintln!(
"SKIP: socket-patch get failed (likely network):\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&socket_patch_run.stdout),
String::from_utf8_lossy(&socket_patch_run.stderr),
);
return;
}
let manifest_path = consumer.join(".socket/manifest.json");
let manifest: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(&manifest_path).expect("manifest.json must exist after get"),
)
.unwrap();
let patch = &manifest["patches"]["pkg:cargo/traitobject@0.0.1"];
assert!(
patch.is_object(),
"manifest should contain the traitobject patch: {manifest}"
);
let post_apply_checksum: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&checksum_path).unwrap()).unwrap();
let pre_files = pre_apply_checksum["files"].as_object().unwrap();
let post_files = post_apply_checksum["files"].as_object().unwrap();
let patched_paths = ["Cargo.toml", "Cargo.lock", "README.md", "src/lib.rs"];
for f in patched_paths {
if let (Some(pre), Some(post)) = (pre_files.get(f), post_files.get(f)) {
assert_ne!(
pre, post,
".cargo-checksum.json entry for {f} should change after apply"
);
assert_eq!(
post.as_str().unwrap().len(),
64,
"post-apply hash for {f} should be 64-hex SHA256"
);
}
}
assert_eq!(
pre_apply_checksum["package"], post_apply_checksum["package"],
".cargo-checksum.json `package` field must survive the rewrite unchanged"
);
let check = cargo_check(&consumer, &cargo_home);
assert!(
check.status.success(),
"cargo check should succeed against patched traitobject.\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&check.stdout),
String::from_utf8_lossy(&check.stderr),
);
}