use std::path::Path;
#[path = "common/mod.rs"]
mod common;
use common::{
git_sha256, parse_json_envelope, run_with_env, write_blob, write_minimal_manifest,
PatchEntry,
};
fn apply_and_parse(
cwd: &Path,
package_root: &Path,
extra_env: &[(&str, &str)],
) -> serde_json::Value {
let (_code, stdout, stderr) = run_with_env(
cwd,
&[
"apply",
"--json",
"--cwd",
cwd.to_str().unwrap(),
"--global-prefix",
package_root.to_str().unwrap(),
],
extra_env,
);
if stdout.trim().is_empty() {
panic!(
"socket-patch apply emitted no JSON.\nstderr:\n{stderr}"
);
}
parse_json_envelope(&stdout)
}
fn find_sidecar_record<'a>(
env: &'a serde_json::Value,
ecosystem: &str,
) -> &'a serde_json::Value {
let sidecars = env["sidecars"]
.as_array()
.unwrap_or_else(|| panic!("envelope.sidecars must be an array.\nenv: {env}"));
sidecars
.iter()
.find(|s| s["ecosystem"] == ecosystem)
.unwrap_or_else(|| {
panic!(
"envelope.sidecars must contain a record with ecosystem={ecosystem}.\nenv: {env}"
)
})
}
#[test]
fn pypi_apply_emits_pypi_record_stale_advisory() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path();
let site_packages = cwd.join("site-packages");
let dist_info = site_packages.join("requests-2.28.0.dist-info");
std::fs::create_dir_all(&dist_info).unwrap();
std::fs::write(
dist_info.join("METADATA"),
"Metadata-Version: 2.1\nName: requests\nVersion: 2.28.0\n",
)
.unwrap();
let target = site_packages.join("payload.py");
let original = b"# original\n";
std::fs::write(&target, original).unwrap();
let patched = b"# patched\n";
let before = git_sha256(original);
let after = git_sha256(patched);
let socket_dir = cwd.join(".socket");
write_minimal_manifest(
&socket_dir,
"pkg:pypi/requests@2.28.0",
"20000001-0000-4001-8001-000000000001",
&[PatchEntry {
file_name: "package/payload.py",
before_hash: &before,
after_hash: &after,
}],
);
write_blob(&socket_dir, &after, patched);
let env = apply_and_parse(cwd, &site_packages, &[]);
assert_eq!(std::fs::read(&target).unwrap(), patched);
let record = find_sidecar_record(&env, "pypi");
assert_eq!(
record["purl"], "pkg:pypi/requests@2.28.0",
"record must denormalize the PURL.\nrecord: {record}"
);
let files = record["files"].as_array().expect("files array");
assert!(
files.is_empty(),
"pypi advisory-only path must report no files[]; got {record}"
);
let advisory = record
.get("advisory")
.unwrap_or_else(|| panic!("advisory missing.\nrecord: {record}"));
assert_eq!(
advisory["code"], "pypi_record_stale",
"code contract: pypi must emit pypi_record_stale"
);
assert_eq!(
advisory["severity"], "warning",
"severity contract: pypi advisory is severity=warning"
);
assert!(
advisory["message"]
.as_str()
.map(|s| !s.is_empty())
.unwrap_or(false),
"advisory.message must be non-empty"
);
}
#[test]
fn gem_apply_emits_gem_bundle_install_reverts_advisory() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path();
let gem_root = cwd.join("gems");
let gem_dir = gem_root.join("rails-7.1.0");
std::fs::create_dir_all(gem_dir.join("lib")).unwrap();
let target = gem_dir.join("lib").join("rails.rb");
let original = b"module Rails; end\n";
std::fs::write(&target, original).unwrap();
let patched = b"module Rails; VERSION = '7.1.0-patched'.freeze; end\n";
let before = git_sha256(original);
let after = git_sha256(patched);
let socket_dir = cwd.join(".socket");
write_minimal_manifest(
&socket_dir,
"pkg:gem/rails@7.1.0",
"20000002-0000-4002-8002-000000000002",
&[PatchEntry {
file_name: "package/lib/rails.rb",
before_hash: &before,
after_hash: &after,
}],
);
write_blob(&socket_dir, &after, patched);
let env = apply_and_parse(cwd, &gem_root, &[]);
assert_eq!(std::fs::read(&target).unwrap(), patched);
let record = find_sidecar_record(&env, "gem");
assert_eq!(record["purl"], "pkg:gem/rails@7.1.0");
let files = record["files"].as_array().expect("files array");
assert!(
files.is_empty(),
"gem advisory-only path must report no files[]; got {record}"
);
let advisory = record.get("advisory").expect("advisory missing");
assert_eq!(
advisory["code"], "gem_bundle_install_reverts",
"code contract: gem must emit gem_bundle_install_reverts"
);
assert_eq!(advisory["severity"], "warning");
}
#[cfg(feature = "golang")]
#[test]
fn golang_apply_emits_go_mod_verify_fails_advisory() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path();
let cache = cwd.join("gomodcache");
let module_dir = cache.join("github.com").join("gin-gonic").join("gin@v1.9.1");
std::fs::create_dir_all(&module_dir).unwrap();
let target = module_dir.join("gin.go");
let original = b"package gin\n";
std::fs::write(&target, original).unwrap();
let patched = b"package gin\n// patched\n";
let before = git_sha256(original);
let after = git_sha256(patched);
let socket_dir = cwd.join(".socket");
write_minimal_manifest(
&socket_dir,
"pkg:golang/github.com/gin-gonic/gin@v1.9.1",
"20000003-0000-4003-8003-000000000003",
&[PatchEntry {
file_name: "package/gin.go",
before_hash: &before,
after_hash: &after,
}],
);
write_blob(&socket_dir, &after, patched);
let env = apply_and_parse(
cwd,
&cache,
&[("GOMODCACHE", cache.to_str().unwrap())],
);
assert_eq!(std::fs::read(&target).unwrap(), patched);
let record = find_sidecar_record(&env, "golang");
assert_eq!(
record["purl"],
"pkg:golang/github.com/gin-gonic/gin@v1.9.1"
);
let files = record["files"].as_array().expect("files array");
assert!(
files.is_empty(),
"golang advisory-only path must report no files[]; got {record}"
);
let advisory = record.get("advisory").expect("advisory missing");
assert_eq!(
advisory["code"], "go_mod_verify_fails",
"code contract: golang must emit go_mod_verify_fails"
);
assert_eq!(advisory["severity"], "warning");
}
#[cfg(feature = "nuget")]
#[test]
fn nuget_apply_deletes_metadata_and_records_files() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path();
let packages = cwd.join("nuget-packages");
let pkg_dir = packages.join("newtonsoft.json").join("13.0.3");
std::fs::create_dir_all(pkg_dir.join("lib")).unwrap();
std::fs::write(
pkg_dir.join(".nupkg.metadata"),
r#"{"contentHash":"deadbeef"}"#,
)
.unwrap();
let target = pkg_dir.join("payload.txt");
let original = b"hello\n";
std::fs::write(&target, original).unwrap();
let patched = b"hello patched\n";
let before = git_sha256(original);
let after = git_sha256(patched);
let socket_dir = cwd.join(".socket");
write_minimal_manifest(
&socket_dir,
"pkg:nuget/Newtonsoft.Json@13.0.3",
"20000004-0000-4004-8004-000000000004",
&[PatchEntry {
file_name: "package/payload.txt",
before_hash: &before,
after_hash: &after,
}],
);
write_blob(&socket_dir, &after, patched);
let env = apply_and_parse(
cwd,
&packages,
&[
("NUGET_PACKAGES", packages.to_str().unwrap()),
("SOCKET_EXPERIMENTAL_NUGET", "1"),
],
);
assert_eq!(std::fs::read(&target).unwrap(), patched);
assert!(
!pkg_dir.join(".nupkg.metadata").exists(),
"nuget fixup must delete .nupkg.metadata"
);
let record = find_sidecar_record(&env, "nuget");
let files = record["files"].as_array().expect("files array");
assert_eq!(
files.len(),
1,
"expected one file entry for .nupkg.metadata deletion; got {record}"
);
assert_eq!(files[0]["path"], ".nupkg.metadata");
assert_eq!(
files[0]["action"], "deleted",
"action contract: .nupkg.metadata is `deleted`, not `rewritten`"
);
assert!(
record.get("advisory").is_none() || record["advisory"].is_null(),
"unsigned nuget path must not emit an advisory; got {record}"
);
}
#[cfg(all(unix, feature = "nuget"))]
#[test]
fn nuget_apply_with_non_utf8_filename_in_pkg_dir() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path();
let packages = cwd.join("nuget-packages");
let pkg_dir = packages.join("newtonsoft.json").join("13.0.3");
std::fs::create_dir_all(pkg_dir.join("lib")).unwrap();
std::fs::write(
pkg_dir.join(".nupkg.metadata"),
r#"{"contentHash":"deadbeef"}"#,
)
.unwrap();
let bad_name = OsStr::from_bytes(&[0xff, 0xfe, b'-', b'b', b'a', b'd']);
let bad_path = pkg_dir.join(bad_name);
if std::fs::write(&bad_path, b"binary").is_err() {
eprintln!("SKIP: filesystem rejects non-UTF8 filenames");
return;
}
let target = pkg_dir.join("payload.txt");
let original = b"hello\n";
std::fs::write(&target, original).unwrap();
let patched = b"hello patched\n";
let before = git_sha256(original);
let after = git_sha256(patched);
let socket_dir = cwd.join(".socket");
write_minimal_manifest(
&socket_dir,
"pkg:nuget/Newtonsoft.Json@13.0.3",
"20000007-0000-4007-8007-000000000007",
&[PatchEntry {
file_name: "package/payload.txt",
before_hash: &before,
after_hash: &after,
}],
);
write_blob(&socket_dir, &after, patched);
let env = apply_and_parse(
cwd,
&packages,
&[
("NUGET_PACKAGES", packages.to_str().unwrap()),
("SOCKET_EXPERIMENTAL_NUGET", "1"),
],
);
assert_eq!(std::fs::read(&target).unwrap(), patched);
assert!(!pkg_dir.join(".nupkg.metadata").exists());
let record = find_sidecar_record(&env, "nuget");
let files = record["files"].as_array().expect("files array");
assert_eq!(files.len(), 1, "metadata deletion expected");
assert_eq!(files[0]["path"], ".nupkg.metadata");
assert!(
record.get("advisory").is_none() || record["advisory"].is_null(),
"non-UTF8 file must not trigger the signed-marker advisory; got {record}"
);
}
#[cfg(feature = "nuget")]
#[test]
fn nuget_apply_with_metadata_directory_reports_sidecar_fixup_failed() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path();
let packages = cwd.join("nuget-packages");
let pkg_dir = packages.join("newtonsoft.json").join("13.0.3");
std::fs::create_dir_all(pkg_dir.join("lib")).unwrap();
std::fs::create_dir(pkg_dir.join(".nupkg.metadata")).unwrap();
std::fs::write(
pkg_dir.join(".nupkg.metadata").join("placeholder"),
b"non-empty so the dir can't be remove_file-removed even on permissive platforms",
)
.unwrap();
let target = pkg_dir.join("payload.txt");
let original = b"hello\n";
std::fs::write(&target, original).unwrap();
let patched = b"hello patched\n";
let before = git_sha256(original);
let after = git_sha256(patched);
let socket_dir = cwd.join(".socket");
write_minimal_manifest(
&socket_dir,
"pkg:nuget/Newtonsoft.Json@13.0.3",
"20000006-0000-4006-8006-000000000006",
&[PatchEntry {
file_name: "package/payload.txt",
before_hash: &before,
after_hash: &after,
}],
);
write_blob(&socket_dir, &after, patched);
let env = apply_and_parse(
cwd,
&packages,
&[
("NUGET_PACKAGES", packages.to_str().unwrap()),
("SOCKET_EXPERIMENTAL_NUGET", "1"),
],
);
assert_eq!(std::fs::read(&target).unwrap(), patched);
let record = find_sidecar_record(&env, "nuget");
let advisory = record.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(".nupkg.metadata"),
"advisory message must reference the metadata path; got {msg:?}"
);
let files = record["files"].as_array().expect("files array");
assert!(
files.is_empty(),
"failed fixup must not report any deleted files; got {record}"
);
}
#[cfg(feature = "nuget")]
#[test]
fn nuget_apply_signed_package_emits_files_and_advisory() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path();
let packages = cwd.join("nuget-packages");
let pkg_dir = packages.join("newtonsoft.json").join("13.0.3");
std::fs::create_dir_all(pkg_dir.join("lib")).unwrap();
std::fs::write(
pkg_dir.join(".nupkg.metadata"),
r#"{"contentHash":"deadbeef"}"#,
)
.unwrap();
std::fs::write(
pkg_dir.join("newtonsoft.json.13.0.3.nupkg.sha512"),
"abc123",
)
.unwrap();
let target = pkg_dir.join("payload.txt");
let original = b"hello\n";
std::fs::write(&target, original).unwrap();
let patched = b"hello patched\n";
let before = git_sha256(original);
let after = git_sha256(patched);
let socket_dir = cwd.join(".socket");
write_minimal_manifest(
&socket_dir,
"pkg:nuget/Newtonsoft.Json@13.0.3",
"20000005-0000-4005-8005-000000000005",
&[PatchEntry {
file_name: "package/payload.txt",
before_hash: &before,
after_hash: &after,
}],
);
write_blob(&socket_dir, &after, patched);
let env = apply_and_parse(
cwd,
&packages,
&[
("NUGET_PACKAGES", packages.to_str().unwrap()),
("SOCKET_EXPERIMENTAL_NUGET", "1"),
],
);
let record = find_sidecar_record(&env, "nuget");
let files = record["files"].as_array().expect("files array");
assert_eq!(files.len(), 1, "metadata deletion must still be reported");
assert_eq!(files[0]["path"], ".nupkg.metadata");
assert_eq!(files[0]["action"], "deleted");
let advisory = record.get("advisory").unwrap_or_else(|| {
panic!(
"signed package must emit an advisory alongside files[].\nrecord: {record}"
)
});
assert_eq!(
advisory["code"], "nuget_signed_package_tampered",
"code contract: signed-package case emits nuget_signed_package_tampered"
);
assert_eq!(advisory["severity"], "warning");
assert!(advisory["message"]
.as_str()
.map(|s| !s.is_empty())
.unwrap_or(false));
}