use super::*;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use crate::compat::host::{InputReads, Sandbox};
use crate::compat::zip::Flavour;
use crate::compat::zip::sink::FILE_SIZE_LIMIT;
struct Scratch(PathBuf);
impl Scratch {
fn new(tag: &str) -> Scratch {
static N: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let d =
std::env::temp_dir().join(format!("stuffr-zipupd-{}-{tag}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
Scratch(d)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn run(
dir: &Path,
flavour: Flavour,
args: &[&str],
) -> (i32, String, String, BTreeMap<PathBuf, InputReads>) {
let reads = Arc::new(Mutex::new(BTreeMap::new()));
let stdout = Arc::new(Mutex::new(Vec::new()));
let host = Host::Sandbox(Sandbox {
dir: dir.to_path_buf(),
stdin: Some(Vec::new()),
stdout: stdout.clone(),
budget: Arc::new(AtomicU64::new(u64::MAX)),
reads: reads.clone(),
});
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let env = |_: &str| None;
let mut err = Vec::new();
let code = crate::compat::zip::main(flavour, "zip", b"zip", &args, &env, host, &mut err);
let out = stdout.lock().unwrap().clone();
let reads = reads.lock().unwrap().clone();
(
code,
String::from_utf8_lossy(&out).into_owned(),
String::from_utf8_lossy(&err).into_owned(),
reads,
)
}
fn names(dir: &Path) -> Vec<String> {
let mut v: Vec<String> = std::fs::read_dir(dir)
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
v.sort();
v
}
fn with_archive(tag: &str) -> Scratch {
let s = Scratch::new(tag);
std::fs::write(s.0.join("a.txt"), b"hello\n").unwrap();
let mut big = Vec::new();
let mut x: u32 = 1;
for _ in 0..200_000 {
x = x.wrapping_mul(1_103_515_245).wrapping_add(12345);
big.push((x >> 16) as u8);
}
std::fs::write(s.0.join("big"), big).unwrap();
let (code, out, err, _) = run(&s.0, Flavour::Apple, &["t.zip", "a.txt"]);
assert_eq!(code, 0, "{out} {err}");
s
}
#[test]
fn a_failed_write_mid_update_keeps_the_old_archive() {
for args in [
&["t.zip", "big"][..],
&["-0", "t.zip", "big"],
&["-u", "t.zip", "a.txt", "big"],
&["-g", "t.zip", "big"],
] {
let s = with_archive("full");
let old = std::fs::read(s.0.join("t.zip")).unwrap();
FILE_SIZE_LIMIT.with(|l| l.set(Some(8 * 1024)));
let (code, out, _, _) = run(&s.0, Flavour::Apple, args);
FILE_SIZE_LIMIT.with(|l| l.set(None));
assert_eq!(code, 14, "{args:?}: {out}");
let restore = if args[0] == "-g" {
"attempting to restore t.zip to its previous state\n"
} else {
""
};
assert!(
out.ends_with(&format!(
"zip error: Output file write failure (write error on zip file)\n{restore}"
)),
"{args:?}: {out:?}"
);
assert_eq!(std::fs::read(s.0.join("t.zip")).unwrap(), old, "{args:?}");
assert_eq!(names(&s.0), ["a.txt", "big", "t.zip"], "{args:?}");
}
}
#[test]
fn the_old_archive_is_read_through_track() {
let s = with_archive("track");
let len = std::fs::metadata(s.0.join("t.zip")).unwrap().len();
let (code, out, _, reads) = run(&s.0, Flavour::Apple, &["t.zip", "big"]);
assert_eq!(code, 0, "{out}");
let r = reads.get(&s.0.join("t.zip")).expect("t.zip was read");
assert!(r.opens >= 2, "{r:?}");
assert!(r.bytes >= len, "{r:?}");
}
#[test]
fn an_end_record_cut_short_reads_as_zeros() {
let s = Scratch::new("cut");
std::fs::write(s.0.join("a.txt"), b"hello\n").unwrap();
std::fs::write(s.0.join("t.zip"), b"PK\x05\x06").unwrap();
let (code, out, _, _) = run(&s.0, Flavour::Apple, &["t.zip", "a.txt"]);
assert_eq!(code, 0, "{out}");
assert_eq!(out, " adding: a.txt (stored 0%)\n");
}
#[test]
fn growing_writes_a_copy() {
let s = with_archive("grow");
let old = std::fs::read(s.0.join("t.zip")).unwrap();
let (code, out, _, _) = run(&s.0, Flavour::Apple, &["-T", "-g", "t.zip", "big"]);
assert_eq!(code, 8, "{out}");
assert_eq!(std::fs::read(s.0.join("t.zip")).unwrap(), old);
assert_eq!(names(&s.0), ["a.txt", "big", "t.zip"]);
let (code, out, _, _) = run(&s.0, Flavour::Apple, &["-g", "t.zip", "big"]);
assert_eq!(code, 0, "{out}");
let new = std::fs::read(s.0.join("t.zip")).unwrap();
let cenbeg = old.windows(4).position(|w| w == b"PK\x01\x02").unwrap();
assert_eq!(new[..cenbeg], old[..cenbeg]);
}
#[test]
fn a_sandboxed_test_fails_in_both_flavours() {
for f in [Flavour::Apple, Flavour::Ubuntu] {
let s = with_archive("test");
let (code, out, err, _) = run(&s.0, f, &["-T", "t.zip", "big"]);
assert_eq!(code, 8, "{f:?} {out}");
assert!(err.is_empty(), "{err}");
assert!(out.contains("\ttest of t.zip FAILED\n") || out.contains("test of t.zip FAILED\n"));
assert_eq!(names(&s.0), ["a.txt", "big", "t.zip"]);
}
}
#[test]
fn the_unzip_version_is_read_as_sscanf_reads_it() {
assert_eq!(
scan_unzip_version(b"UnZip 6.00 of 20 April 2009\n"),
Some(6.0)
);
assert_eq!(scan_unzip_version(b"UnZip5.52 x"), Some(5.52));
assert_eq!(scan_unzip_version(b"UnZip 1e1x"), Some(10.0));
assert_eq!(scan_unzip_version(b"UnZip .5"), Some(0.5));
assert_eq!(scan_unzip_version(b"UnZip x"), None);
assert_eq!(scan_unzip_version(b"unzip 6.00"), None);
assert_eq!(scan_unzip_version(b""), None);
}
fn declared_sizes(entries: &[(&[u8], u64, u64)]) -> Vec<u8> {
let mut b = Vec::new();
let mut offs = Vec::new();
for (name, _, _) in entries {
offs.push(b.len() as u32);
b.extend_from_slice(b"PK\x03\x04");
b.extend_from_slice(&[10, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
b.extend_from_slice(&[0u8; 4]); b.extend_from_slice(&1u32.to_le_bytes());
b.extend_from_slice(&1u32.to_le_bytes());
b.extend_from_slice(&(name.len() as u16).to_le_bytes());
b.extend_from_slice(&0u16.to_le_bytes());
b.extend_from_slice(name);
b.push(b'x');
}
let cd = b.len();
for ((name, siz, len), off) in entries.iter().zip(&offs) {
b.extend_from_slice(b"PK\x01\x02");
b.extend_from_slice(&[0x1e, 3, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
b.extend_from_slice(&[0u8; 4]);
b.extend_from_slice(&u32::MAX.to_le_bytes());
b.extend_from_slice(&u32::MAX.to_le_bytes());
b.extend_from_slice(&(name.len() as u16).to_le_bytes());
b.extend_from_slice(&20u16.to_le_bytes());
b.extend_from_slice(&[0u8; 8]); b.extend_from_slice(&0x81a4_0000u32.to_le_bytes());
b.extend_from_slice(&off.to_le_bytes());
b.extend_from_slice(name);
b.extend_from_slice(&[1, 0, 16, 0]);
b.extend_from_slice(&len.to_le_bytes());
b.extend_from_slice(&siz.to_le_bytes());
}
let cd_len = b.len() - cd;
b.extend_from_slice(b"PK\x05\x06\0\0\0\0");
let n = entries.len() as u16;
b.extend_from_slice(&n.to_le_bytes());
b.extend_from_slice(&n.to_le_bytes());
b.extend_from_slice(&(cd_len as u32).to_le_bytes());
b.extend_from_slice(&(cd as u32).to_le_bytes());
b.extend_from_slice(&0u16.to_le_bytes());
b
}
#[test]
fn declared_sizes_near_the_top_wrap() {
let hostile = declared_sizes(&[
(b"a", u64::MAX, u64::MAX),
(b"b", u64::MAX - 1, i64::MAX as u64),
(b"c", i64::MAX as u64, i64::MAX as u64),
]);
for args in [
&["t.zip", "n.txt"][..],
&["-v", "t.zip", "n.txt"],
&["-sf", "t.zip"],
&["-d", "-dc", "-db", "t.zip", "a"],
&["-U", "-v", "t.zip", "*", "--out", "o.zip"],
&["-g", "-v", "t.zip", "n.txt"],
&["-u", "t.zip"],
&["-o", "-z", "t.zip"],
] {
let s = Scratch::new("hostile");
std::fs::write(s.0.join("t.zip"), &hostile).unwrap();
std::fs::write(s.0.join("n.txt"), b"new\n").unwrap();
let (code, _, _, _) = run(&s.0, Flavour::Apple, args);
assert!(
crate::compat::zip::EXIT_CODES.contains(&code),
"{args:?}: {code}"
);
}
}
#[test]
fn show_files_with_move_deletes_nothing() {
let s = with_archive("sfm");
let (code, out, _, _) = run(
&s.0,
Flavour::Apple,
&["-sf", "-m", "t.zip", "a.txt", "big"],
);
assert_eq!(code, 0, "{out}");
assert!(out.starts_with("Would Add/Update:\n"), "{out}");
assert_eq!(names(&s.0), ["a.txt", "big", "t.zip"]);
}
#[test]
fn tt_command_is_whole_at_every_length() {
for ul in [1usize, 4, 5, 6, 7, 8, 13, 16, 29] {
for nl in [1usize, 2, 3, 4, 8, 9, 15, 16, 17, 24] {
let (up, name) = (vec![b'c'; ul], vec![b'n'; nl]);
let cmd = super::tt_command(&up, &name);
assert_eq!(cmd.len(), ul + nl + 3, "{ul} {nl}");
assert_eq!(&cmd[..ul], &up[..]);
assert_eq!(&cmd[ul..ul + 2], b" '");
assert_eq!(&cmd[ul + 2..ul + 2 + nl], &name[..]);
assert_eq!(cmd.last(), Some(&b'\''));
}
}
assert_eq!(super::tt_command(b"t {} x", b"zi1"), b"t 'zi1' x");
assert_eq!(super::tt_command(b"{}{}", b"z"), b" 'z' {}");
}
#[test]
fn growing_with_out_publishes_the_copy() {
let s = with_archive("grow-out");
let old = std::fs::read(s.0.join("t.zip")).unwrap();
std::fs::write(s.0.join("o.zip"), b"an older o.zip").unwrap();
let (code, out, err, _) = run(
&s.0,
Flavour::Apple,
&["-g", "t.zip", "--out", "o.zip", "big"],
);
assert_eq!(code, 0, "{out} {err}");
assert!(
std::fs::read(s.0.join("t.zip")).unwrap() == old,
"the input changed"
);
let grown = std::fs::read(s.0.join("o.zip")).unwrap();
let cenbeg = old.windows(4).position(|w| w == b"PK\x01\x02").unwrap();
assert!(grown[..cenbeg] == old[..cenbeg], "o.zip is not t.zip grown");
assert!(grown.len() > old.len(), "{} <= {}", grown.len(), old.len());
let cd = grown.windows(4).position(|w| w == b"PK\x01\x02").unwrap();
let central = &grown[cd..];
for name in [&b"a.txt"[..], b"big"] {
assert!(central.windows(name.len()).any(|w| w == name), "{name:?}");
}
assert_eq!(names(&s.0), ["a.txt", "big", "o.zip", "t.zip"]);
}
#[test]
fn a_failed_run_leaves_the_output_as_it_was() {
let s = with_archive("dev14-out");
let precious = b"precious bytes, not a zip".to_vec();
std::fs::write(s.0.join("o.zip"), &precious).unwrap();
FILE_SIZE_LIMIT.with(|l| l.set(Some(8 * 1024)));
let (code, out, _, _) = run(&s.0, Flavour::Apple, &["t.zip", "big", "--out", "o.zip"]);
FILE_SIZE_LIMIT.with(|l| l.set(None));
assert_eq!(code, 14, "{out}");
assert_eq!(std::fs::read(s.0.join("o.zip")).unwrap(), precious);
assert_eq!(names(&s.0), ["a.txt", "big", "o.zip", "t.zip"]);
let s = with_archive("dev14-empty");
let mut empty = b"PK\x05\x06".to_vec();
empty.extend_from_slice(&[0u8; 18]);
std::fs::write(s.0.join("e.zip"), &empty).unwrap();
FILE_SIZE_LIMIT.with(|l| l.set(Some(8 * 1024)));
let (code, out, _, _) = run(&s.0, Flavour::Apple, &["e.zip", "big"]);
FILE_SIZE_LIMIT.with(|l| l.set(None));
assert_eq!(code, 14, "{out}");
assert_eq!(std::fs::read(s.0.join("e.zip")).unwrap(), empty);
assert_eq!(names(&s.0), ["a.txt", "big", "e.zip", "t.zip"]);
}