1use std::collections::BTreeMap;
89use std::ffi::{OsStr, OsString};
90use std::io::{self, BufRead, BufReader, Read, Write};
91use std::path::Path;
92use std::process::ExitCode;
93use std::sync::atomic::AtomicU64;
94use std::sync::{Arc, Mutex};
95
96use super::CountedRun;
97use super::host::{self, Host, SANDBOX_OUTPUT_CAP, Sandbox, reset_sigpipe, strerror};
98
99pub(crate) mod args;
100mod coder;
101mod file_io;
102pub(crate) mod hardware;
103mod index;
104mod list;
105pub(crate) mod message;
106mod messages;
107pub(crate) mod options;
108pub(crate) mod suffix;
109
110use args::{Args, OpMode};
111use hardware::Hardware;
112use list::ListTotals;
113use message::{Messages, Status, Verbosity, mask_nonprint};
114
115pub fn run(name: &'static str, argv0: &OsStr, args: Vec<OsString>) -> ExitCode {
119 reset_sigpipe();
123 let argv0 = if argv0.is_empty() {
124 name.as_bytes()
125 } else {
126 argv0.as_encoded_bytes()
127 };
128 let env = |k: &str| std::env::var_os(k);
129 let mut err = io::stderr();
130 let code = main(
131 name,
132 argv0,
133 &args,
134 &env,
135 &mut Host::Real,
136 Hardware::detect(),
137 &mut err,
138 );
139 ExitCode::from(code)
140}
141
142#[doc(hidden)]
155pub fn run_in_counted(
156 dir: &Path,
157 name: &'static str,
158 args: &[OsString],
159 stdin: &[u8],
160 env: &dyn Fn(&str) -> Option<OsString>,
161) -> CountedRun {
162 let stdout = Arc::new(Mutex::new(Vec::new()));
163 let reads = Arc::new(Mutex::new(BTreeMap::new()));
164 let mut host = Host::Sandbox(Sandbox {
165 dir: dir.to_path_buf(),
166 stdin: Some(stdin.to_vec()),
167 stdout: stdout.clone(),
168 budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
169 reads: reads.clone(),
170 });
171 let mut err = Vec::new();
172 let code = main(
173 name,
174 name.as_bytes(),
175 args,
176 env,
177 &mut host,
178 Hardware::SANDBOX,
179 &mut err,
180 );
181 drop(host);
182 let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
183 let reads = std::mem::take(&mut *reads.lock().unwrap_or_else(|p| p.into_inner()));
184 (i32::from(code), out, err, reads)
185}
186
187fn main(
189 name: &str,
190 argv0: &[u8],
191 args: &[OsString],
192 env: &dyn Fn(&str) -> Option<OsString>,
193 host: &mut Host,
194 hw: Hardware,
195 err: &mut dyn Write,
196) -> u8 {
197 host::install_progress_signals(host);
199 let mut out = host.out_writer(None);
200 let (mut a, mut msg) = match args::parse(name, argv0, env, args, host, hw, &mut out, err) {
201 Ok(parsed) => parsed,
202 Err(code) => return code,
203 };
204 msg.automatic = host.stderr_is_tty();
207 msg.robot = a.robot;
208 msg.list = a.op == OpMode::List;
209 if matches!(host, Host::Sandbox(_)) {
210 msg.clock = message::frozen_clock;
211 }
212 if a.op != OpMode::List && a.robot {
213 return msg.fatal(err, messages::ROBOT_CODING, &[]);
214 }
215 msg.files_total = if a.files_from.is_some() {
217 0
218 } else {
219 a.files.len() as u32
220 };
221 if a.op == OpMode::Compress
223 && (a.to_stdout || a.files == [b"-".to_vec()])
224 && is_tty_stdout(host, &mut msg, err)
225 {
226 msg.try_help(err);
227 return Status::Error as u8;
228 }
229 if a.op != OpMode::List {
233 host::install_stop_handler(host);
234 }
235 let names = std::mem::take(&mut a.files);
236 let mut totals = ListTotals::default();
237 for n in &names {
238 let mut file = Some(n.as_slice());
239 if n == b"-" {
240 if a.op == OpMode::Compress {
241 if is_tty_stdout(host, &mut msg, err) {
242 continue;
243 }
244 } else if host.stdin_is_tty() {
245 msg.error(err, messages::TTY_STDIN, &[]);
246 continue;
247 }
248 if a.files_from.as_ref().is_some_and(|f| f.name.is_none()) {
249 msg.error(err, messages::STDIN_BOTH, &[]);
250 continue;
251 }
252 file = None;
253 }
254 if let Err(code) = run_one(&a, file, host, &mut msg, &mut out, err, &mut totals) {
255 return code;
256 }
257 }
258 if let Some(from) = a.files_from.take() {
259 let delim = from.delim;
260 let display = from.display_name().to_vec();
261 let source: Box<dyn Read + '_> = match from.file {
262 Some(f) => Box::new(f),
263 None => Box::new(host.stdin()),
264 };
265 let mut reader = BufReader::new(source);
266 while let Some(name) = read_name(&mut reader, delim, &display, &mut msg, err) {
267 if let Err(code) = run_one(&a, Some(&name), host, &mut msg, &mut out, err, &mut totals)
268 {
269 return code;
270 }
271 }
272 }
273 if a.op == OpMode::List {
274 list::list_totals(&a, &msg, &totals, &mut out);
275 }
276 let status = msg.final_status();
277 message::exit(
278 status,
279 &msg.progname,
280 &mut out,
281 err,
282 msg.verbosity != Verbosity::Silent,
283 )
284}
285
286fn is_tty_stdout(host: &Host, msg: &mut Messages, err: &mut dyn Write) -> bool {
288 let tty = host.stdout_is_tty();
289 if tty {
290 msg.error(err, messages::TTY_STDOUT, &[]);
291 }
292 tty
293}
294
295fn run_one(
299 a: &Args,
300 name: Option<&[u8]>,
301 host: &mut Host,
302 msg: &mut Messages,
303 out: &mut dyn Write,
304 err: &mut dyn Write,
305 totals: &mut ListTotals,
306) -> Result<(), u8> {
307 if a.op == OpMode::List {
308 return list::list_file(a, name, host, msg, out, err, totals);
309 }
310 coder::run(a, name, host, msg, err);
311 Ok(())
312}
313
314fn read_name(
317 r: &mut dyn BufRead,
318 delim: u8,
319 display: &[u8],
320 msg: &mut Messages,
321 err: &mut dyn Write,
322) -> Option<Vec<u8>> {
323 let mut name = Vec::new();
324 loop {
325 let mut byte = [0u8; 1];
326 match r.read(&mut byte) {
327 Ok(0) => {
328 if !name.is_empty() {
329 msg.error(
330 err,
331 messages::NAMES_UNEXPECTED_END,
332 &[&mask_nonprint(display)],
333 );
334 }
335 return None;
336 }
337 Ok(_) => {}
338 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
339 Err(e) => {
340 msg.error(
341 err,
342 messages::READ_NAMES_FAILED,
343 &[&mask_nonprint(display), &strerror(&e)],
344 );
345 return None;
346 }
347 }
348 let c = byte[0];
349 if c == delim {
350 if name.is_empty() {
351 continue;
352 }
353 return Some(name);
354 }
355 if c == 0 {
356 msg.error(err, messages::NAMES_NUL, &[&mask_nonprint(display)]);
357 return None;
358 }
359 name.push(c);
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn names(data: &[u8], delim: u8) -> (Vec<String>, String, Status) {
368 let mut msg = Messages::new(b"xz");
369 let mut err = Vec::new();
370 let mut r = data;
371 let mut got = Vec::new();
372 while let Some(n) = read_name(&mut r, delim, b"(stdin)", &mut msg, &mut err) {
373 got.push(String::from_utf8(n).unwrap());
374 }
375 (got, String::from_utf8(err).unwrap(), msg.status)
376 }
377
378 #[test]
379 fn read_name_splits_and_skips_empty_names() {
380 assert_eq!(
381 names(b"a\n\nb\n", b'\n'),
382 (vec!["a".into(), "b".into()], String::new(), Status::Success)
383 );
384 assert_eq!(names(b"a\0b\0", 0).0, ["a", "b"]);
385 let (got, err, st) = names(b"a\nb", b'\n');
386 assert_eq!(got, ["a"]);
387 assert_eq!(
388 (err.as_str(), st),
389 (
390 "xz: (stdin): Unexpected end of input when reading filenames\n",
391 Status::Error
392 )
393 );
394 let (got, err, _) = names(b"a\0b\n", b'\n');
395 assert!(got.is_empty());
396 assert!(err.contains("Null character found"), "{err}");
397 }
398
399 #[test]
403 fn run_in_counted_codes_in_the_sandbox() {
404 let dir = std::env::temp_dir().join(format!("stuffr-xz-sandbox-{}", std::process::id()));
405 let _ = std::fs::remove_dir_all(&dir);
406 std::fs::create_dir_all(&dir).unwrap();
407 let plain = b"sandboxed\n".repeat(100);
408 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
409 let (code, packed, err, _) = run_in_counted(&dir, "xz", &args(&["-T1"]), &plain, &|_| None);
410 assert_eq!((code, err.as_slice()), (0, &b""[..]));
411 assert!(packed.starts_with(&[0xFD, b'7', b'z', b'X', b'Z', 0]));
412 let (code, out, err, _) = run_in_counted(&dir, "xzcat", &[], &packed, &|_| None);
413 assert_eq!((code, out, err), (0, plain.clone(), Vec::new()));
414
415 std::fs::write(dir.join("a"), &plain).unwrap();
416 let (code, _, err, reads) = run_in_counted(&dir, "xz", &args(&["a"]), b"", &|_| None);
417 assert_eq!((code, err), (0, Vec::new()));
418 assert!(!dir.join("a").exists());
419 assert_eq!(
420 reads.get(&dir.join("a")).map(|r| r.bytes),
421 Some(plain.len() as u64)
422 );
423 let (code, out, err, _) =
424 run_in_counted(&dir, "xz", &args(&["-t", "a.xz"]), b"", &|_| None);
425 assert_eq!((code, out, err), (0, Vec::new(), Vec::new()));
426 let (code, _, err, _) = run_in_counted(&dir, "unxz", &args(&["a.xz"]), b"", &|_| None);
427 assert_eq!((code, err), (0, Vec::new()));
428 assert_eq!(std::fs::read(dir.join("a")).unwrap(), plain);
429
430 let zeros = vec![0u8; 9 << 20];
432 let (_, bomb, _, _) = run_in_counted(&dir, "xz", &args(&["-T1"]), &zeros, &|_| None);
433 let (code, _, err, _) = run_in_counted(&dir, "xzcat", &[], &bomb, &|_| None);
434 assert_eq!(code, 1);
435 assert!(
436 String::from_utf8_lossy(&err).starts_with("xzcat: (stdout): Write error: "),
437 "{}",
438 String::from_utf8_lossy(&err)
439 );
440 let (code, _, err, _) = run_in_counted(&dir, "xz", &args(&["-t"]), &bomb, &|_| None);
442 assert_eq!(code, 1);
443 assert!(
444 String::from_utf8_lossy(&err).starts_with("xz: (stdout): Write error: "),
445 "{}",
446 String::from_utf8_lossy(&err)
447 );
448 let _ = std::fs::remove_dir_all(&dir);
449 }
450
451 #[test]
455 fn run_in_counted_verbose_is_deterministic() {
456 let dir = std::env::temp_dir();
457 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
458 let plain = vec![b'x'; 300_000];
459 let (code, packed, err, _) =
460 run_in_counted(&dir, "xz", &args(&["-v", "-T1", "-0"]), &plain, &|_| None);
461 assert_eq!(code, 0);
462 let ratio = packed.len() as f64 / plain.len() as f64;
463 let want = format!("(stdin): {} B / 293.0 KiB = {ratio:.3}\n", packed.len());
464 assert_eq!(String::from_utf8(err).unwrap(), want);
465 let (code, _, err, _) = run_in_counted(&dir, "xz", &args(&["-tv"]), &packed, &|_| None);
466 assert_eq!((code, String::from_utf8(err).unwrap()), (0, want));
467 }
468
469 #[test]
473 fn run_in_counted_lists_in_the_sandbox() {
474 let dir = std::env::temp_dir().join(format!("stuffr-xz-list-{}", std::process::id()));
475 let _ = std::fs::remove_dir_all(&dir);
476 std::fs::create_dir_all(&dir).unwrap();
477 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
478 let (_, packed, _, _) = run_in_counted(&dir, "xz", &args(&["-T1"]), b"listed\n", &|_| None);
479 std::fs::write(dir.join("a.xz"), &packed).unwrap();
480 let (code, out, err, reads) = run_in_counted(
481 &dir,
482 "xz",
483 &args(&["--robot", "-lvv", "a.xz"]),
484 b"",
485 &|_| None,
486 );
487 assert_eq!((code, String::from_utf8(err).unwrap()), (0, String::new()));
488 let out = String::from_utf8(out).unwrap();
489 assert!(out.starts_with("name\ta.xz\nfile\t1\t1\t"), "{out}");
490 let needed = if cfg!(any(
493 target_arch = "x86_64",
494 all(target_arch = "x86", target_feature = "sse2")
495 )) {
496 8_454_808
497 } else {
498 8_454_776
499 };
500 assert!(
501 out.ends_with(&format!("\tCRC64\t0\t1\t{needed}\tno\t50000002\n")),
502 "{out}"
503 );
504 let read = reads.get(&dir.join("a.xz")).map_or(0, |r| r.bytes);
505 assert!(read > 0 && read <= 4 * packed.len() as u64, "read {read}");
506 let (code, _, err, _) = run_in_counted(&dir, "xz", &args(&["-l"]), &packed, &|_| None);
507 assert_eq!(code, 1);
508 assert_eq!(
509 String::from_utf8(err).unwrap(),
510 "xz: --list does not support reading from standard input\n"
511 );
512 let short = short_block(4, 5);
513 assert_eq!(short.len(), 44);
514 for (i, junk) in [
515 &b""[..],
516 b"\xfd7zXZ\0\0\x01",
517 &[0u8; 64],
518 &packed[..packed.len() - 4],
519 &short,
520 ]
521 .iter()
522 .enumerate()
523 {
524 let name = format!("j{i}.xz");
525 std::fs::write(dir.join(&name), junk).unwrap();
526 let (code, _, _, _) =
527 run_in_counted(&dir, "xz", &args(&["-lvv", &name]), b"", &|_| None);
528 assert_eq!(code, 1, "{name}");
529 }
530 let _ = std::fs::remove_dir_all(&dir);
531 }
532
533 fn crc32(data: &[u8]) -> [u8; 4] {
535 let mut c = flate2::Crc::new();
536 c.update(data);
537 c.sum().to_le_bytes()
538 }
539
540 fn short_block(check: u8, unpadded: u64) -> Vec<u8> {
544 let mut s = vec![0xFD, b'7', b'z', b'X', b'Z', 0, 0, check];
545 s.extend(crc32(&[0, check]));
546 s.extend(std::iter::repeat_n(0x55, ((unpadded + 3) & !3) as usize));
547 let mut index = vec![0, 1];
548 let mut v = unpadded;
549 while v >= 0x80 {
550 index.push(v as u8 | 0x80);
551 v >>= 7;
552 }
553 index.extend([v as u8, 0xE8, 0x07]);
554 while !index.len().is_multiple_of(4) {
555 index.push(0);
556 }
557 let crc = crc32(&index);
558 index.extend(crc);
559 s.extend(&index);
560 let mut tail = ((index.len() / 4 - 1) as u32).to_le_bytes().to_vec();
561 tail.extend([0, check]);
562 s.extend(crc32(&tail));
563 s.extend(tail);
564 s.extend(b"YZ");
565 s
566 }
567
568 #[test]
574 fn list_vv_on_hostile_input_never_panics() {
575 let dir = std::env::temp_dir().join(format!("stuffr-xz-listvv-{}", std::process::id()));
576 let _ = std::fs::remove_dir_all(&dir);
577 std::fs::create_dir_all(&dir).unwrap();
578 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
579 let plain = b"hostile listing\n".repeat(40);
580 let (_, packed, _, _) = run_in_counted(
581 &dir,
582 "xz",
583 &args(&["-T1", "-C", "sha256", "--block-size=100"]),
584 &plain,
585 &|_| None,
586 );
587 let mut inputs = Vec::new();
588 for check in 0..16u8 {
589 for unpadded in 5..=72 {
590 inputs.push(short_block(check, unpadded));
591 }
592 }
593 for i in 0..packed.len() {
594 for mask in [0x01, 0x80, 0xFF] {
595 let mut b = packed.clone();
596 b[i] ^= mask;
597 inputs.push(b);
598 }
599 }
600 for (i, data) in inputs.iter().enumerate() {
601 let name = format!("h{i}.xz");
602 std::fs::write(dir.join(&name), data).unwrap();
603 for form in [&["-lvv"][..], &["--robot", "-lvv"]] {
604 let mut a = form.to_vec();
605 a.push(&name);
606 let (code, _, _, _) = run_in_counted(&dir, "xz", &args(&a), b"", &|_| None);
607 assert!(matches!(code, 0 | 1), "{name} {form:?}: exit {code}");
608 }
609 std::fs::remove_file(dir.join(&name)).unwrap();
610 }
611 let _ = std::fs::remove_dir_all(&dir);
612 }
613
614 #[test]
617 fn files_from_a_file_are_counted() {
618 let dir = std::env::temp_dir().join(format!("stuffr-xz-files-{}", std::process::id()));
619 let _ = std::fs::remove_dir_all(&dir);
620 std::fs::create_dir_all(&dir).unwrap();
621 std::fs::write(dir.join("list"), b"a\n").unwrap();
622 std::fs::write(dir.join("a"), b"listed by name\n").unwrap();
623 let args = ["-k".into(), "--files=list".into()];
624 let (code, _, err, reads) = run_in_counted(&dir, "xz", &args, b"", &|_| None);
625 assert_eq!((code, String::from_utf8(err).unwrap()), (0, String::new()));
626 let got = |n: &str| reads.get(&dir.join(n)).copied().unwrap_or_default();
627 assert_eq!(
628 (got("list").opens, got("list").bytes, got("list").len),
629 (1, 2, 2)
630 );
631 assert_eq!((got("a").opens, got("a").bytes), (1, 15));
632 let _ = std::fs::remove_dir_all(&dir);
633 }
634
635 #[test]
636 fn run_in_counted_parses_in_the_sandbox() {
637 let dir = std::env::temp_dir();
638 let (code, out, err, reads) = run_in_counted(&dir, "xz", &["-h".into()], b"", &|_| None);
639 assert_eq!((code, err.len(), reads.len()), (0, 0, 0));
640 assert!(out.starts_with(b"Usage: xz [OPTION]... [FILE]...\n"));
641 let (code, out, _, _) = run_in_counted(
643 &dir,
644 "xz",
645 &["--robot".into(), "--info-memory".into()],
646 b"",
647 &|_| None,
648 );
649 assert_eq!(code, 0);
650 assert_eq!(
651 String::from_utf8(out).unwrap(),
652 "4294967296\t0\t0\t268435456\t1073741824\t2\n"
653 );
654 let (code, _, err, _) = run_in_counted(&dir, "xz", &["--robot".into()], b"", &|_| None);
655 assert_eq!(code, 1);
656 assert_eq!(
657 String::from_utf8(err).unwrap(),
658 "xz: Compression and decompression with --robot are not supported yet.\n"
659 );
660 }
661}