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