use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use super::super::{Flavour, Uz};
use super::*;
use crate::compat::host::{Host, SANDBOX_OUTPUT_CAP, Sandbox};
struct Run {
parsed: Result<Parsed, i32>,
o: Options,
extract_flag: bool,
noargs: bool,
out: String,
err: String,
}
fn parse(zipinfo: bool, args: &[&str], env: &[(&str, &str)]) -> Run {
parse_as(Flavour::Apple, zipinfo, args, env)
}
fn parse_as(f: Flavour, zipinfo: bool, args: &[&str], env: &[(&str, &str)]) -> Run {
let stdout = Arc::new(Mutex::new(Vec::new()));
let host = Host::Sandbox(Sandbox {
dir: std::env::temp_dir(),
stdin: Some(Vec::new()),
stdout: stdout.clone(),
budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
reads: Arc::new(Mutex::new(BTreeMap::new())),
});
let env: Vec<(String, OsString)> = env
.iter()
.map(|(k, v)| (k.to_string(), OsString::from(v)))
.collect();
let lookup = |k: &str| env.iter().find(|(n, _)| n == k).map(|(_, v)| v.clone());
let mut err = Vec::new();
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let (parsed, o, extract_flag, noargs) = {
let mut uz = Uz::new(host, f, &lookup, &mut err);
let parsed = if zipinfo {
parse_zipinfo(&mut uz, &args)
} else {
parse_unzip(&mut uz, &args)
};
(parsed, uz.o.clone(), uz.extract_flag, uz.noargs)
};
let out = std::mem::take(&mut *stdout.lock().unwrap());
Run {
parsed,
o,
extract_flag,
noargs,
out: String::from_utf8(out).unwrap(),
err: String::from_utf8(err).unwrap(),
}
}
fn unzip(args: &[&str]) -> Run {
parse(false, args, &[])
}
fn ok(r: &Run) -> &Parsed {
match &r.parsed {
Ok(p) => p,
Err(code) => panic!("exit {code}: {}{}", r.out, r.err),
}
}
fn names(v: &[OsString]) -> Vec<&str> {
v.iter().map(|a| a.to_str().unwrap()).collect()
}
#[test]
fn options_cluster_and_count() {
let r = unzip(&["-jo", "-qq", "-aaa", "-LL", "a.zip"]);
let p = ok(&r);
assert_eq!(p.archive, "a.zip");
assert!(r.o.jflag);
assert_eq!(
(r.o.overwrite_all, r.o.qflag, r.o.aflag, r.o.l_flag),
(1, 2, 2, 2)
);
assert!(r.extract_flag);
let r = unzip(&["-aab", "-p", "a.zip"]);
ok(&r);
assert_eq!((r.o.aflag, r.o.cflag, r.o.qflag), (0, true, 999));
assert!(!r.extract_flag);
let r = unzip(&["-vv", "a.zip"]);
ok(&r);
assert_eq!(r.o.vflag, 3);
let r = unzip(&["-ll", "a.zip"]);
ok(&r);
assert_eq!(r.o.vflag, 2);
}
#[test]
fn a_dash_negates_the_next_option() {
let r = unzip(&["-qqq", "--q", "a.zip"]);
ok(&r);
assert_eq!(r.o.qflag, 2);
let r = unzip(&["-qqq", "---q", "a.zip"]);
ok(&r);
assert_eq!(r.o.qflag, 1, "two dashes take two");
let r = unzip(&["-qq", "-x-q", "a.zip"]);
ok(&r);
assert_eq!(r.o.qflag, 1, "-x-q: -x is a no-op, -q negated");
let r = unzip(&["-qq", "--", "-q", "a.zip"]);
ok(&r);
assert_eq!(r.o.qflag, 1, "the negation carries into the next argument");
let r = unzip(&["-j", "-j-", "-ej", "a.zip"]);
ok(&r);
assert!(!r.o.jflag, "-e leaves the pending negation for -j");
let r = unzip(&["-n-n", "-o-o", "a.zip"]);
ok(&r);
assert!(!r.o.overwrite_none);
assert_eq!(r.o.overwrite_all, 0);
let r = unzip(&["-f-f", "a.zip"]);
ok(&r);
assert!(!r.o.fflag && !r.o.uflag);
}
#[test]
fn d_takes_its_value_attached_or_next() {
for args in [
&["-dout", "a.zip"][..],
&["-d", "out", "a.zip"],
&["a.zip", "-d", "out"],
&["a.zip", "-dout"],
] {
let r = unzip(args);
let p = ok(&r);
assert_eq!(p.exdir.as_deref(), Some(OsStr::new("out")), "{args:?}");
assert_eq!(r.o.exdir.as_deref(), Some(&b"out"[..]), "{args:?}");
assert!(p.include.is_empty() && p.exclude.is_empty(), "{args:?}");
}
let r = unzip(&["-dqq", "a.zip"]);
assert_eq!(ok(&r).exdir.as_deref(), Some(OsStr::new("qq")));
assert_eq!(r.o.qflag, 0);
for args in [&["-d", "-x", "a.zip"][..], &["-d"], &["a.zip", "-d"]] {
let r = unzip(args);
assert_eq!(r.parsed, Err(10), "{args:?}");
assert_eq!(r.err, m::MUST_GIVE_EXDIR, "{args:?}");
}
let r = unzip(&["-d", "x", "-d", "y", "a.zip"]);
assert_eq!((r.parsed, r.err.as_str()), (Err(10), m::ONLY_ONE_EXDIR));
let r = unzip(&["-q-d", "x", "a.zip"]);
assert_eq!((r.parsed, r.err.as_str()), (Err(10), m::MUST_GIVE_EXDIR));
let r = unzip(&["-dx", "a.zip", "-dy", "f"]);
let p = ok(&r);
assert_eq!(p.exdir.as_deref(), Some(OsStr::new("x")));
assert_eq!(names(&p.include), ["-dy", "f"]);
let r = unzip(&["-l", "a.zip", "-d", "x"]);
ok(&r);
assert_eq!(r.err, m::NOT_EXTRACTING);
}
#[test]
fn p_takes_a_password_and_a_second_is_read_as_options() {
let r = unzip(&["-P", "secret", "a.zip"]);
ok(&r);
assert_eq!(r.o.pwdarg.as_deref(), Some(&b"secret"[..]));
let r = unzip(&["-Pone", "-Pq", "a.zip"]);
ok(&r);
assert_eq!(r.o.pwdarg.as_deref(), Some(&b"one"[..]));
assert_eq!(r.o.qflag, 1, "the second -P is ignored, its q read");
for args in [&["-P"][..], &["-P", "-q", "a.zip"], &["--P", "x", "a.zip"]] {
let r = unzip(args);
assert_eq!(r.parsed, Err(10), "{args:?}");
assert_eq!(r.err, m::MUST_GIVE_PASSWD, "{args:?}");
}
}
#[test]
fn x_lists_and_operands() {
let r = unzip(&["a.zip", "f1", "f2", "-x", "e1", "e2"]);
let p = ok(&r);
assert_eq!(names(&p.include), ["f1", "f2"]);
assert_eq!(names(&p.exclude), ["e1", "e2"]);
let r = unzip(&["a.zip", "-x", "e1"]);
let p = ok(&r);
assert!(p.include.is_empty());
assert_eq!(names(&p.exclude), ["e1"]);
let r = unzip(&["a.zip", "f1", "-d", "dir", "f2"]);
let p = ok(&r);
assert_eq!(names(&p.include), ["f1"]);
assert_eq!(p.exdir.as_deref(), Some(OsStr::new("dir")));
let r = unzip(&["a.zip", "f1", "-x", "e1", "-d", "dir", "e2", "-x", "e3"]);
let p = ok(&r);
assert_eq!(names(&p.include), ["f1"]);
assert_eq!(names(&p.exclude), ["e1"]);
let r = unzip(&["a.zip", "-d", "dir", "f1", "-x", "e1"]);
let p = ok(&r);
assert_eq!(names(&p.include), ["f1"]);
assert_eq!(names(&p.exclude), ["e1"]);
let r = unzip(&["a.zip", "f1", "-d", "dir", "f2", "-x", "e"]);
let p = ok(&r);
assert_eq!(names(&p.include), ["f1"]);
assert_eq!(names(&p.exclude), ["e"]);
assert_eq!(p.exdir.as_deref(), Some(OsStr::new("dir")));
let r = unzip(&["a.zip", "-xf", "f"]);
assert_eq!(names(&ok(&r).include), ["-xf", "f"]);
let r = unzip(&["a.zip", "f1", "-x"]);
let p = ok(&r);
assert_eq!(names(&p.include), ["f1"]);
assert!(p.exclude.is_empty());
}
#[test]
fn usage_help_and_bad_options() {
let r = unzip(&["-h", "a.zip"]);
assert_eq!(r.parsed, Err(0));
assert!(r.out.contains(m::UNZIP_USAGE_LINE2), "{}", r.out);
assert_eq!(r.err, "");
let r = unzip(&[]);
assert_eq!(r.parsed, Err(0));
assert!(r.noargs);
assert!(r.out.ends_with(m::UNZIP_USAGE_LINE5));
let r = unzip(&["-l"]);
assert_eq!(r.parsed, Err(10));
assert_eq!(r.out, "");
assert!(r.err.ends_with(m::UNZIP_USAGE_LINE5));
let r = unzip(&["-k", "-d", "x", "a.zip"]);
assert_eq!(r.parsed, Err(10));
assert!(r.err.contains(m::UNZIP_USAGE_LINE2));
let r = unzip(&["-t", "-k", "a.zip"]);
assert_eq!(r.parsed, Err(10));
assert_eq!(r.err, "");
assert!(r.out.contains(m::UNZIP_USAGE_LINE2));
let r = unzip(&["-ct", "a.zip"]);
assert_eq!(r.parsed, Err(10));
assert!(r.out.starts_with(m::INVALID_OPTIONS), "{}", r.out);
let r = unzip(&["-hh"]);
assert_eq!(r.parsed, Err(0));
assert!(r.out.starts_with("\nExtended Help for UnZip\n"));
assert!(r.out.contains("(or %% or #, depending on OS)"));
let r = unzip(&["-lZ", "a.zip"]);
assert_eq!(r.parsed, Err(10));
assert!(r.err.starts_with(m::Z_FIRST), "{}", r.err);
let r = unzip(&["-no", "a.zip"]);
ok(&r);
assert_eq!(r.err, m::IGNORE_O_OPTION);
assert_eq!(r.o.overwrite_all, 0);
}
#[test]
fn v_alone_is_the_version_screen() {
let r = unzip(&["-v"]);
assert_eq!(r.parsed, Err(0));
assert!(r.out.contains(m::APPLE_COMPILED_WITH), "{}", r.out);
let none: String = ["UNZIP", "UNZIPOPT", "ZIPINFO", "ZIPINFOOPT"]
.iter()
.map(|n| format!("{n:>16}: [none]\n"))
.collect();
assert!(r.out.ends_with(&none), "{}", r.out);
assert_eq!(unzip(&["-v", "-k"]).parsed, Err(0));
let r = unzip(&["-vqqqq"]);
assert_eq!((r.parsed, r.out.as_str()), (Err(0), "600\n"));
assert_eq!(unzip(&["-v-v"]).parsed, Err(10));
}
#[test]
fn unzip_and_unzipopt_come_before_the_command_line() {
let r = parse(false, &["-n", "a.zip"], &[("UNZIP", "-o -q")]);
ok(&r);
assert_eq!(r.err, m::IGNORE_O_OPTION);
assert_eq!(r.o.qflag, 1);
let r = parse(false, &["--q", "a.zip"], &[("UNZIP", "-qq")]);
ok(&r);
assert_eq!(r.o.qflag, 1);
let r = parse(false, &["-j-j", "a.zip"], &[("UNZIP", "-j")]);
ok(&r);
assert!(!r.o.jflag);
for unzip_var in [None, Some(""), Some(" \t ")] {
let mut env = vec![("UNZIPOPT", "-j")];
if let Some(v) = unzip_var {
env.push(("UNZIP", v));
}
let r = parse(false, &["a.zip"], &env);
ok(&r);
assert!(r.o.jflag, "{unzip_var:?}");
}
let r = parse(false, &["a.zip"], &[("UNZIP", "-q"), ("UNZIPOPT", "-j")]);
ok(&r);
assert!(!r.o.jflag);
let r = parse(false, &["a.zip"], &[("ZIPINFO", "-k")]);
ok(&r);
let r = parse(false, &[], &[("UNZIP", "-q")]);
assert_eq!(r.parsed, Err(0));
assert!(r.noargs);
let r = parse(false, &["f"], &[("UNZIP", "-d x a.zip")]);
let p = ok(&r);
assert_eq!(p.archive, "a.zip");
assert_eq!(p.exdir.as_deref(), Some(OsStr::new("x")));
assert_eq!(names(&p.include), ["f"]);
let r = parse(false, &["-v"], &[("UNZIP", " -q \"-x\" ")]);
let line = format!("{:>16}: {}\n", "UNZIP", " -q \"-x\" ");
assert!(r.out.contains(&line), "{}", r.out);
}
#[test]
fn zipinfo_reads_its_own_variables_and_options() {
let r = parse(true, &["a.zip"], &[("ZIPINFO", "-1"), ("UNZIP", "-k")]);
ok(&r);
assert_eq!(r.o.lflag, 1);
assert!(r.o.zipinfo_mode);
assert!(!r.extract_flag);
let r = parse(true, &["a.zip"], &[("ZIPINFOOPT", "-m")]);
ok(&r);
assert_eq!(r.o.lflag, 4);
let r = parse(true, &["a.zip"], &[]);
ok(&r);
assert_eq!((r.o.lflag, r.o.hflag, r.o.tflag), (3, true, true));
let r = parse(true, &["a.zip", "x"], &[]);
ok(&r);
assert_eq!((r.o.lflag, r.o.hflag, r.o.tflag), (3, false, false));
let r = parse(true, &["-h", "a.zip", "x"], &[]);
ok(&r);
assert_eq!((r.o.lflag, r.o.hflag, r.o.tflag), (3, true, false));
let r = parse(true, &["-h", "a.zip"], &[]);
ok(&r);
assert_eq!((r.o.lflag, r.o.hflag, r.o.tflag), (0, true, false));
let r = parse(true, &["-1hz", "a.zip"], &[]);
ok(&r);
assert_eq!((r.o.hflag, r.o.tflag, r.o.zflag), (false, false, 0));
let r = parse(true, &["-v", "a.zip"], &[]);
ok(&r);
assert_eq!((r.o.lflag, r.o.hflag, r.o.tflag), (10, true, false));
let r = parse(true, &["-Z", "-l-l", "-zT", "a.zip"], &[]);
ok(&r);
assert_eq!((r.o.lflag, r.o.zflag, r.o.t_flag), (LFLAG, 1, true));
let r = parse(true, &["-l"], &[]);
assert_eq!(r.parsed, Err(0));
assert!(r.out.contains(m::ZIPINFO_USAGE_LINE2));
let r = parse(true, &["-k", "a.zip"], &[]);
assert_eq!(r.parsed, Err(10));
assert!(r.err.contains(m::ZIPINFO_USAGE_LINE3));
assert_eq!(parse(true, &["-o", "a.zip"], &[]).parsed, Err(10));
let r = parse(true, &["a.zip", "f", "-x", "e", "-d", "x"], &[]);
let p = ok(&r);
assert_eq!(
(names(&p.include), names(&p.exclude)),
(vec!["f"], vec!["e"])
);
assert_eq!(r.err, m::NOT_EXTRACTING);
}
#[test]
fn an_argument_longer_than_the_window_allows_is_refused() {
let long = "a".repeat((WSIZE >> 2) - 160 + 1);
let r = unzip(&["-q", &long]);
assert_eq!(r.parsed, Err(10));
assert_eq!(
r.err,
"error: command line parameter #2 exceeds internal size limit\n"
);
let fits = "a".repeat((WSIZE >> 2) - 160);
assert_eq!(ok(&unzip(&[&fits])).archive, fits.as_str());
let r = parse(false, &["a.zip"], &[("UNZIP", &format!("-q {long}"))]);
assert_eq!(r.parsed, Err(10));
assert!(r.err.contains("#2 "), "{}", r.err);
}
#[test]
fn envargs_splits_and_unquotes() {
let split = |value: &str| -> Vec<String> {
let stdout = Arc::new(Mutex::new(Vec::new()));
let host = Host::Sandbox(Sandbox {
dir: std::env::temp_dir(),
stdin: None,
stdout,
budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
reads: Arc::new(Mutex::new(BTreeMap::new())),
});
let value = OsString::from(value);
let lookup = |k: &str| (k == "V").then(|| value.clone());
let mut err = Vec::new();
let uz = Uz::new(host, Flavour::Apple, &lookup, &mut err);
let argv = vec![b"prog".to_vec(), b"cmd".to_vec()];
envargs(&uz, argv, "V", "W")
.into_iter()
.map(|a| String::from_utf8(a).unwrap())
.collect()
};
assert_eq!(split("-a -b\t-c"), ["prog", "-a", "-b", "-c", "cmd"]);
assert_eq!(split(" -a "), ["prog", "-a", "cmd"]);
assert_eq!(split(r#""-d" x"#), ["prog", "-d", "x", "cmd"]);
assert_eq!(split(r#""a b" c"#), ["prog", "a b", "c", "cmd"]);
assert_eq!(split(r#""a\"b""#), ["prog", "a\"b", "cmd"]);
assert_eq!(split(r#""a\\b""#), ["prog", r"a\b", "cmd"]);
assert_eq!(split(r"a\b"), ["prog", r"a\b", "cmd"]);
assert_eq!(split(r#""ab"#), ["prog", "ab", "cmd"]);
assert_eq!(split(r#""ab\"#), ["prog", "ab", "cmd"]);
assert_eq!(split(r#""a"b"#), ["prog", "a", "b", "cmd"]);
assert_eq!(split(r#""""#), ["prog", "", "cmd"]);
assert_eq!(split(""), ["prog", "cmd"]);
}
#[test]
fn funzip_takes_a_password_then_a_file() {
let a = |args: &[&str]| parse_funzip(&args.iter().map(OsString::from).collect::<Vec<_>>());
assert_eq!(a(&[]), FunzipArgs::default());
let got = a(&["-pw", "in.zip", "more"]);
assert_eq!(got.password.as_deref(), Some(&b"pw"[..]));
assert_eq!(got.input.as_deref(), Some(OsStr::new("in.zip")));
let got = a(&["in.zip"]);
assert_eq!(
(got.password, got.input.as_deref()),
(None, Some(OsStr::new("in.zip")))
);
let got = a(&["-"]);
assert_eq!((got.password.as_deref(), got.input), (Some(&b""[..]), None));
}
#[test]
fn charsets_are_taken_as_ubuntu_takes_them() {
let ubuntu = |zipinfo: bool, args: &[&str]| parse_as(Flavour::Ubuntu, zipinfo, args, &[]);
let r = ubuntu(false, &["-I", "", "-l", "a.zip"]);
assert_eq!(ok(&r).archive, "a.zip");
assert_eq!((r.o.iso_cp.as_slice(), r.o.vflag), (&b""[..], 1));
let r = ubuntu(false, &["-OCP866q", "-I", "ISO-8859-5", "-l", "a.zip"]);
assert_eq!(ok(&r).archive, "a.zip");
assert_eq!(r.o.oem_cp, b"CP866q");
assert_eq!(r.o.iso_cp, b"ISO-8859-5");
let long = "x".repeat(30);
let r = ubuntu(true, &["-1O", &long, "a.zip"]);
assert_eq!(ok(&r).archive, "a.zip");
assert_eq!(r.o.oem_cp, vec![b'x'; 25]);
for (args, msg) in [
(
&["-O"][..],
"error: a valid character encoding should follow the -O argument",
),
(
&["-O-l", "a.zip"],
"error: a valid character encoding should follow the -I argument",
),
(&["--I", "x", "a.zip"], "error: encodings can't be negated"),
] {
let r = ubuntu(false, args);
assert_eq!(
(&r.parsed.as_ref().err(), r.err.as_str()),
(&Some(&10), msg),
"{args:?}"
);
}
let r = unzip(&["-I", "x", "a.zip"]);
assert_eq!(r.parsed.as_ref().err(), Some(&10));
}