1use std::collections::BTreeMap;
6use std::ffi::{OsStr, OsString};
7use std::io::{self, IsTerminal};
8use std::path::{Path, PathBuf};
9use std::process::ExitCode;
10
11pub mod bzip2;
12pub mod bzip2recover;
13pub mod gzip;
14mod host;
15pub mod links;
16pub mod xz;
17
18#[doc(hidden)]
19pub use host::InputReads;
20
21#[doc(hidden)]
24pub type CountedRun = (i32, Vec<u8>, Vec<u8>, BTreeMap<PathBuf, InputReads>);
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum Family {
30 Bzip2,
32 Gzip,
35 Xz,
37}
38
39const NAMES: &[(&str, Family)] = &[
42 ("bzip2", Family::Bzip2),
43 ("bunzip2", Family::Bzip2),
44 ("bzcat", Family::Bzip2),
45 ("bzip2recover", Family::Bzip2),
46 ("gzip", Family::Gzip),
47 ("gunzip", Family::Gzip),
48 ("zcat", Family::Gzip),
49 ("gzcat", Family::Gzip),
50 ("xz", Family::Xz),
51 ("unxz", Family::Xz),
52 ("xzcat", Family::Xz),
53 ("lzma", Family::Xz),
54 ("unlzma", Family::Xz),
55 ("lzcat", Family::Xz),
56];
57
58pub fn names() -> &'static [&'static str] {
60 static LIST: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
61 LIST.get_or_init(|| NAMES.iter().map(|&(n, _)| n).collect())
62}
63
64pub fn family_for(argv0: &OsStr) -> Option<(Family, &'static str)> {
68 let file = Path::new(argv0).file_name()?;
69 let file = file.to_str()?;
70 #[cfg(windows)]
71 let file = file.strip_suffix(".exe").unwrap_or(file);
72 NAMES
73 .iter()
74 .find(|(n, _)| *n == file)
75 .map(|&(n, fam)| (fam, n))
76}
77
78pub fn dispatch(args: Vec<OsString>) -> Option<ExitCode> {
83 let (family, name) = family_for(args.first()?)?;
84 let mut args = args.into_iter();
85 let argv0 = args.next().unwrap_or_default();
86 let rest = args.collect();
87 Some(match family {
88 Family::Bzip2 if name == "bzip2recover" => bzip2recover::run(argv0, rest),
90 Family::Bzip2 => bzip2::run(name, rest),
91 Family::Gzip => gzip::run(name, rest),
92 Family::Xz => xz::run(name, &argv0, rest),
93 })
94}
95
96#[doc(hidden)]
106pub fn run_in(
107 dir: &Path,
108 name: &str,
109 args: Vec<OsString>,
110 stdin: &[u8],
111 gzip_flavour: gzip::Flavour,
112) -> Option<(i32, Vec<u8>, Vec<u8>)> {
113 run_in_counted(dir, name, args, stdin, gzip_flavour).map(|(code, out, err, _)| (code, out, err))
114}
115
116#[doc(hidden)]
121pub fn run_in_counted(
122 dir: &Path,
123 name: &str,
124 args: Vec<OsString>,
125 stdin: &[u8],
126 gzip_flavour: gzip::Flavour,
127) -> Option<CountedRun> {
128 let (family, canonical) = family_for(OsStr::new(name))?;
129 Some(match family {
130 Family::Bzip2 if canonical == "bzip2recover" => {
131 let (code, err) = bzip2recover::run_in(dir, name.as_bytes(), &args);
132 (code, Vec::new(), err, BTreeMap::new())
133 }
134 Family::Bzip2 => {
135 let (code, out, err) = bzip2::run_in(dir, canonical, &args, stdin);
136 (code, out, err, BTreeMap::new())
137 }
138 Family::Gzip => gzip::run_in_counted(gzip_flavour, dir, canonical, &args, stdin, &|_| None),
139 Family::Xz => xz::run_in_counted(dir, canonical, &args, stdin, &|_| None),
140 })
141}
142
143pub(crate) fn stdin_is_tty() -> bool {
145 io::stdin().is_terminal()
146}
147
148pub(crate) fn stdout_is_tty() -> bool {
150 io::stdout().is_terminal()
151}
152
153pub(crate) fn copy_metadata_from(meta: &std::fs::Metadata, to: &Path) -> io::Result<()> {
162 copy_metadata_with_mtime(meta, to, None)
163}
164
165pub(crate) fn copy_metadata_with_mtime(
168 meta: &std::fs::Metadata,
169 to: &Path,
170 mtime: Option<std::time::SystemTime>,
171) -> io::Result<()> {
172 let mut times = std::fs::FileTimes::new();
173 if let Ok(t) = meta.accessed() {
174 times = times.set_accessed(t);
175 }
176 if let Some(t) = mtime.or_else(|| meta.modified().ok()) {
177 times = times.set_modified(t);
178 }
179 std::fs::File::options()
180 .write(true)
181 .open(to)?
182 .set_times(times)?;
183 #[cfg(unix)]
184 {
185 use std::os::unix::fs::MetadataExt;
186 match std::os::unix::fs::chown(to, Some(meta.uid()), Some(meta.gid())) {
187 Err(e) if e.kind() != io::ErrorKind::PermissionDenied => return Err(e),
188 _ => {}
189 }
190 }
191 std::fs::set_permissions(to, meta.permissions())
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 const G: gzip::Flavour = gzip::Flavour::Gnu;
201
202 #[test]
205 fn fuzz_names_match_the_dispatch_table() {
206 assert_eq!(stuffr::testing::COMPAT_NAMES, names());
207 }
208
209 #[test]
210 fn compat_names_are_recognised_by_file_name_only() {
211 use std::ffi::OsStr;
212 assert_eq!(family_for(OsStr::new("bzip2")).map(|f| f.1), Some("bzip2"));
213 assert_eq!(
214 family_for(OsStr::new("/usr/local/bin/bunzip2")).map(|f| f.1),
215 Some("bunzip2")
216 );
217 assert_eq!(family_for(OsStr::new("bzcat")).map(|f| f.1), Some("bzcat"));
218 assert_eq!(
219 family_for(OsStr::new("bzip2recover")).map(|f| f.1),
220 Some("bzip2recover")
221 );
222 assert!(family_for(OsStr::new("stuffr")).is_none());
223 assert!(family_for(OsStr::new("/x/bzip2/stuffr")).is_none());
224 assert!(family_for(OsStr::new("bzip2x")).is_none());
225 #[cfg(windows)]
226 assert_eq!(
227 family_for(OsStr::new("bzip2.exe")).map(|f| f.1),
228 Some("bzip2")
229 );
230 }
231
232 #[test]
233 fn stuffr_itself_does_not_dispatch() {
234 assert!(dispatch(vec!["stuffr".into(), "list".into()]).is_none());
235 }
236
237 #[test]
238 fn dispatch_ignores_an_empty_argv() {
239 assert!(dispatch(Vec::new()).is_none());
240 }
241
242 #[cfg(unix)]
243 #[test]
244 fn dispatch_ignores_a_non_utf8_argv0() {
245 use std::os::unix::ffi::OsStrExt;
246 let bad = OsStr::from_bytes(b"bzip2\xff").to_os_string();
247 assert!(dispatch(vec![bad]).is_none());
248 let bad = OsStr::from_bytes(b"/x/\xffbzip2").to_os_string();
249 assert!(dispatch(vec![bad]).is_none());
250 }
251
252 #[test]
255 fn run_in_works_inside_its_directory() {
256 let dir = std::env::temp_dir().join(format!("stuffr-run-in-{}", std::process::id()));
257 let _ = std::fs::remove_dir_all(&dir);
258 std::fs::create_dir_all(&dir).unwrap();
259 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
260 let (code, out, err) =
261 run_in(&dir, "bzip2", vec!["-k".into(), "a".into()], b"", G).unwrap();
262 assert_eq!(
263 (code, out.len()),
264 (0, 0),
265 "{}",
266 String::from_utf8_lossy(&err)
267 );
268 let packed = std::fs::read(dir.join("a.bz2")).unwrap();
269 assert!(packed.starts_with(b"BZh9"));
270 let (code, out, _) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
271 assert_eq!((code, out.as_slice()), (0, &b"hello hello hello\n"[..]));
272 let (code, out, err) = run_in(&dir, "bzip2recover", vec!["a.bz2".into()], b"", G).unwrap();
273 assert_eq!(
274 (code, out.len()),
275 (0, 0),
276 "{}",
277 String::from_utf8_lossy(&err)
278 );
279 assert!(dir.join("rec00001a.bz2").exists());
280 let (code, _, err) = run_in(&dir, "bunzip2", vec![], b"BZh9garbage", G).unwrap();
282 assert_eq!(code, 2, "{}", String::from_utf8_lossy(&err));
283 assert!(run_in(&dir, "stuffr", vec![], b"", G).is_none());
284 let _ = std::fs::remove_dir_all(&dir);
285 }
286
287 #[test]
292 fn run_in_counted_counts_the_gzip_familys_inputs() {
293 let dir = std::env::temp_dir().join(format!("stuffr-run-in-count-{}", std::process::id()));
294 for flavour in [gzip::Flavour::Gnu, gzip::Flavour::Bsd] {
295 let _ = std::fs::remove_dir_all(&dir);
296 std::fs::create_dir_all(&dir).unwrap();
297 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
298 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
299 let (code, _, err, reads) =
300 run_in_counted(&dir, "gzip", args(&["-k", "a"]), b"", flavour).unwrap();
301 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
302 let a = reads[&dir.join("a")];
303 assert_eq!((a.opens, a.bytes, a.len), (1, 18, 18), "{flavour:?}");
304 assert_eq!(reads.len(), 1, "{reads:?}");
305
306 let len = std::fs::metadata(dir.join("a.gz")).unwrap().len();
307 for mode in [&["-t", "a.gz", "a.gz"][..], &["-l", "a.gz"]] {
308 let (code, _, err, reads) =
309 run_in_counted(&dir, "gzip", args(mode), b"", flavour).unwrap();
310 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
311 let gz = reads[&dir.join("a.gz")];
312 assert_eq!(gz.opens, mode.len() as u64 - 1, "{flavour:?} {mode:?}");
313 assert_eq!(gz.len, len);
314 assert!(gz.bytes > 0 && gz.bytes <= 2 * len * gz.opens, "{gz:?}");
315 }
316 let (_, _, _, reads) =
317 run_in_counted(&dir, "bzip2", args(&["-k", "a"]), b"", flavour).unwrap();
318 assert!(reads.is_empty(), "{reads:?}");
319 }
320 let _ = std::fs::remove_dir_all(&dir);
321 }
322
323 #[test]
325 fn run_in_caps_what_a_run_writes() {
326 let dir = std::env::temp_dir().join(format!("stuffr-run-in-cap-{}", std::process::id()));
327 let _ = std::fs::remove_dir_all(&dir);
328 std::fs::create_dir_all(&dir).unwrap();
329 let big = vec![0u8; 9 * 1024 * 1024];
330 let (code, packed, _) = run_in(&dir, "bzip2", vec![], &big, G).unwrap();
331 assert_eq!(code, 0);
332 let (code, out, err) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
333 let err = String::from_utf8_lossy(&err);
334 assert_eq!(code, 1, "{err}");
335 let cap = 8 * 1024 * 1024;
338 assert!(
339 out.len() <= cap && out.len() > cap - 64 * 1024,
340 "{}",
341 out.len()
342 );
343 assert!(err.contains("I/O or other error"), "{err}");
344 let (code, out, err) = run_in(&dir, "bzip2", vec!["-t".into()], &packed, G).unwrap();
346 let err = String::from_utf8_lossy(&err);
347 assert_eq!((code, out.len()), (1, 0), "{err}");
348 assert!(err.contains("I/O or other error"), "{err}");
349 let _ = std::fs::remove_dir_all(&dir);
350 }
351
352 #[test]
353 fn names_lists_the_table() {
354 assert_eq!(
355 names(),
356 [
357 "bzip2",
358 "bunzip2",
359 "bzcat",
360 "bzip2recover",
361 "gzip",
362 "gunzip",
363 "zcat",
364 "gzcat",
365 "xz",
366 "unxz",
367 "xzcat",
368 "lzma",
369 "unlzma",
370 "lzcat"
371 ]
372 );
373 }
374
375 #[test]
376 fn copy_metadata_carries_mtime_and_permissions() {
377 let dir = std::env::temp_dir().join(format!("stuffr-meta-{}", std::process::id()));
378 std::fs::create_dir_all(&dir).unwrap();
379 let (a, b) = (dir.join("a"), dir.join("b"));
380 std::fs::write(&a, b"x").unwrap();
381 std::fs::write(&b, b"y").unwrap();
382 let old = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
383 std::fs::File::options()
384 .write(true)
385 .open(&a)
386 .unwrap()
387 .set_modified(old)
388 .unwrap();
389 #[cfg(unix)]
390 {
391 use std::os::unix::fs::PermissionsExt;
392 std::fs::set_permissions(&a, std::fs::Permissions::from_mode(0o640)).unwrap();
393 }
394 copy_metadata_from(&std::fs::metadata(&a).unwrap(), &b).unwrap();
395 let (ma, mb) = (
396 std::fs::metadata(&a).unwrap(),
397 std::fs::metadata(&b).unwrap(),
398 );
399 assert_eq!(ma.modified().unwrap(), mb.modified().unwrap());
400 assert_eq!(ma.permissions(), mb.permissions());
401 let _ = std::fs::remove_dir_all(&dir);
402 }
403}