use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::io::{self, BufRead, BufReader, Read, 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, reset_sigpipe, strerror};
pub(crate) mod args;
mod coder;
mod file_io;
pub(crate) mod hardware;
mod index;
mod list;
pub(crate) mod message;
mod messages;
pub(crate) mod options;
pub(crate) mod suffix;
use args::{Args, OpMode};
use hardware::Hardware;
use list::ListTotals;
use message::{Messages, Status, Verbosity, mask_nonprint};
pub fn run(name: &'static str, argv0: &OsStr, args: Vec<OsString>) -> ExitCode {
reset_sigpipe();
let argv0 = if argv0.is_empty() {
name.as_bytes()
} else {
argv0.as_encoded_bytes()
};
let env = |k: &str| std::env::var_os(k);
let mut err = io::stderr();
let code = main(
name,
argv0,
&args,
&env,
&mut Host::Real,
Hardware::detect(),
&mut err,
);
ExitCode::from(code)
}
#[doc(hidden)]
pub fn run_in_counted(
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 mut 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(
name,
name.as_bytes(),
args,
env,
&mut host,
Hardware::SANDBOX,
&mut err,
);
drop(host);
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()));
(i32::from(code), out, err, reads)
}
fn main(
name: &str,
argv0: &[u8],
args: &[OsString],
env: &dyn Fn(&str) -> Option<OsString>,
host: &mut Host,
hw: Hardware,
err: &mut dyn Write,
) -> u8 {
host::install_progress_signals(host);
let mut out = host.out_writer(None);
let (mut a, mut msg) = match args::parse(name, argv0, env, args, host, hw, &mut out, err) {
Ok(parsed) => parsed,
Err(code) => return code,
};
msg.automatic = host.stderr_is_tty();
msg.robot = a.robot;
msg.list = a.op == OpMode::List;
if matches!(host, Host::Sandbox(_)) {
msg.clock = message::frozen_clock;
}
if a.op != OpMode::List && a.robot {
return msg.fatal(err, messages::ROBOT_CODING, &[]);
}
msg.files_total = if a.files_from.is_some() {
0
} else {
a.files.len() as u32
};
if a.op == OpMode::Compress
&& (a.to_stdout || a.files == [b"-".to_vec()])
&& is_tty_stdout(host, &mut msg, err)
{
msg.try_help(err);
return Status::Error as u8;
}
if a.op != OpMode::List {
host::install_stop_handler(host);
}
let names = std::mem::take(&mut a.files);
let mut totals = ListTotals::default();
for n in &names {
let mut file = Some(n.as_slice());
if n == b"-" {
if a.op == OpMode::Compress {
if is_tty_stdout(host, &mut msg, err) {
continue;
}
} else if host.stdin_is_tty() {
msg.error(err, messages::TTY_STDIN, &[]);
continue;
}
if a.files_from.as_ref().is_some_and(|f| f.name.is_none()) {
msg.error(err, messages::STDIN_BOTH, &[]);
continue;
}
file = None;
}
if let Err(code) = run_one(&a, file, host, &mut msg, &mut out, err, &mut totals) {
return code;
}
}
if let Some(from) = a.files_from.take() {
let delim = from.delim;
let display = from.display_name().to_vec();
let source: Box<dyn Read + '_> = match from.file {
Some(f) => Box::new(f),
None => Box::new(host.stdin()),
};
let mut reader = BufReader::new(source);
while let Some(name) = read_name(&mut reader, delim, &display, &mut msg, err) {
if let Err(code) = run_one(&a, Some(&name), host, &mut msg, &mut out, err, &mut totals)
{
return code;
}
}
}
if a.op == OpMode::List {
list::list_totals(&a, &msg, &totals, &mut out);
}
let status = msg.final_status();
message::exit(
status,
&msg.progname,
&mut out,
err,
msg.verbosity != Verbosity::Silent,
)
}
fn is_tty_stdout(host: &Host, msg: &mut Messages, err: &mut dyn Write) -> bool {
let tty = host.stdout_is_tty();
if tty {
msg.error(err, messages::TTY_STDOUT, &[]);
}
tty
}
fn run_one(
a: &Args,
name: Option<&[u8]>,
host: &mut Host,
msg: &mut Messages,
out: &mut dyn Write,
err: &mut dyn Write,
totals: &mut ListTotals,
) -> Result<(), u8> {
if a.op == OpMode::List {
return list::list_file(a, name, host, msg, out, err, totals);
}
coder::run(a, name, host, msg, err);
Ok(())
}
fn read_name(
r: &mut dyn BufRead,
delim: u8,
display: &[u8],
msg: &mut Messages,
err: &mut dyn Write,
) -> Option<Vec<u8>> {
let mut name = Vec::new();
loop {
let mut byte = [0u8; 1];
match r.read(&mut byte) {
Ok(0) => {
if !name.is_empty() {
msg.error(
err,
messages::NAMES_UNEXPECTED_END,
&[&mask_nonprint(display)],
);
}
return None;
}
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => {
msg.error(
err,
messages::READ_NAMES_FAILED,
&[&mask_nonprint(display), &strerror(&e)],
);
return None;
}
}
let c = byte[0];
if c == delim {
if name.is_empty() {
continue;
}
return Some(name);
}
if c == 0 {
msg.error(err, messages::NAMES_NUL, &[&mask_nonprint(display)]);
return None;
}
name.push(c);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn names(data: &[u8], delim: u8) -> (Vec<String>, String, Status) {
let mut msg = Messages::new(b"xz");
let mut err = Vec::new();
let mut r = data;
let mut got = Vec::new();
while let Some(n) = read_name(&mut r, delim, b"(stdin)", &mut msg, &mut err) {
got.push(String::from_utf8(n).unwrap());
}
(got, String::from_utf8(err).unwrap(), msg.status)
}
#[test]
fn read_name_splits_and_skips_empty_names() {
assert_eq!(
names(b"a\n\nb\n", b'\n'),
(vec!["a".into(), "b".into()], String::new(), Status::Success)
);
assert_eq!(names(b"a\0b\0", 0).0, ["a", "b"]);
let (got, err, st) = names(b"a\nb", b'\n');
assert_eq!(got, ["a"]);
assert_eq!(
(err.as_str(), st),
(
"xz: (stdin): Unexpected end of input when reading filenames\n",
Status::Error
)
);
let (got, err, _) = names(b"a\0b\n", b'\n');
assert!(got.is_empty());
assert!(err.contains("Null character found"), "{err}");
}
#[test]
fn run_in_counted_codes_in_the_sandbox() {
let dir = std::env::temp_dir().join(format!("stuffr-xz-sandbox-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let plain = b"sandboxed\n".repeat(100);
let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
let (code, packed, err, _) = run_in_counted(&dir, "xz", &args(&["-T1"]), &plain, &|_| None);
assert_eq!((code, err.as_slice()), (0, &b""[..]));
assert!(packed.starts_with(&[0xFD, b'7', b'z', b'X', b'Z', 0]));
let (code, out, err, _) = run_in_counted(&dir, "xzcat", &[], &packed, &|_| None);
assert_eq!((code, out, err), (0, plain.clone(), Vec::new()));
std::fs::write(dir.join("a"), &plain).unwrap();
let (code, _, err, reads) = run_in_counted(&dir, "xz", &args(&["a"]), b"", &|_| None);
assert_eq!((code, err), (0, Vec::new()));
assert!(!dir.join("a").exists());
assert_eq!(
reads.get(&dir.join("a")).map(|r| r.bytes),
Some(plain.len() as u64)
);
let (code, out, err, _) =
run_in_counted(&dir, "xz", &args(&["-t", "a.xz"]), b"", &|_| None);
assert_eq!((code, out, err), (0, Vec::new(), Vec::new()));
let (code, _, err, _) = run_in_counted(&dir, "unxz", &args(&["a.xz"]), b"", &|_| None);
assert_eq!((code, err), (0, Vec::new()));
assert_eq!(std::fs::read(dir.join("a")).unwrap(), plain);
let zeros = vec![0u8; 9 << 20];
let (_, bomb, _, _) = run_in_counted(&dir, "xz", &args(&["-T1"]), &zeros, &|_| None);
let (code, _, err, _) = run_in_counted(&dir, "xzcat", &[], &bomb, &|_| None);
assert_eq!(code, 1);
assert!(
String::from_utf8_lossy(&err).starts_with("xzcat: (stdout): Write error: "),
"{}",
String::from_utf8_lossy(&err)
);
let (code, _, err, _) = run_in_counted(&dir, "xz", &args(&["-t"]), &bomb, &|_| None);
assert_eq!(code, 1);
assert!(
String::from_utf8_lossy(&err).starts_with("xz: (stdout): Write error: "),
"{}",
String::from_utf8_lossy(&err)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn run_in_counted_verbose_is_deterministic() {
let dir = std::env::temp_dir();
let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
let plain = vec![b'x'; 300_000];
let (code, packed, err, _) =
run_in_counted(&dir, "xz", &args(&["-v", "-T1", "-0"]), &plain, &|_| None);
assert_eq!(code, 0);
let ratio = packed.len() as f64 / plain.len() as f64;
let want = format!("(stdin): {} B / 293.0 KiB = {ratio:.3}\n", packed.len());
assert_eq!(String::from_utf8(err).unwrap(), want);
let (code, _, err, _) = run_in_counted(&dir, "xz", &args(&["-tv"]), &packed, &|_| None);
assert_eq!((code, String::from_utf8(err).unwrap()), (0, want));
}
#[test]
fn run_in_counted_lists_in_the_sandbox() {
let dir = std::env::temp_dir().join(format!("stuffr-xz-list-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
let (_, packed, _, _) = run_in_counted(&dir, "xz", &args(&["-T1"]), b"listed\n", &|_| None);
std::fs::write(dir.join("a.xz"), &packed).unwrap();
let (code, out, err, reads) = run_in_counted(
&dir,
"xz",
&args(&["--robot", "-lvv", "a.xz"]),
b"",
&|_| None,
);
assert_eq!((code, String::from_utf8(err).unwrap()), (0, String::new()));
let out = String::from_utf8(out).unwrap();
assert!(out.starts_with("name\ta.xz\nfile\t1\t1\t"), "{out}");
let needed = if cfg!(any(
target_arch = "x86_64",
all(target_arch = "x86", target_feature = "sse2")
)) {
8_454_808
} else {
8_454_776
};
assert!(
out.ends_with(&format!("\tCRC64\t0\t1\t{needed}\tno\t50000002\n")),
"{out}"
);
let read = reads.get(&dir.join("a.xz")).map_or(0, |r| r.bytes);
assert!(read > 0 && read <= 4 * packed.len() as u64, "read {read}");
let (code, _, err, _) = run_in_counted(&dir, "xz", &args(&["-l"]), &packed, &|_| None);
assert_eq!(code, 1);
assert_eq!(
String::from_utf8(err).unwrap(),
"xz: --list does not support reading from standard input\n"
);
let short = short_block(4, 5);
assert_eq!(short.len(), 44);
for (i, junk) in [
&b""[..],
b"\xfd7zXZ\0\0\x01",
&[0u8; 64],
&packed[..packed.len() - 4],
&short,
]
.iter()
.enumerate()
{
let name = format!("j{i}.xz");
std::fs::write(dir.join(&name), junk).unwrap();
let (code, _, _, _) =
run_in_counted(&dir, "xz", &args(&["-lvv", &name]), b"", &|_| None);
assert_eq!(code, 1, "{name}");
}
let _ = std::fs::remove_dir_all(&dir);
}
fn crc32(data: &[u8]) -> [u8; 4] {
let mut c = flate2::Crc::new();
c.update(data);
c.sum().to_le_bytes()
}
fn short_block(check: u8, unpadded: u64) -> Vec<u8> {
let mut s = vec![0xFD, b'7', b'z', b'X', b'Z', 0, 0, check];
s.extend(crc32(&[0, check]));
s.extend(std::iter::repeat_n(0x55, ((unpadded + 3) & !3) as usize));
let mut index = vec![0, 1];
let mut v = unpadded;
while v >= 0x80 {
index.push(v as u8 | 0x80);
v >>= 7;
}
index.extend([v as u8, 0xE8, 0x07]);
while !index.len().is_multiple_of(4) {
index.push(0);
}
let crc = crc32(&index);
index.extend(crc);
s.extend(&index);
let mut tail = ((index.len() / 4 - 1) as u32).to_le_bytes().to_vec();
tail.extend([0, check]);
s.extend(crc32(&tail));
s.extend(tail);
s.extend(b"YZ");
s
}
#[test]
fn list_vv_on_hostile_input_never_panics() {
let dir = std::env::temp_dir().join(format!("stuffr-xz-listvv-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
let plain = b"hostile listing\n".repeat(40);
let (_, packed, _, _) = run_in_counted(
&dir,
"xz",
&args(&["-T1", "-C", "sha256", "--block-size=100"]),
&plain,
&|_| None,
);
let mut inputs = Vec::new();
for check in 0..16u8 {
for unpadded in 5..=72 {
inputs.push(short_block(check, unpadded));
}
}
for i in 0..packed.len() {
for mask in [0x01, 0x80, 0xFF] {
let mut b = packed.clone();
b[i] ^= mask;
inputs.push(b);
}
}
for (i, data) in inputs.iter().enumerate() {
let name = format!("h{i}.xz");
std::fs::write(dir.join(&name), data).unwrap();
for form in [&["-lvv"][..], &["--robot", "-lvv"]] {
let mut a = form.to_vec();
a.push(&name);
let (code, _, _, _) = run_in_counted(&dir, "xz", &args(&a), b"", &|_| None);
assert!(matches!(code, 0 | 1), "{name} {form:?}: exit {code}");
}
std::fs::remove_file(dir.join(&name)).unwrap();
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn files_from_a_file_are_counted() {
let dir = std::env::temp_dir().join(format!("stuffr-xz-files-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("list"), b"a\n").unwrap();
std::fs::write(dir.join("a"), b"listed by name\n").unwrap();
let args = ["-k".into(), "--files=list".into()];
let (code, _, err, reads) = run_in_counted(&dir, "xz", &args, b"", &|_| None);
assert_eq!((code, String::from_utf8(err).unwrap()), (0, String::new()));
let got = |n: &str| reads.get(&dir.join(n)).copied().unwrap_or_default();
assert_eq!(
(got("list").opens, got("list").bytes, got("list").len),
(1, 2, 2)
);
assert_eq!((got("a").opens, got("a").bytes), (1, 15));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn run_in_counted_parses_in_the_sandbox() {
let dir = std::env::temp_dir();
let (code, out, err, reads) = run_in_counted(&dir, "xz", &["-h".into()], b"", &|_| None);
assert_eq!((code, err.len(), reads.len()), (0, 0, 0));
assert!(out.starts_with(b"Usage: xz [OPTION]... [FILE]...\n"));
let (code, out, _, _) = run_in_counted(
&dir,
"xz",
&["--robot".into(), "--info-memory".into()],
b"",
&|_| None,
);
assert_eq!(code, 0);
assert_eq!(
String::from_utf8(out).unwrap(),
"4294967296\t0\t0\t268435456\t1073741824\t2\n"
);
let (code, _, err, _) = run_in_counted(&dir, "xz", &["--robot".into()], b"", &|_| None);
assert_eq!(code, 1);
assert_eq!(
String::from_utf8(err).unwrap(),
"xz: Compression and decompression with --robot are not supported yet.\n"
);
}
}