use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::io::{self, BufWriter, Write};
use std::path::Path;
use std::process::ExitCode;
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use super::CountedRun;
use super::host::{self, Host, SANDBOX_OUTPUT_CAP, Sandbox};
pub(crate) mod cli;
mod extract;
mod fileio;
pub(crate) mod flavour;
mod funzip;
mod list;
mod matching;
mod messages;
mod process;
pub(crate) mod ttyio;
mod unix;
mod zipinfo;
pub use flavour::Flavour;
use messages::{self as m, printf};
const FLAVOR_VAR: &str = "STUFFR_UNZIP_FLAVOR";
pub(crate) const PK_OK: i32 = 0;
pub(crate) const PK_COOL: i32 = 0;
pub(crate) const PK_WARN: i32 = 1;
pub(crate) const PK_ERR: i32 = 2;
pub(crate) const PK_BADERR: i32 = 3;
pub(crate) const PK_NOZIP: i32 = 9;
pub(crate) const PK_PARAM: i32 = 10;
pub(crate) const PK_FIND: i32 = 11;
pub(crate) const PK_EOF: i32 = 51;
pub(crate) const IZ_CTRLC: i32 = 80;
pub(crate) const IZ_DIR: i32 = 76;
pub(crate) const MSG_STDERR: u32 = 0x0001;
pub(crate) const MSG_LNEWLN: u32 = 0x0020;
pub(crate) const MSG_TNEWLN: u32 = 0x0040;
#[doc(hidden)]
pub const EXIT_CODES_APPLE: &[i32] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 50, 51, 80, 81, 82];
#[doc(hidden)]
pub const EXIT_CODES_UBUNTU: &[i32] =
&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 50, 51, 80, 81, 82];
pub fn default_flavour() -> Flavour {
if cfg!(any(
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
)) {
Flavour::Apple
} else {
Flavour::Ubuntu
}
}
pub(crate) fn flavour_from(value: Option<OsString>) -> Result<Flavour, OsString> {
match value {
None => Ok(default_flavour()),
Some(v) if v.is_empty() => Ok(default_flavour()),
Some(v) if v == "apple" => Ok(Flavour::Apple),
Some(v) if v == "ubuntu" => Ok(Flavour::Ubuntu),
Some(v) => Err(v),
}
}
pub(crate) struct Uz<'a> {
pub(crate) host: Host,
pub(crate) flavour: Flavour,
pub(crate) o: cli::Options,
pub(crate) env: &'a dyn Fn(&str) -> Option<OsString>,
pub(crate) out: BufWriter<Box<dyn Write + Send>>,
pub(crate) err: &'a mut dyn Write,
pub(crate) sol: bool,
pub(crate) noargs: bool,
pub(crate) extract_flag: bool,
pub(crate) process_all_files: bool,
pub(crate) native_is_utf8: bool,
pub(crate) unicode_escape_all: bool,
pub(crate) ck: unix::mapname::Checkdir,
pub(crate) qf: Option<Vec<u8>>,
pub(crate) overwrite_mode: i32,
pub(crate) answerbuf: [u8; 10],
pub(crate) stdin: Option<Box<dyn io::BufRead>>,
pub(crate) key: Option<ttyio::Password>,
pub(crate) nopwd: bool,
pub(crate) newzip: bool,
pub(crate) abort: Option<i32>,
pub(crate) pending_slide: Vec<(usize, Vec<u8>, bool)>,
pub(crate) slide: Box<[u8]>,
pub(crate) zin: fileio::ZipIn,
pub(crate) wildzipfn: Vec<u8>,
pub(crate) zipfn: Vec<u8>,
pub(crate) pfnames: Vec<Vec<u8>>,
pub(crate) pxnames: Vec<Vec<u8>>,
pub(crate) no_ecrec: bool,
pub(crate) ecrec: process::Ecrec,
pub(crate) lrec: process::Lrec,
pub(crate) csize: i64,
pub(crate) zip64: bool,
pub(crate) real_ecrec_offset: i64,
pub(crate) expect_ecrec_offset: i64,
pub(crate) sig: [u8; 4],
pub(crate) filename: Vec<u8>,
pub(crate) filename_full: Option<Vec<u8>>,
pub(crate) crec_relative_offset_local_header: u64,
pub(crate) height: i32,
pub(crate) width: i32,
pub(crate) lines: i32,
pub(crate) numlines: u64,
pub(crate) chars: i32,
}
impl<'a> Uz<'a> {
pub(crate) fn new(
host: Host,
flavour: Flavour,
env: &'a dyn Fn(&str) -> Option<OsString>,
err: &'a mut dyn Write,
) -> Uz<'a> {
let out = host.out_writer(None);
Uz {
host,
flavour,
o: cli::Options::new(),
env,
out,
err,
sol: true, noargs: false,
extract_flag: false,
process_all_files: false,
native_is_utf8: false,
unicode_escape_all: false,
ck: unix::mapname::Checkdir::default(),
qf: None,
overwrite_mode: extract::OVERWRT_QUERY,
answerbuf: [0; 10],
stdin: None,
key: None,
nopwd: false,
newzip: false,
abort: None,
pending_slide: Vec::new(),
slide: vec![0u8; stuffr::infozip::WSIZE].into_boxed_slice(),
zin: fileio::ZipIn::new(),
wildzipfn: Vec::new(),
zipfn: Vec::new(),
pfnames: Vec::new(),
pxnames: Vec::new(),
no_ecrec: false,
ecrec: process::Ecrec::default(),
lrec: process::Lrec::default(),
csize: 0,
zip64: false,
real_ecrec_offset: 0,
expect_ecrec_offset: 0,
sig: [0; 4],
filename: vec![0u8; flavour::filnamsiz(flavour)],
filename_full: None,
crec_relative_offset_local_header: 0,
height: 0,
width: 0,
lines: 0,
numlines: 0,
chars: 0,
}
}
pub(crate) fn stdin(&mut self) -> &mut dyn io::BufRead {
if self.stdin.is_none() {
self.stdin = Some(match self.host {
Host::Real => Box::new(io::stdin().lock()),
Host::Sandbox(_) => Box::new(self.host.stdin()),
});
}
self.stdin.as_deref_mut().expect("just opened")
}
pub(crate) fn exit_run(&mut self, code: i32) {
if matches!(self.host, Host::Real) {
let _ = self.out.flush();
let _ = self.err.flush();
std::process::exit(code);
}
self.abort = Some(code);
}
pub(crate) fn info(&mut self, flag: u32, buf: &[u8]) {
let owned: Vec<u8>;
let buf = if flag & MSG_TNEWLN != 0
&& ((buf.is_empty() && !self.sol) || (!buf.is_empty() && buf.last() != Some(&b'\n')))
{
owned = [buf, b"\n"].concat();
&owned[..]
} else {
buf
};
self.put_in_slide(buf);
self.print_raw(flag & !MSG_TNEWLN, buf);
}
fn put_in_slide(&mut self, buf: &[u8]) {
self.put_slide_at(0, buf, true);
}
pub(crate) fn put_slide_at(&mut self, at: usize, buf: &[u8], nul: bool) {
if self.slide.is_empty() {
self.pending_slide.push((at, buf.to_vec(), nul));
return;
}
let end = at + buf.len();
if end <= self.slide.len() {
self.slide[at..end].copy_from_slice(buf);
if nul && end < self.slide.len() {
self.slide[end] = 0;
}
}
}
pub(crate) fn restore_slide(&mut self) {
for (at, b, nul) in std::mem::take(&mut self.pending_slide) {
self.put_slide_at(at, &b, nul);
}
}
pub(crate) fn print_raw(&mut self, flag: u32, buf: &[u8]) {
let _ = self.message(flag, buf, false);
}
pub(crate) fn print_data(&mut self, buf: &[u8]) -> io::Result<()> {
self.message(0, buf, true)
}
fn message(&mut self, flag: u32, buf: &[u8], strict: bool) -> io::Result<()> {
if self.abort.is_some() {
return Ok(());
}
let to_stderr = flag & MSG_STDERR != 0 && !self.o.tflag;
let owned: Vec<u8>;
let buf = if flag & MSG_TNEWLN != 0
&& ((buf.is_empty() && !self.sol) || (!buf.is_empty() && buf.last() != Some(&b'\n')))
{
owned = [buf, b"\n"].concat();
&owned[..]
} else {
buf
};
if self.o.m_flag {
let (height, width) = ttyio::screensize(&self.host);
self.height = height - 2;
self.width = width;
}
let also_stderr = flag & MSG_STDERR != 0
&& self.o.tflag
&& !self.host.stdout_is_tty()
&& self.host.stderr_is_tty();
if flag & MSG_LNEWLN != 0 && !self.sol {
self.emit(to_stderr, b"\n", strict)?;
if self.o.m_flag {
self.chars = 0;
self.numlines += 1;
self.lines += 1;
if self.lines >= self.height {
self.more();
if self.abort.is_some() {
return Ok(());
}
}
}
if also_stderr {
let _ = self.err.write_all(b"\n");
let _ = self.err.flush();
}
self.sol = true;
}
let mut q = 0;
if self.o.m_flag {
for p in 0..buf.len() {
let mut islinefeed = false;
if buf[p] == b'\n' {
islinefeed = true;
} else if buf[p] == b'\r' {
self.chars = 0;
} else {
self.chars += 1;
if self.chars >= self.width {
islinefeed = true;
}
}
if islinefeed && self.o.m_flag {
self.chars = 0;
self.numlines += 1;
self.lines += 1;
if self.lines >= self.height {
self.emit(to_stderr, &buf[q..=p], strict)?;
self.sol = true;
q = p + 1;
self.more();
if self.abort.is_some() {
return Ok(());
}
}
}
}
}
let rest = &buf[q..];
if !rest.is_empty() {
self.emit(to_stderr, rest, strict)?;
if also_stderr {
let _ = self.err.write_all(rest);
let _ = self.err.flush();
}
self.sol = buf.last() == Some(&b'\n');
}
Ok(())
}
fn more(&mut self) {
let mut prompt = Vec::new();
printf(
&mut prompt,
m::MORE_PROMPT,
&[self.numlines.to_string().as_bytes()],
);
fileio::more_pause(self, &prompt, 1);
}
fn emit(&mut self, to_stderr: bool, buf: &[u8], strict: bool) -> io::Result<()> {
if !strict {
self.write(to_stderr, buf);
return Ok(());
}
let w: &mut dyn Write = if to_stderr {
&mut *self.err
} else {
&mut self.out
};
w.write_all(buf)?;
w.flush()
}
pub(crate) fn info_fmt(&mut self, flag: u32, fmt: &str, args: &[&[u8]]) {
let mut buf = Vec::new();
printf(&mut buf, fmt, args);
self.info(flag, &buf);
}
fn write(&mut self, to_stderr: bool, buf: &[u8]) {
if to_stderr {
let _ = self.err.write_all(buf);
let _ = self.err.flush();
} else {
let _ = self.out.write_all(buf);
let _ = self.out.flush();
}
}
}
pub fn run(name: &'static str, argv0: &OsStr, args: Vec<OsString>) -> ExitCode {
host::reset_sigpipe();
let env = |k: &str| std::env::var_os(k);
let mut err = io::stderr();
let flavour = match flavour_from(env(FLAVOR_VAR)) {
Ok(f) => f,
Err(v) => {
printf(
&mut err,
m::FLAVOR_REFUSED,
&[name.as_bytes(), v.as_encoded_bytes()],
);
return ExitCode::from(1);
}
};
let argv0 = if argv0.is_empty() {
name.as_bytes()
} else {
argv0.as_encoded_bytes()
};
let code = main(flavour, name, argv0, &args, &env, Host::Real, &mut err);
ExitCode::from(code as u8)
}
#[doc(hidden)]
pub fn run_in_counted(
flavour: Flavour,
dir: &Path,
name: &'static str,
args: &[OsString],
stdin: &[u8],
env: &dyn Fn(&str) -> Option<OsString>,
) -> CountedRun {
let stdout = Arc::new(Mutex::new(Vec::new()));
let reads = Arc::new(Mutex::new(BTreeMap::new()));
let host = Host::Sandbox(Sandbox {
dir: dir.to_path_buf(),
stdin: Some(stdin.to_vec()),
stdout: stdout.clone(),
budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
reads: reads.clone(),
});
let mut err = Vec::new();
let code = main(flavour, name, name.as_bytes(), args, env, host, &mut err);
let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
let reads = std::mem::take(&mut *reads.lock().unwrap_or_else(|p| p.into_inner()));
(code, out, err, reads)
}
fn main(
flavour: Flavour,
name: &str,
argv0: &[u8],
args: &[OsString],
env: &dyn Fn(&str) -> Option<OsString>,
host: Host,
err: &mut dyn Write,
) -> i32 {
if name == "funzip" {
let mut uz = Uz::new(host, flavour, env, err);
return funzip::funzip(&mut uz, args);
}
let _interrupt =
host::arm_interrupt_exit(&host, host::UNZIP_SIGNALS, IZ_CTRLC, m::HANDLER_TEXT);
let mut uz = Uz::new(host, flavour, env, err);
uz.native_is_utf8 = fileio::init_locale(&uz.host);
if flavour::iconv_charsets(flavour) {
unix::charset::init_conversion_charsets(&mut uz);
}
let code = unzip(&mut uz, argv0, args);
let _ = uz.out.flush();
code
}
fn starts_with_ignore_case(s: &[u8], prefix: &[u8]) -> bool {
s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix)
}
fn unzip(uz: &mut Uz, argv0: &[u8], args: &[OsString]) -> i32 {
let start = argv0
.iter()
.rposition(|&b| b == b'/' || (cfg!(windows) && b == b'\\'))
.map_or(0, |i| i + 1);
let p = &argv0[start..];
let zipinfo = starts_with_ignore_case(p, b"zipinfo")
|| starts_with_ignore_case(p, b"ii")
|| args
.first()
.is_some_and(|a| a.as_encoded_bytes().starts_with(b"-Z"));
let parsed = if zipinfo {
cli::parse_zipinfo(uz, args)
} else {
cli::parse_unzip(uz, args)
};
match parsed {
Ok(p) => process_zipfiles(uz, &p),
Err(code) => code,
}
}
fn process_zipfiles(uz: &mut Uz, parsed: &cli::Parsed) -> i32 {
process::process_zipfiles(uz, parsed)
}
#[cfg(test)]
mod tests {
use super::*;
fn run(f: Flavour, name: &'static str, args: &[&str]) -> (i32, String, String) {
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let dir = std::env::temp_dir();
let (code, out, err, _) = run_in_counted(f, &dir, name, &args, b"", &|_| None);
(
code,
String::from_utf8(out).unwrap(),
String::from_utf8(err).unwrap(),
)
}
#[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 banners_name_stuffr_and_the_build() {
let v = env!("CARGO_PKG_VERSION");
let (code, out, err) = run(Flavour::Apple, "unzip", &[]);
assert_eq!((code, err.as_str()), (0, ""));
let want = format!(
"UnZip 6.00 of 20 April 2009, by stuffr {v} (altered; original by Info-ZIP, \
with modifications by Apple Inc.).\nPlease report bugs to Apple.\n\nUsage: "
);
assert!(out.starts_with(&want), "{out}");
let (_, out, _) = run(Flavour::Ubuntu, "unzip", &["-h"]);
let want = format!(
"UnZip 6.00 of 20 April 2009, by stuffr {v} (altered; Debian's build, original \
by Info-ZIP).\n\nUsage: "
);
assert!(out.starts_with(&want), "{out}");
let (code, out, _) = run(Flavour::Apple, "zipinfo", &[]);
assert_eq!(code, 0);
assert!(
out.starts_with(&format!(
"ZipInfo 3.00 of 20 April 2009, by stuffr {v} (altered; original by Greg \
Roelofs and the Info-ZIP group, with modifications by Apple Inc.).\n\nList"
)),
"{out}"
);
let (_, out, _) = run(Flavour::Ubuntu, "unzip", &["-Z"]);
assert!(
out.starts_with(&format!(
"ZipInfo 3.00 of 20 April 2009, by stuffr {v} (altered; Debian's build, \
original by Greg Roelofs and the Info-ZIP group).\n"
)),
"{out}"
);
let (_, out, _) = run(Flavour::Apple, "unzip", &["-v"]);
assert!(
out.contains(
"\nCompiled with gcc Apple LLVM 21.0.0 (clang-2100.3.31.1) [+internal-os] \
for Unix Mac OS X on Aug 8 2026.\n\n"
),
"{out}"
);
let (_, out, _) = run(Flavour::Ubuntu, "unzip", &["-v"]);
assert!(
out.contains("\nCompiled with gcc 13.2.0 for Unix (Linux ELF).\n\n"),
"{out}"
);
}
#[test]
fn past_the_parser_a_run_never_panics() {
for f in [Flavour::Apple, Flavour::Ubuntu] {
let codes = match f {
Flavour::Apple => EXIT_CODES_APPLE,
Flavour::Ubuntu => EXIT_CODES_UBUNTU,
};
for (name, args) in [
("funzip", &[][..]),
("unzip", &["-l", "a.zip", "x", "-x", "y"]),
("zipinfo", &["a.zip"]),
] {
let (code, _, _) = run(f, name, args);
assert!(codes.contains(&code), "{f:?} {name} {args:?}: exit {code}");
}
}
}
#[test]
fn ubuntu_adds_only_pk_bomb() {
let extra: Vec<i32> = EXIT_CODES_UBUNTU
.iter()
.copied()
.filter(|c| !EXIT_CODES_APPLE.contains(c))
.collect();
assert_eq!(extra, [12]);
assert!(
EXIT_CODES_APPLE
.iter()
.all(|c| EXIT_CODES_UBUNTU.contains(c))
);
}
}