#![cfg(unix)]
use std::path::{Path, PathBuf};
#[path = "common/mod.rs"]
mod common;
use common::{
assert_run_ok, git_sha256, git_sha256_file, run, write_blob, write_minimal_manifest,
PatchEntry,
};
const TEST_PURL: &str = "pkg:npm/cow-fixture@1.0.0";
const TEST_UUID: &str = "33333333-3333-4333-8333-333333333333";
const ORIGINAL_BYTES: &[u8] = b"module.exports = function() { return 'before'; };\n";
const PATCHED_BYTES: &[u8] = b"module.exports = function() { return 'after'; };\n";
struct Fixture {
root: tempfile::TempDir,
}
impl Fixture {
fn new() -> Self {
let dir = tempfile::tempdir().expect("tempdir");
let pkg = dir.path().join("node_modules/cow-fixture");
std::fs::create_dir_all(&pkg).unwrap();
std::fs::write(
pkg.join("package.json"),
r#"{"name":"cow-fixture","version":"1.0.0"}"#,
)
.unwrap();
Fixture { root: dir }
}
fn root(&self) -> &Path {
self.root.path()
}
fn index_js(&self) -> PathBuf {
self.root.path().join("node_modules/cow-fixture/index.js")
}
fn stage_patch(&self) -> (String, String) {
let before_hash = git_sha256(ORIGINAL_BYTES);
let after_hash = git_sha256(PATCHED_BYTES);
let socket = self.root.path().join(".socket");
write_minimal_manifest(
&socket,
TEST_PURL,
TEST_UUID,
&[PatchEntry {
file_name: "package/index.js",
before_hash: &before_hash,
after_hash: &after_hash,
}],
);
write_blob(&socket, &after_hash, PATCHED_BYTES);
(before_hash, after_hash)
}
}
#[test]
fn apply_breaks_hardlink_before_patching() {
let fx = Fixture::new();
let outside = fx.root().join("outside-store-entry.js");
std::fs::write(&outside, ORIGINAL_BYTES).unwrap();
std::fs::hard_link(&outside, fx.index_js()).unwrap();
use std::os::unix::fs::MetadataExt;
assert_eq!(
std::fs::metadata(&outside).unwrap().nlink(),
2,
"hardlink fixture should produce nlink=2"
);
assert_eq!(git_sha256_file(&fx.index_js()), git_sha256(ORIGINAL_BYTES));
fx.stage_patch();
assert_run_ok(fx.root(), &["apply"], "socket-patch apply");
assert_eq!(
git_sha256_file(&fx.index_js()),
git_sha256(PATCHED_BYTES),
"package's index.js should now match the patched bytes"
);
assert_eq!(
git_sha256_file(&outside),
git_sha256(ORIGINAL_BYTES),
"the hardlinked sibling MUST stay byte-identical; CoW failure"
);
assert_eq!(
std::fs::metadata(&outside).unwrap().nlink(),
1,
"after CoW, the outside file should be a single-link inode"
);
}
#[test]
fn apply_replaces_symlink_with_private_file() {
let fx = Fixture::new();
let outside = fx.root().join("outside-target.js");
std::fs::write(&outside, ORIGINAL_BYTES).unwrap();
std::os::unix::fs::symlink(&outside, fx.index_js()).unwrap();
let lstat = std::fs::symlink_metadata(fx.index_js()).unwrap();
assert!(
lstat.file_type().is_symlink(),
"fixture must produce a symlink"
);
assert_eq!(git_sha256_file(&fx.index_js()), git_sha256(ORIGINAL_BYTES));
fx.stage_patch();
assert_run_ok(fx.root(), &["apply"], "socket-patch apply");
let post = std::fs::symlink_metadata(fx.index_js()).unwrap();
assert!(
post.file_type().is_file() && !post.file_type().is_symlink(),
"index.js must be a regular file after apply, not a symlink"
);
assert_eq!(
git_sha256_file(&fx.index_js()),
git_sha256(PATCHED_BYTES)
);
assert_eq!(
git_sha256_file(&outside),
git_sha256(ORIGINAL_BYTES),
"the symlink target must NOT have been mutated; CoW must replace the link with a private file"
);
}
#[test]
fn apply_breaks_hardlinks_on_multi_file_patch() {
let fx = Fixture::new();
let pkg = fx.root().join("node_modules/cow-fixture");
std::fs::create_dir_all(pkg.join("lib")).unwrap();
let outside_a = fx.root().join("outside-a.js");
let outside_b = fx.root().join("outside-b.js");
std::fs::write(&outside_a, b"AAA original\n").unwrap();
std::fs::write(&outside_b, b"BBB original\n").unwrap();
std::fs::hard_link(&outside_a, pkg.join("index.js")).unwrap();
std::fs::hard_link(&outside_b, pkg.join("lib/helper.js")).unwrap();
let before_a = git_sha256(b"AAA original\n");
let after_a = git_sha256(b"AAA patched!\n");
let before_b = git_sha256(b"BBB original\n");
let after_b = git_sha256(b"BBB patched!\n");
let socket = fx.root().join(".socket");
write_minimal_manifest(
&socket,
TEST_PURL,
TEST_UUID,
&[
PatchEntry {
file_name: "package/index.js",
before_hash: &before_a,
after_hash: &after_a,
},
PatchEntry {
file_name: "package/lib/helper.js",
before_hash: &before_b,
after_hash: &after_b,
},
],
);
write_blob(&socket, &after_a, b"AAA patched!\n");
write_blob(&socket, &after_b, b"BBB patched!\n");
assert_run_ok(fx.root(), &["apply"], "socket-patch apply multi-file");
assert_eq!(std::fs::read(pkg.join("index.js")).unwrap(), b"AAA patched!\n");
assert_eq!(
std::fs::read(pkg.join("lib/helper.js")).unwrap(),
b"BBB patched!\n"
);
assert_eq!(std::fs::read(&outside_a).unwrap(), b"AAA original\n");
assert_eq!(std::fs::read(&outside_b).unwrap(), b"BBB original\n");
}
#[test]
fn apply_against_regular_file_leaves_no_cow_litter() {
let fx = Fixture::new();
std::fs::write(fx.index_js(), ORIGINAL_BYTES).unwrap();
fx.stage_patch();
assert_run_ok(fx.root(), &["apply"], "socket-patch apply");
assert_eq!(git_sha256_file(&fx.index_js()), git_sha256(PATCHED_BYTES));
let pkg_dir = fx.root().join("node_modules/cow-fixture");
let mut entries = std::fs::read_dir(&pkg_dir).unwrap();
while let Some(Ok(entry)) = entries.next() {
let name = entry.file_name().to_string_lossy().to_string();
assert!(
!name.starts_with(".socket-cow-") && !name.starts_with(".socket-stage-"),
"stage / cow temp file leaked into package directory: {name}"
);
}
}
#[test]
fn apply_failure_does_not_cow_or_modify() {
let fx = Fixture::new();
let outside = fx.root().join("outside.js");
std::fs::write(&outside, ORIGINAL_BYTES).unwrap();
std::fs::hard_link(&outside, fx.index_js()).unwrap();
use std::os::unix::fs::MetadataExt;
let pre_inode = std::fs::metadata(&outside).unwrap().ino();
let before_hash = git_sha256(ORIGINAL_BYTES);
let claimed_after_hash = git_sha256(PATCHED_BYTES);
let socket = fx.root().join(".socket");
write_minimal_manifest(
&socket,
TEST_PURL,
TEST_UUID,
&[PatchEntry {
file_name: "package/index.js",
before_hash: &before_hash,
after_hash: &claimed_after_hash,
}],
);
write_blob(&socket, &claimed_after_hash, b"deliberately wrong bytes\n");
let (code, _stdout, _stderr) = run(fx.root(), &["apply"]);
assert_eq!(code, 1, "hash-mismatch apply must exit non-zero");
assert_eq!(git_sha256_file(&fx.index_js()), git_sha256(ORIGINAL_BYTES));
assert_eq!(git_sha256_file(&outside), git_sha256(ORIGINAL_BYTES));
assert_eq!(
std::fs::metadata(&outside).unwrap().ino(),
std::fs::metadata(fx.index_js()).unwrap().ino(),
"failed apply must not break the hardlink"
);
assert_eq!(pre_inode, std::fs::metadata(&outside).unwrap().ino());
}