use std::collections::HashMap;
use socket_patch_core::patch::cow::{break_hardlink_if_needed, CowAction};
use socket_patch_core::patch::sidecars::dispatch_fixup;
#[tokio::test]
async fn dispatch_fixup_empty_patched_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let out = dispatch_fixup(
"pkg:cargo/anything@1.0.0",
tmp.path(),
&[],
&HashMap::new(),
)
.await
.unwrap();
assert!(out.is_none(), "empty patched must short-circuit to None");
}
#[tokio::test]
async fn dispatch_fixup_unknown_ecosystem_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let out = dispatch_fixup(
"pkg:totally-not-an-ecosystem/x@1",
tmp.path(),
&["x".to_string()],
&HashMap::new(),
)
.await
.unwrap();
assert!(out.is_none(), "unknown ecosystem must short-circuit to None");
}
#[cfg(feature = "cargo")]
#[tokio::test]
async fn dispatch_fixup_cargo_sha256_file_failure_arm() {
use socket_patch_core::patch::sidecars::SidecarError;
let tmp = tempfile::tempdir().unwrap();
let pkg = tmp.path();
std::fs::write(
pkg.join(".cargo-checksum.json"),
r#"{"files":{"a.txt":"deadbeef"},"package":"00"}"#,
)
.unwrap();
let result = dispatch_fixup(
"pkg:cargo/anything@1.0.0",
pkg,
&["package/missing-on-disk.txt".to_string()],
&HashMap::new(),
)
.await;
let err = result.expect_err("missing file in patched list must surface as Err");
match err {
SidecarError::Io { path, .. } => {
assert!(
path.contains("missing-on-disk.txt"),
"Io error path must reference the missing file; got {path:?}"
);
}
other => panic!("expected SidecarError::Io, got {other:?}"),
}
}
#[cfg(feature = "nuget")]
#[tokio::test]
async fn dispatch_fixup_nuget_with_nonexistent_pkg_path() {
let tmp = tempfile::tempdir().unwrap();
let absent = tmp.path().join("does-not-exist");
let out = dispatch_fixup(
"pkg:nuget/Anything@1.0.0",
&absent,
&["package/file.txt".to_string()],
&HashMap::new(),
)
.await
.unwrap();
assert!(
out.is_none(),
"non-existent pkg_path must yield no sidecar record"
);
}
#[tokio::test]
async fn cow_missing_path_yields_no_file() {
let tmp = tempfile::tempdir().unwrap();
let action =
break_hardlink_if_needed(&tmp.path().join("does-not-exist.txt"))
.await
.expect("lstat NotFound is the explicit early-return arm");
assert!(matches!(action, CowAction::NoFile));
}
#[cfg(unix)]
#[tokio::test]
async fn cow_lstat_permission_denied_propagates_io_error() {
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
if Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim() == "0")
.unwrap_or(false)
{
eprintln!("SKIP: root bypasses dir-search permission checks");
return;
}
let tmp = tempfile::tempdir().unwrap();
let locked = tmp.path().join("locked");
std::fs::create_dir(&locked).unwrap();
let target = locked.join("file.txt");
std::fs::write(&target, b"content").unwrap();
let mut perms = std::fs::metadata(&locked).unwrap().permissions();
perms.set_mode(0o000);
std::fs::set_permissions(&locked, perms).unwrap();
let result = break_hardlink_if_needed(&target).await;
let mut restore = std::fs::metadata(&locked).unwrap().permissions();
restore.set_mode(0o755);
let _ = std::fs::set_permissions(&locked, restore);
let err = result.expect_err("expected I/O error from locked-dir lstat");
assert_ne!(
err.kind(),
std::io::ErrorKind::NotFound,
"expected permission-denied class error; got {err:?}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn cow_symlink_to_missing_target_propagates_read_error() {
let tmp = tempfile::tempdir().unwrap();
let link = tmp.path().join("dangling");
let absent = tmp.path().join("does-not-exist");
std::os::unix::fs::symlink(&absent, &link).unwrap();
let err = break_hardlink_if_needed(&link)
.await
.expect_err("read through dangling symlink must propagate the error");
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn cow_symlink_unremovable_propagates_remove_error() {
use std::process::Command;
if Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim() == "0")
.unwrap_or(false)
{
eprintln!("SKIP: root bypasses chflags uchg restrictions");
return;
}
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("real-file.txt");
std::fs::write(&target, b"content").unwrap();
let link = tmp.path().join("immutable-link");
std::os::unix::fs::symlink(&target, &link).unwrap();
let status = Command::new("chflags")
.arg("-h")
.arg("uchg")
.arg(&link)
.status()
.expect("chflags");
assert!(status.success());
let result = break_hardlink_if_needed(&link).await;
let _ = Command::new("chflags").arg("-h").arg("nouchg").arg(&link).status();
let err = result.expect_err("rename over immutable symlink must propagate EPERM");
assert_ne!(err.kind(), std::io::ErrorKind::NotFound);
let meta = std::fs::symlink_metadata(&link)
.expect("failed CoW must leave the original symlink in place");
assert!(
meta.file_type().is_symlink(),
"original symlink must survive a failed break, got {meta:?}"
);
let leftover: Vec<_> = std::fs::read_dir(tmp.path())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with(".socket-cow-"))
.collect();
assert!(leftover.is_empty(), "stage litter left behind: {leftover:?}");
}
#[cfg(unix)]
#[tokio::test]
async fn cow_hardlink_unreadable_propagates_read_error() {
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
if Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim() == "0")
.unwrap_or(false)
{
eprintln!("SKIP: root bypasses chmod 0000 restrictions");
return;
}
let tmp = tempfile::tempdir().unwrap();
let a = tmp.path().join("a.txt");
std::fs::write(&a, b"data").unwrap();
let b = tmp.path().join("b.txt");
std::fs::hard_link(&a, &b).unwrap();
let mut p = std::fs::metadata(&a).unwrap().permissions();
p.set_mode(0o000);
std::fs::set_permissions(&a, p).unwrap();
let result = break_hardlink_if_needed(&b).await;
let mut restore = std::fs::metadata(&a).unwrap().permissions();
restore.set_mode(0o644);
let _ = std::fs::set_permissions(&a, restore);
let err = result.expect_err("read of unreadable hardlinked file must propagate");
assert_ne!(err.kind(), std::io::ErrorKind::NotFound);
}
#[cfg(unix)]
#[tokio::test]
async fn cow_stage_write_failure_propagates() {
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
if Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim() == "0")
.unwrap_or(false)
{
eprintln!("SKIP: root bypasses chmod 0500 restrictions");
return;
}
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("pkg");
std::fs::create_dir(&dir).unwrap();
let a = dir.join("orig.txt");
std::fs::write(&a, b"content").unwrap();
let b = dir.join("link.txt");
std::fs::hard_link(&a, &b).unwrap();
let mut p = std::fs::metadata(&dir).unwrap().permissions();
p.set_mode(0o500);
std::fs::set_permissions(&dir, p).unwrap();
let result = break_hardlink_if_needed(&b).await;
let mut restore = std::fs::metadata(&dir).unwrap().permissions();
restore.set_mode(0o755);
let _ = std::fs::set_permissions(&dir, restore);
let err = result.expect_err("stage write into read-only parent must fail");
assert_ne!(err.kind(), std::io::ErrorKind::NotFound);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn cow_symlink_stage_write_failure_propagates() {
use std::process::Command;
if Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim() == "0")
.unwrap_or(false)
{
eprintln!("SKIP: root bypasses ACL deny entries");
return;
}
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("pkg");
std::fs::create_dir(&dir).unwrap();
let target = dir.join("orig.txt");
std::fs::write(&target, b"shared bytes").unwrap();
let link = dir.join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
let user = std::env::var("USER").unwrap_or_else(|_| "$(id -un)".to_string());
let status = Command::new("chmod")
.arg("+a")
.arg(format!("{user} deny add_file"))
.arg(&dir)
.status()
.expect("chmod +a");
assert!(status.success(), "ACL set must succeed");
let result = break_hardlink_if_needed(&link).await;
let _ = Command::new("chmod").arg("-a#").arg("0").arg(&dir).status();
let err = result.expect_err(
"with deny-add_file ACL, write_via_stage_rename's stage create must fail, \
surfacing the stage-write `?` Err arm",
);
assert_ne!(err.kind(), std::io::ErrorKind::NotFound);
let meta = std::fs::symlink_metadata(&link)
.expect("failed CoW must leave the original symlink in place");
assert!(
meta.file_type().is_symlink(),
"original symlink must survive a failed stage write, got {meta:?}"
);
assert_eq!(
std::fs::read(&link).unwrap(),
b"shared bytes",
"symlink must still resolve to its original target content"
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn cow_rename_failure_runs_stage_cleanup() {
use std::os::unix::fs::MetadataExt;
use std::process::Command;
if Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim() == "0")
.unwrap_or(false)
{
eprintln!("SKIP: root bypasses chflags uchg restrictions");
return;
}
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("file.txt");
std::fs::write(&target, b"original").unwrap();
let link = tmp.path().join("hardlink.txt");
std::fs::hard_link(&target, &link).unwrap();
assert_eq!(
std::fs::metadata(&target).unwrap().nlink(),
2,
"test setup: target must have nlink=2 to drive cow's hardlink branch"
);
let chflags_status = Command::new("chflags")
.arg("uchg")
.arg(&target)
.status()
.expect("chflags binary must exist on macOS");
assert!(
chflags_status.success(),
"chflags uchg must succeed for a file we own"
);
let cow_result = break_hardlink_if_needed(&target).await;
let _ = Command::new("chflags").arg("nouchg").arg(&target).status();
let err = cow_result.expect_err("immutable target must cause rename failure");
assert_ne!(
err.kind(),
std::io::ErrorKind::NotFound,
"expected EPERM-class error, got {err:?}"
);
let leftover_stages: Vec<_> = std::fs::read_dir(tmp.path())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with(".socket-cow-")
})
.collect();
assert!(
leftover_stages.is_empty(),
"stage cleanup must remove all .socket-cow-* turds; found {leftover_stages:?}"
);
}