1use std::ffi::{OsStr, OsString};
87use std::io::Write;
88use std::path::Path;
89use std::process::ExitCode;
90
91use super::CountedRun;
92use super::host::{self, Host, SANDBOX_OUTPUT_CAP, Sandbox};
93
94pub(crate) mod cli;
95mod codec;
96mod files;
97mod messages;
98
99use cli::Command;
100
101pub fn run(name: &'static str, argv0: &OsStr, args: Vec<OsString>) -> ExitCode {
105 host::reset_sigpipe();
106 let argv0 = if argv0.is_empty() {
107 name.as_bytes()
108 } else {
109 argv0.as_encoded_bytes()
110 };
111 let code = main(argv0, &args, &mut Host::Real, &mut std::io::stderr());
112 ExitCode::from(code as u8)
113}
114
115#[doc(hidden)]
119pub fn run_in_counted(
120 dir: &Path,
121 name: &'static str,
122 args: &[OsString],
123 stdin: &[u8],
124) -> CountedRun {
125 use std::collections::BTreeMap;
126 use std::sync::atomic::AtomicU64;
127 use std::sync::{Arc, Mutex};
128
129 let stdout = Arc::new(Mutex::new(Vec::new()));
130 let reads = Arc::new(Mutex::new(BTreeMap::new()));
131 let mut host = Host::Sandbox(Sandbox {
132 dir: dir.to_path_buf(),
133 stdin: Some(stdin.to_vec()),
134 stdout: stdout.clone(),
135 budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
136 reads: reads.clone(),
137 });
138 let mut err = Vec::new();
139 let code = main(name.as_bytes(), args, &mut host, &mut err);
140 drop(host);
141 let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
142 let reads = std::mem::take(&mut *reads.lock().unwrap_or_else(|p| p.into_inner()));
143 (code, out, err, reads)
144}
145
146fn main(argv0: &[u8], args: &[OsString], host: &mut Host, err: &mut dyn Write) -> i32 {
148 let mut out = host.out_writer(None);
149 let name = cli::file_name(argv0);
150 let code = match cli::parse(argv0, args, err) {
151 Err(Command::Help) => {
152 let _ = err.write_all(&messages::help_text(name));
153 0
154 }
155 Err(Command::Version) => {
156 let _ = out.write_all(messages::version_line().as_bytes());
157 0
158 }
159 Err(_) => {
162 let _ = out.write_all(&messages::help_text(name));
163 1
164 }
165 Ok(params) => {
166 let mut ctx = files::Ctx::new(¶ms, host, err);
168 let mut is_ok = files::read_dictionary(&mut ctx, params.command);
169 if is_ok {
170 let mut bufs = codec::Buffers::new();
171 is_ok = match params.command {
172 Command::Compress => codec::compress_files(&mut ctx, &mut bufs),
173 _ => codec::decompress_files(&mut ctx, &mut bufs),
174 };
175 }
176 if ctx.iterator_error {
177 is_ok = false;
178 }
179 i32::from(!is_ok)
180 }
181 };
182 let _ = out.flush();
183 code
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn run(name: &'static str, args: &[&str]) -> (i32, String, String) {
191 let dir = std::env::temp_dir();
192 let args: Vec<OsString> = args.iter().map(OsString::from).collect();
193 let (code, out, err, _) = run_in_counted(&dir, name, &args, b"");
194 (
195 code,
196 String::from_utf8(out).unwrap(),
197 String::from_utf8(err).unwrap(),
198 )
199 }
200
201 #[test]
202 fn help_goes_to_standard_error_and_usage_errors_to_standard_output() {
203 let (code, out, err) = run("brcat", &["-h"]);
204 assert_eq!((code, out.as_str()), (0, ""));
205 assert!(
206 err.starts_with("Usage: brcat [OPTION]... [FILE]...\n"),
207 "{err}"
208 );
209 let (code, out, err) = run("brotli", &["-x"]);
210 assert_eq!((code, err.as_str()), (1, "invalid argument -x\n"));
211 assert!(out.starts_with("Usage: brotli ["), "{out}");
212 }
213
214 #[test]
215 fn version_goes_to_standard_output() {
216 assert_eq!(
217 run("unbrotli", &["-V"]),
218 (0, "brotli 1.2.1\n".into(), String::new())
219 );
220 }
221
222 struct Scratch(std::path::PathBuf);
224
225 impl Scratch {
226 fn new() -> Self {
227 use std::sync::atomic::{AtomicUsize, Ordering};
228 static N: AtomicUsize = AtomicUsize::new(0);
229 let d = std::env::temp_dir().join(format!(
230 "stuffr-brotli-{}-{}",
231 std::process::id(),
232 N.fetch_add(1, Ordering::Relaxed)
233 ));
234 let _ = std::fs::remove_dir_all(&d);
235 std::fs::create_dir_all(&d).unwrap();
236 Scratch(d)
237 }
238 }
239
240 impl Drop for Scratch {
241 fn drop(&mut self) {
242 let _ = std::fs::remove_dir_all(&self.0);
243 }
244 }
245
246 fn run_at(dir: &Path, args: &[&str], stdin: &[u8]) -> (i32, Vec<u8>, String) {
247 let args: Vec<OsString> = args.iter().map(OsString::from).collect();
248 let (code, out, err, _) = run_in_counted(dir, "brotli", &args, stdin);
249 (code, out, String::from_utf8(err).unwrap())
250 }
251
252 const BR: &[u8] = b"\x21\x14\x00\x04hello\n\x03";
254
255 #[test]
256 fn compresses_a_file_and_standard_input_in_the_sandbox() {
257 let s = Scratch::new();
258 std::fs::write(s.0.join("a"), b"hello\n").unwrap();
259 assert_eq!(run_at(&s.0, &["a"], b""), (0, Vec::new(), String::new()));
260 let written = std::fs::read(s.0.join("a.br")).unwrap();
261 let (code, out, err) = run_at(&s.0, &["-c"], b"hello\n");
262 assert_eq!((code, err.as_str()), (0, ""));
263 if cfg!(feature = "brotli-c") {
264 assert_eq!(written, BR);
265 assert_ne!(out, BR);
267 }
268 assert_eq!(run_at(&s.0, &["-jf", "a"], b"").0, 0);
270 assert!(!s.0.join("a").exists() && s.0.join("a.br").exists());
271 }
272
273 #[test]
274 fn an_output_that_is_the_input_is_refused() {
275 let s = Scratch::new();
276 std::fs::write(s.0.join("a"), b"hello\n").unwrap();
277 for args in [&["-f", "-o", "a", "a"][..], &["-df", "-o", "a", "a"]] {
278 let (code, out, err) = run_at(&s.0, args, b"");
279 assert_eq!(
280 (code, out.as_slice(), err.as_str()),
281 (
282 1,
283 &b""[..],
284 "input file [a] is the same as the output file\n"
285 ),
286 "{args:?}"
287 );
288 assert_eq!(std::fs::read(s.0.join("a")).unwrap(), b"hello\n");
289 }
290 let (code, _, err) = run_at(&s.0, &["-o", "a", "a"], b"");
292 assert_eq!(
293 (code, err.as_str()),
294 (1, "failed to open output file [a]: File exists\n")
295 );
296 }
297
298 #[test]
299 fn names_map_as_next_file_maps_them() {
300 let s = Scratch::new();
301 std::fs::write(s.0.join("x"), BR).unwrap();
302 let (code, _, err) = run_at(&s.0, &["-d", "x"], b"");
303 assert_eq!(
304 (code, err.as_str()),
305 (1, "empty output file name for [x] input file\n")
306 );
307 std::fs::write(s.0.join("abcd"), BR).unwrap();
308 let (code, _, err) = run_at(&s.0, &["-d", "abcd"], b"");
309 assert_eq!(
310 (code, err.as_str()),
311 (1, "input file [abcd] suffix mismatch\n")
312 );
313 let (code, _, err) = run_at(&s.0, &["-S", ".x", "nosuch"], b"");
314 assert_eq!(
315 (code, err.as_str()),
316 (
317 1,
318 "failed to open input file [nosuch]: No such file or directory\n"
319 )
320 );
321 }
322
323 #[test]
324 fn verbose_and_squash_lines() {
325 let s = Scratch::new();
326 std::fs::write(s.0.join("a"), b"hello\n").unwrap();
327 let (code, _, err) = run_at(&s.0, &["-v", "-s", "a"], b"");
328 assert_eq!(code, 0);
329 let size = if cfg!(feature = "brotli-c") {
331 "11 B"
332 } else {
333 ""
334 };
335 assert!(err.starts_with("Compressed [a]: 6 B -> "), "{err}");
336 assert!(err.contains(size));
337 assert!(
338 err.ends_with(" in 0.00 sec\nOutput is larger than input\n"),
339 "{err}"
340 );
341 assert!(!s.0.join("a.br").exists());
342 }
343
344 fn run_capped(dir: &Path, args: &[&str], budget: u64) -> (i32, String) {
346 use std::collections::BTreeMap;
347 use std::sync::atomic::AtomicU64;
348 use std::sync::{Arc, Mutex};
349 let mut host = Host::Sandbox(Sandbox {
350 dir: dir.to_path_buf(),
351 stdin: Some(Vec::new()),
352 stdout: Arc::new(Mutex::new(Vec::new())),
353 budget: Arc::new(AtomicU64::new(budget)),
354 reads: Arc::new(Mutex::new(BTreeMap::new())),
355 });
356 let args: Vec<OsString> = args.iter().map(OsString::from).collect();
357 let mut err = Vec::new();
358 let code = main(b"brotli", &args, &mut host, &mut err);
359 (code, String::from_utf8(err).unwrap())
360 }
361
362 #[test]
366 fn rm_keeps_the_input_when_the_close_fails() {
367 let s = Scratch::new();
368 std::fs::write(s.0.join("a"), b"hello\n").unwrap();
369 let (code, err) = run_capped(&s.0, &["-j", "a"], 5);
370 assert_eq!(code, 1);
371 assert!(err.starts_with("fclose failed [a.br]: "), "{err}");
372 assert_eq!(std::fs::read(s.0.join("a")).unwrap(), b"hello\n");
373 std::fs::write(s.0.join("b.br"), BR).unwrap();
374 let (code, _) = run_capped(&s.0, &["-dj", "b.br"], 3);
375 assert_eq!(code, 1);
376 assert_eq!(std::fs::read(s.0.join("b.br")).unwrap(), BR);
377 assert_eq!(run_capped(&s.0, &["-jf", "a"], 1 << 20).0, 0);
379 assert!(!s.0.join("a").exists());
380 }
381
382 #[test]
384 fn a_later_input_operand_is_never_overwritten() {
385 let s = Scratch::new();
386 std::fs::write(s.0.join("a"), b"hello\n").unwrap();
387 std::fs::write(s.0.join("a.br"), b"keep").unwrap();
388 let (code, _, err) = run_at(&s.0, &["-f", "a", "a.br"], b"");
389 assert_eq!(
390 (code, err.as_str()),
391 (
392 1,
393 "input file [a.br] would be overwritten by the output of [a]\n"
394 )
395 );
396 assert_eq!(std::fs::read(s.0.join("a.br")).unwrap(), b"keep");
397 let (code, _, _) = run_at(&s.0, &["-f", "a", "-"], b"");
400 assert_eq!(code, 0);
401 }
402
403 #[test]
406 fn decodes_tests_and_checks_comments() {
407 let s = Scratch::new();
408 let mut two = BR.to_vec();
409 two.extend_from_slice(BR);
410 std::fs::write(s.0.join("two.br"), &two).unwrap();
411 assert_eq!(
412 run_at(&s.0, &["-vdcK", "two.br"], b""),
413 (
414 0,
415 b"hello\nhello\n".to_vec(),
416 "extra input\nDecompressed [two.br]: 22 B -> 12 B in 0.00 sec\n".into()
417 )
418 );
419 let (code, out, err) = run_at(&s.0, &["-vdc", "two.br"], b"");
420 assert_eq!(
421 (code, out.as_slice(), err.as_str()),
422 (
423 1,
424 &b"hello\n"[..],
425 "corrupt input [two.br]\nreason: extra input\n"
426 )
427 );
428 let (code, _, err) = run_at(&s.0, &["-vt"], &BR[..8]);
430 assert_eq!(
431 (code, err.as_str()),
432 (1, "corrupt input [con]\nreason: truncated input\n")
433 );
434 let (code, _, err) = run_at(&s.0, &["-vt"], b"\x1c");
436 assert_eq!(
437 (code, err.as_str()),
438 (1, "corrupt input [con]\nreason: FORMAT_RESERVED (-2)\n")
439 );
440 let foo = b"\x21\x4b\x00foo\x28\x00\x08hello\n\x03";
442 assert_eq!(run_at(&s.0, &["-t", "-C", "Zm9v"], foo).0, 0);
443 let (code, _, err) = run_at(&s.0, &["-vt", "-C", "Zm9x"], foo);
444 assert_eq!(
445 (code, err.as_str()),
446 (1, "corrupt input [con]\nreason: comment mismatch\n")
447 );
448 let (code, _, err) = run_at(&s.0, &["-t", "-C", "Zm9v"], BR);
449 assert_eq!((code, err.as_str()), (0, "corrupt input [con]\n"));
450 }
451
452 #[test]
455 fn test_mode_is_charged_in_the_sandbox() {
456 let s = Scratch::new();
457 let mut enc = stuffr::brotli_opts::Encoder::new(
459 &stuffr::brotli_opts::EncoderParams {
460 quality: 5,
461 lgwin: 22,
462 large_window: false,
463 size_hint: None,
464 },
465 None,
466 )
467 .unwrap();
468 let zeros = vec![0u8; 2 << 20];
469 let mut input = &zeros[..];
470 let mut buf = vec![0u8; 1 << 16];
471 let mut outv = Vec::new();
472 while !enc.is_finished() {
473 let mut out = &mut buf[..];
474 let room = out.len();
475 assert!(enc.compress_stream(
476 stuffr::brotli_opts::Operation::Finish,
477 &mut input,
478 &mut out
479 ));
480 let n = room - out.len();
481 outv.extend_from_slice(&buf[..n]);
482 }
483 std::fs::write(s.0.join("z.br"), &outv).unwrap();
484 assert_eq!(run_capped(&s.0, &["-t", "z.br"], 4 << 20).0, 0);
485 let (code, err) = run_capped(&s.0, &["-t", "z.br"], 1 << 20);
486 assert_eq!(code, 1);
487 assert!(
490 err.starts_with("failed to write output [z.br.br]: "),
491 "{err}"
492 );
493 assert!(!s.0.join("z.br.br").exists());
494 }
495}