1use std::ffi::{OsStr, OsString};
6use std::io::{self, IsTerminal};
7use std::path::Path;
8use std::process::ExitCode;
9
10pub mod bzip2;
11pub mod bzip2recover;
12pub mod links;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum Family {
18 Bzip2,
20}
21
22const NAMES: &[(&str, Family)] = &[
25 ("bzip2", Family::Bzip2),
26 ("bunzip2", Family::Bzip2),
27 ("bzcat", Family::Bzip2),
28 ("bzip2recover", Family::Bzip2),
29];
30
31pub fn names() -> &'static [&'static str] {
33 static LIST: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
34 LIST.get_or_init(|| NAMES.iter().map(|&(n, _)| n).collect())
35}
36
37pub fn family_for(argv0: &OsStr) -> Option<(Family, &'static str)> {
41 let file = Path::new(argv0).file_name()?;
42 let file = file.to_str()?;
43 #[cfg(windows)]
44 let file = file.strip_suffix(".exe").unwrap_or(file);
45 NAMES
46 .iter()
47 .find(|(n, _)| *n == file)
48 .map(|&(n, fam)| (fam, n))
49}
50
51pub fn dispatch(args: Vec<OsString>) -> Option<ExitCode> {
56 let (family, name) = family_for(args.first()?)?;
57 let mut args = args.into_iter();
58 let argv0 = args.next().unwrap_or_default();
59 let rest = args.collect();
60 Some(match family {
61 Family::Bzip2 if name == "bzip2recover" => bzip2recover::run(argv0, rest),
63 Family::Bzip2 => bzip2::run(name, rest),
64 })
65}
66
67#[doc(hidden)]
75pub fn run_in(
76 dir: &Path,
77 name: &str,
78 args: Vec<OsString>,
79 stdin: &[u8],
80) -> Option<(i32, Vec<u8>, Vec<u8>)> {
81 let (family, canonical) = family_for(OsStr::new(name))?;
82 Some(match family {
83 Family::Bzip2 if canonical == "bzip2recover" => {
84 let (code, err) = bzip2recover::run_in(dir, name.as_bytes(), &args);
85 (code, Vec::new(), err)
86 }
87 Family::Bzip2 => bzip2::run_in(dir, canonical, &args, stdin),
88 })
89}
90
91pub(crate) fn stdin_is_tty() -> bool {
93 io::stdin().is_terminal()
94}
95
96pub(crate) fn stdout_is_tty() -> bool {
98 io::stdout().is_terminal()
99}
100
101pub(crate) fn copy_metadata_from(meta: &std::fs::Metadata, to: &Path) -> io::Result<()> {
110 let mut times = std::fs::FileTimes::new();
111 if let Ok(t) = meta.accessed() {
112 times = times.set_accessed(t);
113 }
114 if let Ok(t) = meta.modified() {
115 times = times.set_modified(t);
116 }
117 std::fs::File::options()
118 .write(true)
119 .open(to)?
120 .set_times(times)?;
121 #[cfg(unix)]
122 {
123 use std::os::unix::fs::MetadataExt;
124 match std::os::unix::fs::chown(to, Some(meta.uid()), Some(meta.gid())) {
125 Err(e) if e.kind() != io::ErrorKind::PermissionDenied => return Err(e),
126 _ => {}
127 }
128 }
129 std::fs::set_permissions(to, meta.permissions())
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
140 fn fuzz_names_match_the_dispatch_table() {
141 assert_eq!(stuffr::testing::COMPAT_NAMES, names());
142 }
143
144 #[test]
145 fn compat_names_are_recognised_by_file_name_only() {
146 use std::ffi::OsStr;
147 assert_eq!(family_for(OsStr::new("bzip2")).map(|f| f.1), Some("bzip2"));
148 assert_eq!(
149 family_for(OsStr::new("/usr/local/bin/bunzip2")).map(|f| f.1),
150 Some("bunzip2")
151 );
152 assert_eq!(family_for(OsStr::new("bzcat")).map(|f| f.1), Some("bzcat"));
153 assert_eq!(
154 family_for(OsStr::new("bzip2recover")).map(|f| f.1),
155 Some("bzip2recover")
156 );
157 assert!(family_for(OsStr::new("stuffr")).is_none());
158 assert!(family_for(OsStr::new("/x/bzip2/stuffr")).is_none());
159 assert!(family_for(OsStr::new("bzip2x")).is_none());
160 #[cfg(windows)]
161 assert_eq!(
162 family_for(OsStr::new("bzip2.exe")).map(|f| f.1),
163 Some("bzip2")
164 );
165 }
166
167 #[test]
168 fn stuffr_itself_does_not_dispatch() {
169 assert!(dispatch(vec!["stuffr".into(), "list".into()]).is_none());
170 }
171
172 #[test]
173 fn dispatch_ignores_an_empty_argv() {
174 assert!(dispatch(Vec::new()).is_none());
175 }
176
177 #[cfg(unix)]
178 #[test]
179 fn dispatch_ignores_a_non_utf8_argv0() {
180 use std::os::unix::ffi::OsStrExt;
181 let bad = OsStr::from_bytes(b"bzip2\xff").to_os_string();
182 assert!(dispatch(vec![bad]).is_none());
183 let bad = OsStr::from_bytes(b"/x/\xffbzip2").to_os_string();
184 assert!(dispatch(vec![bad]).is_none());
185 }
186
187 #[test]
190 fn run_in_works_inside_its_directory() {
191 let dir = std::env::temp_dir().join(format!("stuffr-run-in-{}", std::process::id()));
192 let _ = std::fs::remove_dir_all(&dir);
193 std::fs::create_dir_all(&dir).unwrap();
194 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
195 let (code, out, err) = run_in(&dir, "bzip2", vec!["-k".into(), "a".into()], b"").unwrap();
196 assert_eq!(
197 (code, out.len()),
198 (0, 0),
199 "{}",
200 String::from_utf8_lossy(&err)
201 );
202 let packed = std::fs::read(dir.join("a.bz2")).unwrap();
203 assert!(packed.starts_with(b"BZh9"));
204 let (code, out, _) = run_in(&dir, "bzcat", vec![], &packed).unwrap();
205 assert_eq!((code, out.as_slice()), (0, &b"hello hello hello\n"[..]));
206 let (code, out, err) = run_in(&dir, "bzip2recover", vec!["a.bz2".into()], b"").unwrap();
207 assert_eq!(
208 (code, out.len()),
209 (0, 0),
210 "{}",
211 String::from_utf8_lossy(&err)
212 );
213 assert!(dir.join("rec00001a.bz2").exists());
214 let (code, _, err) = run_in(&dir, "bunzip2", vec![], b"BZh9garbage").unwrap();
216 assert_eq!(code, 2, "{}", String::from_utf8_lossy(&err));
217 assert!(run_in(&dir, "stuffr", vec![], b"").is_none());
218 let _ = std::fs::remove_dir_all(&dir);
219 }
220
221 #[test]
223 fn run_in_caps_what_a_run_writes() {
224 let dir = std::env::temp_dir().join(format!("stuffr-run-in-cap-{}", std::process::id()));
225 let _ = std::fs::remove_dir_all(&dir);
226 std::fs::create_dir_all(&dir).unwrap();
227 let big = vec![0u8; 9 * 1024 * 1024];
228 let (code, packed, _) = run_in(&dir, "bzip2", vec![], &big).unwrap();
229 assert_eq!(code, 0);
230 let (code, out, err) = run_in(&dir, "bzcat", vec![], &packed).unwrap();
231 let err = String::from_utf8_lossy(&err);
232 assert_eq!(code, 1, "{err}");
233 let cap = 8 * 1024 * 1024;
236 assert!(
237 out.len() <= cap && out.len() > cap - 64 * 1024,
238 "{}",
239 out.len()
240 );
241 assert!(err.contains("I/O or other error"), "{err}");
242 let (code, out, err) = run_in(&dir, "bzip2", vec!["-t".into()], &packed).unwrap();
244 let err = String::from_utf8_lossy(&err);
245 assert_eq!((code, out.len()), (1, 0), "{err}");
246 assert!(err.contains("I/O or other error"), "{err}");
247 let _ = std::fs::remove_dir_all(&dir);
248 }
249
250 #[test]
251 fn names_lists_the_table() {
252 assert_eq!(names(), ["bzip2", "bunzip2", "bzcat", "bzip2recover"]);
253 }
254
255 #[test]
256 fn copy_metadata_carries_mtime_and_permissions() {
257 let dir = std::env::temp_dir().join(format!("stuffr-meta-{}", std::process::id()));
258 std::fs::create_dir_all(&dir).unwrap();
259 let (a, b) = (dir.join("a"), dir.join("b"));
260 std::fs::write(&a, b"x").unwrap();
261 std::fs::write(&b, b"y").unwrap();
262 let old = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
263 std::fs::File::options()
264 .write(true)
265 .open(&a)
266 .unwrap()
267 .set_modified(old)
268 .unwrap();
269 #[cfg(unix)]
270 {
271 use std::os::unix::fs::PermissionsExt;
272 std::fs::set_permissions(&a, std::fs::Permissions::from_mode(0o640)).unwrap();
273 }
274 copy_metadata_from(&std::fs::metadata(&a).unwrap(), &b).unwrap();
275 let (ma, mb) = (
276 std::fs::metadata(&a).unwrap(),
277 std::fs::metadata(&b).unwrap(),
278 );
279 assert_eq!(ma.modified().unwrap(), mb.modified().unwrap());
280 assert_eq!(ma.permissions(), mb.permissions());
281 let _ = std::fs::remove_dir_all(&dir);
282 }
283}