use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::path::Path;
use std::process::ExitCode;
use super::CountedRun;
use super::host::{self, Host, SANDBOX_OUTPUT_CAP, Sandbox};
pub(crate) mod cli;
mod codec;
mod files;
mod messages;
use cli::Command;
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 code = main(argv0, &args, &mut Host::Real, &mut std::io::stderr());
ExitCode::from(code as u8)
}
#[doc(hidden)]
pub fn run_in_counted(
dir: &Path,
name: &'static str,
args: &[OsString],
stdin: &[u8],
) -> CountedRun {
use std::collections::BTreeMap;
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
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, &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], host: &mut Host, err: &mut dyn Write) -> i32 {
let mut out = host.out_writer(None);
let name = cli::file_name(argv0);
let code = match cli::parse(argv0, args, err) {
Err(Command::Help) => {
let _ = err.write_all(&messages::help_text(name));
0
}
Err(Command::Version) => {
let _ = out.write_all(messages::version_line().as_bytes());
0
}
Err(_) => {
let _ = out.write_all(&messages::help_text(name));
1
}
Ok(params) => {
let mut ctx = files::Ctx::new(¶ms, host, err);
let mut is_ok = files::read_dictionary(&mut ctx, params.command);
if is_ok {
let mut bufs = codec::Buffers::new();
is_ok = match params.command {
Command::Compress => codec::compress_files(&mut ctx, &mut bufs),
_ => codec::decompress_files(&mut ctx, &mut bufs),
};
}
if ctx.iterator_error {
is_ok = false;
}
i32::from(!is_ok)
}
};
let _ = out.flush();
code
}
#[cfg(test)]
mod tests {
use super::*;
fn run(name: &'static str, args: &[&str]) -> (i32, String, String) {
let dir = std::env::temp_dir();
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let (code, out, err, _) = run_in_counted(&dir, name, &args, b"");
(
code,
String::from_utf8(out).unwrap(),
String::from_utf8(err).unwrap(),
)
}
#[test]
fn help_goes_to_standard_error_and_usage_errors_to_standard_output() {
let (code, out, err) = run("brcat", &["-h"]);
assert_eq!((code, out.as_str()), (0, ""));
assert!(
err.starts_with("Usage: brcat [OPTION]... [FILE]...\n"),
"{err}"
);
let (code, out, err) = run("brotli", &["-x"]);
assert_eq!((code, err.as_str()), (1, "invalid argument -x\n"));
assert!(out.starts_with("Usage: brotli ["), "{out}");
}
#[test]
fn version_goes_to_standard_output() {
assert_eq!(
run("unbrotli", &["-V"]),
(0, "brotli 1.2.1\n".into(), String::new())
);
}
struct Scratch(std::path::PathBuf);
impl Scratch {
fn new() -> Self {
use std::sync::atomic::{AtomicUsize, Ordering};
static N: AtomicUsize = AtomicUsize::new(0);
let d = std::env::temp_dir().join(format!(
"stuffr-brotli-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
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_at(dir: &Path, args: &[&str], stdin: &[u8]) -> (i32, Vec<u8>, String) {
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let (code, out, err, _) = run_in_counted(dir, "brotli", &args, stdin);
(code, out, String::from_utf8(err).unwrap())
}
const BR: &[u8] = b"\x21\x14\x00\x04hello\n\x03";
#[test]
fn compresses_a_file_and_standard_input_in_the_sandbox() {
let s = Scratch::new();
std::fs::write(s.0.join("a"), b"hello\n").unwrap();
assert_eq!(run_at(&s.0, &["a"], b""), (0, Vec::new(), String::new()));
let written = std::fs::read(s.0.join("a.br")).unwrap();
let (code, out, err) = run_at(&s.0, &["-c"], b"hello\n");
assert_eq!((code, err.as_str()), (0, ""));
if cfg!(feature = "brotli-c") {
assert_eq!(written, BR);
assert_ne!(out, BR);
}
assert_eq!(run_at(&s.0, &["-jf", "a"], b"").0, 0);
assert!(!s.0.join("a").exists() && s.0.join("a.br").exists());
}
#[test]
fn an_output_that_is_the_input_is_refused() {
let s = Scratch::new();
std::fs::write(s.0.join("a"), b"hello\n").unwrap();
for args in [&["-f", "-o", "a", "a"][..], &["-df", "-o", "a", "a"]] {
let (code, out, err) = run_at(&s.0, args, b"");
assert_eq!(
(code, out.as_slice(), err.as_str()),
(
1,
&b""[..],
"input file [a] is the same as the output file\n"
),
"{args:?}"
);
assert_eq!(std::fs::read(s.0.join("a")).unwrap(), b"hello\n");
}
let (code, _, err) = run_at(&s.0, &["-o", "a", "a"], b"");
assert_eq!(
(code, err.as_str()),
(1, "failed to open output file [a]: File exists\n")
);
}
#[test]
fn names_map_as_next_file_maps_them() {
let s = Scratch::new();
std::fs::write(s.0.join("x"), BR).unwrap();
let (code, _, err) = run_at(&s.0, &["-d", "x"], b"");
assert_eq!(
(code, err.as_str()),
(1, "empty output file name for [x] input file\n")
);
std::fs::write(s.0.join("abcd"), BR).unwrap();
let (code, _, err) = run_at(&s.0, &["-d", "abcd"], b"");
assert_eq!(
(code, err.as_str()),
(1, "input file [abcd] suffix mismatch\n")
);
let (code, _, err) = run_at(&s.0, &["-S", ".x", "nosuch"], b"");
assert_eq!(
(code, err.as_str()),
(
1,
"failed to open input file [nosuch]: No such file or directory\n"
)
);
}
#[test]
fn verbose_and_squash_lines() {
let s = Scratch::new();
std::fs::write(s.0.join("a"), b"hello\n").unwrap();
let (code, _, err) = run_at(&s.0, &["-v", "-s", "a"], b"");
assert_eq!(code, 0);
let size = if cfg!(feature = "brotli-c") {
"11 B"
} else {
""
};
assert!(err.starts_with("Compressed [a]: 6 B -> "), "{err}");
assert!(err.contains(size));
assert!(
err.ends_with(" in 0.00 sec\nOutput is larger than input\n"),
"{err}"
);
assert!(!s.0.join("a.br").exists());
}
fn run_capped(dir: &Path, args: &[&str], budget: u64) -> (i32, String) {
use std::collections::BTreeMap;
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
let mut host = Host::Sandbox(Sandbox {
dir: dir.to_path_buf(),
stdin: Some(Vec::new()),
stdout: Arc::new(Mutex::new(Vec::new())),
budget: Arc::new(AtomicU64::new(budget)),
reads: Arc::new(Mutex::new(BTreeMap::new())),
});
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let mut err = Vec::new();
let code = main(b"brotli", &args, &mut host, &mut err);
(code, String::from_utf8(err).unwrap())
}
#[test]
fn rm_keeps_the_input_when_the_close_fails() {
let s = Scratch::new();
std::fs::write(s.0.join("a"), b"hello\n").unwrap();
let (code, err) = run_capped(&s.0, &["-j", "a"], 5);
assert_eq!(code, 1);
assert!(err.starts_with("fclose failed [a.br]: "), "{err}");
assert_eq!(std::fs::read(s.0.join("a")).unwrap(), b"hello\n");
std::fs::write(s.0.join("b.br"), BR).unwrap();
let (code, _) = run_capped(&s.0, &["-dj", "b.br"], 3);
assert_eq!(code, 1);
assert_eq!(std::fs::read(s.0.join("b.br")).unwrap(), BR);
assert_eq!(run_capped(&s.0, &["-jf", "a"], 1 << 20).0, 0);
assert!(!s.0.join("a").exists());
}
#[test]
fn a_later_input_operand_is_never_overwritten() {
let s = Scratch::new();
std::fs::write(s.0.join("a"), b"hello\n").unwrap();
std::fs::write(s.0.join("a.br"), b"keep").unwrap();
let (code, _, err) = run_at(&s.0, &["-f", "a", "a.br"], b"");
assert_eq!(
(code, err.as_str()),
(
1,
"input file [a.br] would be overwritten by the output of [a]\n"
)
);
assert_eq!(std::fs::read(s.0.join("a.br")).unwrap(), b"keep");
let (code, _, _) = run_at(&s.0, &["-f", "a", "-"], b"");
assert_eq!(code, 0);
}
#[test]
fn decodes_tests_and_checks_comments() {
let s = Scratch::new();
let mut two = BR.to_vec();
two.extend_from_slice(BR);
std::fs::write(s.0.join("two.br"), &two).unwrap();
assert_eq!(
run_at(&s.0, &["-vdcK", "two.br"], b""),
(
0,
b"hello\nhello\n".to_vec(),
"extra input\nDecompressed [two.br]: 22 B -> 12 B in 0.00 sec\n".into()
)
);
let (code, out, err) = run_at(&s.0, &["-vdc", "two.br"], b"");
assert_eq!(
(code, out.as_slice(), err.as_str()),
(
1,
&b"hello\n"[..],
"corrupt input [two.br]\nreason: extra input\n"
)
);
let (code, _, err) = run_at(&s.0, &["-vt"], &BR[..8]);
assert_eq!(
(code, err.as_str()),
(1, "corrupt input [con]\nreason: truncated input\n")
);
let (code, _, err) = run_at(&s.0, &["-vt"], b"\x1c");
assert_eq!(
(code, err.as_str()),
(1, "corrupt input [con]\nreason: FORMAT_RESERVED (-2)\n")
);
let foo = b"\x21\x4b\x00foo\x28\x00\x08hello\n\x03";
assert_eq!(run_at(&s.0, &["-t", "-C", "Zm9v"], foo).0, 0);
let (code, _, err) = run_at(&s.0, &["-vt", "-C", "Zm9x"], foo);
assert_eq!(
(code, err.as_str()),
(1, "corrupt input [con]\nreason: comment mismatch\n")
);
let (code, _, err) = run_at(&s.0, &["-t", "-C", "Zm9v"], BR);
assert_eq!((code, err.as_str()), (0, "corrupt input [con]\n"));
}
#[test]
fn test_mode_is_charged_in_the_sandbox() {
let s = Scratch::new();
let mut enc = stuffr::brotli_opts::Encoder::new(
&stuffr::brotli_opts::EncoderParams {
quality: 5,
lgwin: 22,
large_window: false,
size_hint: None,
},
None,
)
.unwrap();
let zeros = vec![0u8; 2 << 20];
let mut input = &zeros[..];
let mut buf = vec![0u8; 1 << 16];
let mut outv = Vec::new();
while !enc.is_finished() {
let mut out = &mut buf[..];
let room = out.len();
assert!(enc.compress_stream(
stuffr::brotli_opts::Operation::Finish,
&mut input,
&mut out
));
let n = room - out.len();
outv.extend_from_slice(&buf[..n]);
}
std::fs::write(s.0.join("z.br"), &outv).unwrap();
assert_eq!(run_capped(&s.0, &["-t", "z.br"], 4 << 20).0, 0);
let (code, err) = run_capped(&s.0, &["-t", "z.br"], 1 << 20);
assert_eq!(code, 1);
assert!(
err.starts_with("failed to write output [z.br.br]: "),
"{err}"
);
assert!(!s.0.join("z.br.br").exists());
}
}