#![cfg(target_os = "macos")]
use crate::common::*;
use std::path::Path;
use std::process::Command;
const ATTR: &str = "com.apple.quarantine";
const VALUE: &str = "0081;5f000000;Safari;";
fn quarantine_of(path: &Path) -> Option<String> {
let out = Command::new("/usr/bin/xattr")
.args(["-p", ATTR])
.arg(path)
.output()
.unwrap();
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
}
fn quarantine(path: &Path) {
let st = Command::new("/usr/bin/xattr")
.args(["-w", ATTR, VALUE])
.arg(path)
.status()
.unwrap();
assert!(st.success());
}
fn tree_tar(path: &Path) {
write_raw_tar(
path,
&[
Raw::Dir("d/"),
Raw::File("d/x", b"x"),
Raw::File("g/h/i", b"i"),
Raw::Hardlink("d/y", "d/x"),
Raw::Symlink("d/link", "x"),
],
);
}
fn nodes(root: &Path) -> Vec<std::path::PathBuf> {
let mut all = vec![root.to_path_buf()];
let mut i = 0;
while i < all.len() {
let p = all[i].clone();
if std::fs::symlink_metadata(&p).unwrap().is_dir() {
for e in std::fs::read_dir(&p).unwrap() {
all.push(e.unwrap().path());
}
}
i += 1;
}
all.retain(|p| {
!std::fs::symlink_metadata(p)
.unwrap()
.file_type()
.is_symlink()
});
all
}
fn assert_all(root: &Path, want: Option<&str>, at_least: usize) {
let nodes = nodes(root);
assert!(nodes.len() >= at_least, "{nodes:?}");
for n in nodes {
assert_eq!(quarantine_of(&n).as_deref(), want, "{}", n.display());
}
}
fn ok(args: &[&str]) {
let out = run_output(args);
assert!(
out.status.code().is_some_and(|c| c == 0 || c == 4),
"{args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn unpack_and_salvage_quarantine_everything_they_make() {
let dir = tmp_dir();
let archive = dir.join("q.tar");
tree_tar(&archive);
quarantine(&archive);
let out = dir.join("out");
ok(&[
"unpack",
archive.to_str().unwrap(),
"-C",
out.to_str().unwrap(),
]);
assert_all(&out, Some(VALUE), 7);
let salvaged = dir.join("salvaged");
ok(&[
"salvage",
archive.to_str().unwrap(),
"-C",
salvaged.to_str().unwrap(),
]);
assert_all(&salvaged, Some(VALUE), 4);
let pre = dir.join("pre");
std::fs::create_dir(&pre).unwrap();
ok(&[
"unpack",
archive.to_str().unwrap(),
"-C",
pre.to_str().unwrap(),
]);
assert_eq!(quarantine_of(&pre), None);
assert_eq!(quarantine_of(&pre.join("d/x")).as_deref(), Some(VALUE));
}
#[test]
fn an_unquarantined_archive_makes_nothing_quarantined() {
let dir = tmp_dir();
let archive = dir.join("clean.tar");
tree_tar(&archive);
let out = dir.join("out");
ok(&[
"unpack",
archive.to_str().unwrap(),
"-C",
out.to_str().unwrap(),
]);
assert_all(&out, None, 7);
}
#[test]
fn a_single_stream_decode_carries_the_quarantine() {
let dir = tmp_dir();
let gz = dir.join("tool.gz");
std::fs::write(&gz, gzip_wrap(b"#!/bin/sh\necho hi\n")).unwrap();
quarantine(&gz);
let out = dir.join("tool");
ok(&["unpack", gz.to_str().unwrap(), "-o", out.to_str().unwrap()]);
assert_eq!(quarantine_of(&out).as_deref(), Some(VALUE));
let clean = dir.join("clean.gz");
std::fs::write(&clean, gzip_wrap(b"data")).unwrap();
let out = dir.join("clean");
ok(&[
"unpack",
clean.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
]);
assert_eq!(quarantine_of(&out), None);
}
#[test]
fn convert_between_containers_carries_the_quarantine() {
let dir = tmp_dir();
let tree = dir.join("tree");
std::fs::create_dir(&tree).unwrap();
std::fs::write(tree.join("a.txt"), b"alpha").unwrap();
let zip = dir.join("t.zip");
ok(&["pack", tree.to_str().unwrap(), "-o", zip.to_str().unwrap()]);
quarantine(&zip);
let tgz = dir.join("t.tar.gz");
ok(&[
"convert",
zip.to_str().unwrap(),
"-o",
tgz.to_str().unwrap(),
]);
assert_eq!(quarantine_of(&tgz).as_deref(), Some(VALUE));
let clean = dir.join("clean.zip");
ok(&[
"pack",
tree.to_str().unwrap(),
"-o",
clean.to_str().unwrap(),
]);
let out = dir.join("clean.tar.gz");
ok(&[
"convert",
clean.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
]);
assert_eq!(quarantine_of(&out), None);
}
#[test]
fn a_forced_output_through_a_symlink_quarantines_the_file_it_reaches() {
let dir = tmp_dir();
let input = dir.join("q.txt");
std::fs::write(&input, b"downloaded").unwrap();
quarantine(&input);
let real = dir.join("real.gz");
std::fs::write(&real, b"old").unwrap();
let link = dir.join("link.gz");
std::os::unix::fs::symlink(&real, &link).unwrap();
ok(&[
"pack",
input.to_str().unwrap(),
"-o",
link.to_str().unwrap(),
"--force",
]);
assert!(
std::fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink()
);
assert_eq!(quarantine_of(&real).as_deref(), Some(VALUE));
}