use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::io::{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};
pub(crate) mod cli;
mod decode;
mod display;
mod io;
mod list;
mod messages;
use cli::{Command, Op, Stop};
use messages::{self as m, printf};
pub fn run(name: &'static str, argv0: &OsStr, args: Vec<OsString>) -> ExitCode {
host::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 code = main(argv0, &args, &env, &mut Host::Real, &mut std::io::stderr());
ExitCode::from(code as u8)
}
#[doc(hidden)]
pub const EXIT_CODES: [i32; 19] = [
0, 1, 26, 27, 34, 38, 40, 42, 43, 44, 50, 61, 63, 64, 69, 70, 71, 72, 74,
];
#[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.as_bytes(), args, env, &mut host, &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()));
(code, out, err, reads)
}
fn main(
argv0: &[u8],
args: &[OsString],
env: &dyn Fn(&str) -> Option<OsString>,
host: &mut Host,
err: &mut dyn Write,
) -> i32 {
let program = cli::program_name(argv0);
let mut out = host.out_writer(None);
let stop = match cli::parse(program, env, args, host::cores(host), &mut out, err) {
Ok(cmd) => execute(&cmd, host, &mut out, err),
Err(stop) => stop,
};
let _ = out.flush();
drop(out);
if stop.pause {
printf(err, m::PRESS_ENTER, &[]);
let mut byte = [0u8; 1];
let _ = host.stdin().read(&mut byte);
}
stop.code
}
fn execute(cmd: &Command, host: &mut Host, out: &mut dyn Write, err: &mut dyn Write) -> Stop {
announce(cmd, err);
let plan = match cli::plan(cmd, host.stdin_is_tty(), host.stdout_is_tty()) {
Ok(plan) => plan,
Err(refusal) => {
return Stop {
code: cli::refuse(err, cmd, refusal),
pause: false,
};
}
};
let ended = match plan.op {
Op::Compress => io::compress_files(cmd, &plan, host, out, err),
Op::Decompress | Op::Test => io::decompress_files(cmd, &plan, host, out, err),
Op::List => list::list_files(cmd, host, out, err),
};
Stop {
code: ended.code,
pause: ended.pause,
}
}
fn announce(cmd: &Command, err: &mut dyn Write) {
if cmd.display_level >= 3 {
cli::print_banner(err);
}
if cmd.display_level >= 4 {
#[cfg(target_os = "linux")]
printf(err, m::POSIX_C_SOURCE, &[]);
let v = posix_version();
printf(err, m::POSIX_VERSION, &[v.as_bytes(), v.as_bytes()]);
}
}
fn posix_version() -> String {
#[cfg(unix)]
{
let v = unsafe { libc::sysconf(libc::_SC_VERSION) };
if v > 0 {
return v.to_string();
}
}
"200112".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn run(name: &'static str, args: &[&str], stdin: &[u8]) -> (i32, String, String) {
let dir = std::env::temp_dir().join(format!("stuffr-lz4-mod-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a"), b"hello\n").unwrap();
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let (code, out, err, _) = run_in_counted(&dir, name, &args, stdin, &|_| None);
let _ = std::fs::remove_dir_all(&dir);
(
code,
String::from_utf8(out).unwrap(),
String::from_utf8(err).unwrap(),
)
}
#[test]
fn a_sandboxed_run_parses_and_plans() {
let banner = format!(
"*** lz4 v1.10.0 64-bit multithread (stuffr {}), by Yann Collet ***\n",
env!("CARGO_PKG_VERSION")
);
assert_eq!(run("lz4", &["-V"], b""), (0, banner.clone(), String::new()));
assert_eq!(
run("lz4", &["-p", "-V"], b"\n"),
(0, banner, "Press enter to continue...\n".into())
);
let (code, out, err) = run("unlz4", &["a"], b"");
assert_eq!((code, out.as_str()), (1, ""));
assert!(
err.starts_with("Cannot determine an output filename \n"),
"{err}"
);
let (code, _, err) = run("lz4", &["--bogus"], b"");
assert_eq!(code, 1);
assert!(err.starts_with("Incorrect parameters\n"), "{err}");
}
struct Scratch(std::path::PathBuf);
impl Scratch {
fn new(files: &[(&str, &[u8])]) -> 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 dir =
std::env::temp_dir().join(format!("stuffr-lz4-io-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
for (name, data) in files {
std::fs::write(dir.join(name), data).unwrap();
}
Scratch(dir)
}
fn run(&self, name: &'static str, args: &[&str], stdin: &[u8]) -> CountedRun {
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
run_in_counted(&self.0, name, &args, stdin, &|_| None)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn a_sandboxed_run_compresses_decodes_and_tests() {
let plain = b"hello hello hello hello\n".repeat(50);
let d = Scratch::new(&[("a", &plain)]);
let (code, out, err, reads) = d.run("lz4", &["-q", "a"], b"");
assert_eq!((code, out.len(), err.len()), (0, 0, 0));
let read = reads.get(&d.0.join("a")).copied().unwrap_or_default();
assert_eq!((read.opens, read.bytes), (1, plain.len() as u64));
let (code, out, err, _) = d.run("lz4cat", &["a.lz4"], b"");
assert_eq!((code, out, err.len()), (0, plain.clone(), 0));
let (code, _, err, _) = d.run("lz4", &["-t", "a.lz4"], b"");
assert_eq!(code, 0);
let err = String::from_utf8(err).unwrap();
let summary = format!("a.lz4{}: decoded 1200 bytes \n", " ".repeat(26));
assert!(err.ends_with(&summary), "{err:?}");
}
#[test]
fn the_overwrite_question_reads_standard_input() {
let d = Scratch::new(&[("e", b"old")]);
let (code, _, err, _) = d.run("lz4", &["-", "e"], b"y\nhello");
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
let (code, out, _, _) = d.run("lz4", &["-dc", "e"], b"");
assert_eq!((code, out.as_slice()), (0, &b"hello"[..]));
let (code, _, err, _) = d.run("lz4", &["-", "e"], b"n\nhello");
assert_eq!(code, 1);
let refused = format!("?{}not overwritten \n", " ".repeat(5));
assert!(String::from_utf8(err).unwrap().ends_with(&refused));
}
#[test]
fn sparse_standard_output_is_a_pipe_in_the_sandbox() {
let d = Scratch::new(&[("a", b"AAAAAAAA"), ("z", &[0u8; 100])]);
assert_eq!(d.run("lz4", &["-q", "-m", "a", "z"], b"").0, 0);
let (code, out, err, _) = d.run("lz4", &["--sparse", "-dc", "z.lz4"], b"");
assert_eq!((code, out.len()), (69, 0));
assert_eq!(err, b"Error 69 : Final skip error (sparse file)\n \n");
let (code, out, err, _) = d.run("lz4", &["--sparse", "-dc", "a.lz4"], b"");
assert_eq!((code, out.len()), (72, 0));
let err = String::from_utf8(err).unwrap();
assert!(err.starts_with("Error 72 : Sparse skip error("), "{err:?}");
assert!(err.ends_with(" ; try --no-sparse \n"), "{err:?}");
let (code, out, _, _) = d.run("lz4", &["-dc", "a.lz4"], b"");
assert_eq!((code, out.as_slice()), (0, &b"AAAAAAAA"[..]));
}
#[test]
fn an_output_that_is_the_input_is_refused() {
let d = Scratch::new(&[("a", b"keep me")]);
let (code, _, err, _) = d.run("lz4", &["-f", "a", "a"], b"");
assert_eq!(code, 1);
assert_eq!(err, b"a is the input file; not overwritten (stuffr) \n");
assert_eq!(std::fs::read(d.0.join("a")).unwrap(), b"keep me");
}
}