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