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