use std::ffi::OsString;
use super::*;
#[test]
fn the_default_flavour_follows_the_platform() {
let apple = cfg!(any(
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
));
let want = if apple {
Flavour::Apple
} else {
Flavour::Ubuntu
};
assert_eq!(default_flavour(), want);
}
#[test]
fn the_flavour_variable_rejects_other_values() {
let d = default_flavour();
assert_eq!(flavour_from(None), Ok(d));
assert_eq!(flavour_from(Some("".into())), Ok(d));
assert_eq!(flavour_from(Some("apple".into())), Ok(Flavour::Apple));
assert_eq!(flavour_from(Some("ubuntu".into())), Ok(Flavour::Ubuntu));
for bad in ["Apple", "UBUNTU", " apple", "debian", "gnu"] {
assert_eq!(flavour_from(Some(bad.into())), Err(bad.into()));
}
}
fn go(name: &str, args: &[&str], stdin: &[u8]) -> (i32, String, String) {
let dir = std::env::temp_dir();
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let (code, out, err) = run_in(&dir, name, args, stdin, Flavour::Apple);
(
code,
String::from_utf8(out).unwrap(),
String::from_utf8(err).unwrap(),
)
}
#[test]
fn run_in_reaches_each_program() {
for (name, header) in [
("zip", "Zip 3.0 (July 5th 2008), by stuffr "),
("zipcloak", "ZipCloak 3.0 (July 5th 2008), by stuffr "),
("zipnote", "ZipNote 3.0 (July 5th 2008), by stuffr "),
("zipsplit", "ZipSplit 3.0 (July 5th 2008), by stuffr "),
] {
let (code, out, err) = go(name, &["-h"], b"");
assert_eq!((code, err.as_str()), (0, ""), "{name}");
assert!(out.contains(header), "{name}: {out}");
}
let (code, out, _) = go("zipnote", &[], b"");
assert_eq!(code, 0);
assert!(out.contains("Usage: \x20zipnote [-w] [-q] [-b path] zipfile"));
}
#[test]
fn a_sandbox_is_not_a_terminal() {
let dir = std::env::temp_dir();
let (code, out, err) = run_in(&dir, "zip", vec![], b"data", Flavour::Apple);
assert_eq!(
(code, err.as_slice()),
(0, &b" adding: - (deflated -49%)\n"[..])
);
assert!(out.starts_with(b"PK\x03\x04") && out.windows(4).any(|w| w == b"PK\x07\x08"));
let (code, out, _) = go("zip", &["-v"], b"");
assert_eq!(code, 0);
assert!(out.contains("Zip special compilation options:"), "{out}");
}
#[test]
fn the_exit_codes_are_the_ziperr_ones() {
for c in [
0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19,
] {
assert!(EXIT_CODES.contains(&c));
}
assert!(!EXIT_CODES.contains(&1));
assert!(!EXIT_CODES.contains(&17));
}
#[test]
fn an_io_error_gets_the_zip_io_error_line() {
let env = |_: &str| None;
let mut err = Vec::new();
let host = Host::Sandbox(Sandbox {
dir: std::env::temp_dir(),
stdin: None,
stdout: Arc::default(),
budget: Arc::new(AtomicU64::new(1 << 20)),
reads: Arc::default(),
});
let mut z = Zr::new(host, Flavour::Apple, "zip", &env, &mut err);
let e = io::Error::from_raw_os_error(2);
assert_eq!(z.ziperr_os(18, b"nosuch", &e), Exit { code: 18 });
let out = match &z.host {
Host::Sandbox(s) => s.stdout.lock().unwrap().clone(),
Host::Real => unreachable!(),
};
z.set_line(false);
drop(z);
assert_eq!(
String::from_utf8(out).unwrap(),
"zip I/O error: No such file or directory\nzip error: File not found or no read permission (nosuch)\n"
);
}
#[test]
fn asctime_is_c_asctime() {
let host = Host::Sandbox(Sandbox {
dir: std::env::temp_dir(),
stdin: None,
stdout: Arc::default(),
budget: Arc::new(AtomicU64::new(0)),
reads: Arc::default(),
});
assert_eq!(asctime(&host, 116_989_432), "Sun Sep 16 01:03:52 1973\n");
assert_eq!(
asctime(&host, 1_000_000_000),
"Sun Sep \x209 01:46:40 2001\n"
);
assert_eq!(asctime(&host, 0), "Thu Jan \x201 00:00:00 1970\n");
}
#[test]
fn the_log_is_charged_to_the_sandbox_budget() {
let dir = std::env::temp_dir();
let path = dir.join(format!("stuffr-zipcharge-{}-log", std::process::id()));
let _ = std::fs::remove_file(&path);
let budget = Arc::new(AtomicU64::new(10));
let host = Host::Sandbox(Sandbox {
dir,
stdin: None,
stdout: Arc::default(),
budget: budget.clone(),
reads: Arc::default(),
});
let env = |_: &str| None;
let mut err = Vec::new();
let mut z = Zr::new(host, Flavour::Apple, "zip", &env, &mut err);
z.log = Some(Box::new(std::fs::File::create(&path).unwrap()));
z.log_write(b"12345678");
z.log_write(b"abcdef");
drop(z);
let written = std::fs::read(&path).unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(written, b"12345678");
assert_eq!(budget.load(std::sync::atomic::Ordering::Relaxed), 2);
}
#[test]
fn a_pattern_file_is_a_tracked_open() {
let dir = std::env::temp_dir().join(format!("stuffr-zippat-{}-d", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("names"), b"a\nb\n").unwrap();
let args: Vec<OsString> = ["-i", "@names", "out.zip", "a"]
.iter()
.map(OsString::from)
.collect();
let (_, _, _, reads) = run_in_counted(Flavour::Apple, &dir, "zip", &args, b"");
let _ = std::fs::remove_dir_all(&dir);
let r = reads
.iter()
.find(|(p, _)| p.ends_with("names"))
.map(|(_, r)| *r)
.expect("the pattern file was opened through Host::track");
assert_eq!((r.opens, r.bytes, r.len), (1, 4, 4));
}
#[cfg(unix)]
struct SigScratch(std::path::PathBuf);
#[cfg(unix)]
impl SigScratch {
fn new(tag: &str) -> SigScratch {
let d = std::env::temp_dir().join(format!("stuffr-zipsig-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
SigScratch(d)
}
}
#[cfg(unix)]
impl Drop for SigScratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[cfg(unix)]
#[test]
fn an_interrupt_during_replace_leaves_a_whole_archive() {
use crate::compat::host;
use crate::compat::zip::sink;
use std::os::unix::io::AsRawFd;
use std::sync::atomic::Ordering;
for shape in ["plain", "hardlink", "symlink"] {
let s = SigScratch::new(shape);
let real = s.0.join("real.zip");
let archive = s.0.join("a.zip");
let tmp = s.0.join("zi123456");
std::fs::write(&real, b"OLD ARCHIVE BYTES").unwrap();
match shape {
"plain" => std::fs::rename(&real, &archive).unwrap(),
"hardlink" => std::fs::hard_link(&real, &archive).unwrap(),
_ => std::os::unix::fs::symlink(&real, &archive).unwrap(),
}
let new = vec![b'N'; 300_000];
let capture = std::fs::File::create(s.0.join("mesg")).unwrap();
let cfd = capture.as_raw_fd();
let status = unsafe {
let pid = libc::fork();
assert!(pid >= 0, "fork: {}", std::io::Error::last_os_error());
if pid == 0 {
host::disarm_inherited();
libc::dup2(cfd, 1);
host::install_zip_abort(&Host::Real, messages::INTERRUPTED_ABORTING);
host::zip_mesg_state(&Host::Real, false, false);
let made = host::create_new_armed_if(true, &tmp, host::ZIP_SIGNALS);
let Ok((mut f, _armed)) = made else {
libc::_exit(20);
};
if std::io::Write::write_all(&mut f, &new).is_err() {
libc::_exit(21);
}
drop(f);
sink::RAISE_MID_REPLACE.store(true, Ordering::SeqCst);
let r = sink::replace(&archive, &tmp);
libc::_exit(if r.is_ok() { 22 } else { 23 });
}
let mut status = 0;
while libc::waitpid(pid, &mut status, 0) < 0 {
assert_eq!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::EINTR)
);
}
status
};
assert!(
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 9,
"{shape}: status {status:#x}"
);
assert_eq!(
std::fs::read(&archive).ok().map(|b| b.len()),
Some(new.len()),
"{shape}: the archive is not the whole new one"
);
assert!(
std::fs::read(&archive).unwrap() == new,
"{shape}: not the new bytes"
);
if shape != "plain" {
assert!(
std::fs::read(&real).unwrap() == new,
"{shape}: through the link"
);
}
assert!(!tmp.exists(), "{shape}: the temporary survived");
let mut want = b"\n".to_vec();
want.extend_from_slice(messages::INTERRUPTED_ABORTING);
assert_eq!(
String::from_utf8_lossy(&std::fs::read(s.0.join("mesg")).unwrap()),
String::from_utf8_lossy(&want),
"{shape}: zip's answer"
);
}
}
#[cfg(unix)]
#[test]
fn an_interrupt_after_a_failed_replace_keeps_the_temporary() {
use crate::compat::host;
use crate::compat::zip::sink;
use std::os::unix::io::AsRawFd;
use std::sync::atomic::Ordering;
let s = SigScratch::new("failed");
let real = s.0.join("real.zip");
let archive = s.0.join("a.zip");
let tmp = s.0.join("zi654321");
std::fs::write(&real, b"OLD ARCHIVE BYTES").unwrap();
std::fs::hard_link(&real, &archive).unwrap();
let new = vec![b'N'; 300_000];
let capture = std::fs::File::create(s.0.join("mesg")).unwrap();
let cfd = capture.as_raw_fd();
let status = unsafe {
let pid = libc::fork();
assert!(pid >= 0, "fork: {}", std::io::Error::last_os_error());
if pid == 0 {
host::disarm_inherited();
libc::dup2(cfd, 1);
host::install_zip_abort(&Host::Real, messages::INTERRUPTED_ABORTING);
host::zip_mesg_state(&Host::Real, false, false);
let made = host::create_new_armed_if(true, &tmp, host::ZIP_SIGNALS);
let Ok((mut f, _armed)) = made else {
libc::_exit(20);
};
if std::io::Write::write_all(&mut f, &new).is_err() {
libc::_exit(21);
}
drop(f);
libc::signal(libc::SIGXFSZ, libc::SIG_IGN);
let limit = libc::rlimit {
rlim_cur: 4096,
rlim_max: libc::RLIM_INFINITY,
};
if libc::setrlimit(libc::RLIMIT_FSIZE, &limit) != 0 {
libc::_exit(24);
}
sink::RAISE_MID_REPLACE.store(true, Ordering::SeqCst);
let r = sink::replace(&archive, &tmp);
libc::_exit(if r.is_ok() { 22 } else { 23 });
}
let mut status = 0;
while libc::waitpid(pid, &mut status, 0) < 0 {
assert_eq!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::EINTR)
);
}
status
};
assert!(
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 9,
"status {status:#x}"
);
assert!(
std::fs::read(&tmp).ok().as_deref() == Some(&new[..]),
"the temporary, the only whole copy, is gone or cut"
);
let mut want = b"\n".to_vec();
want.extend_from_slice(messages::INTERRUPTED_ABORTING);
assert_eq!(
String::from_utf8_lossy(&std::fs::read(s.0.join("mesg")).unwrap()),
String::from_utf8_lossy(&want),
"zip's answer"
);
}