1use std::collections::BTreeMap;
24use std::ffi::OsString;
25use std::io::{self, Write};
26use std::path::Path;
27use std::process::ExitCode;
28use std::sync::atomic::AtomicU64;
29use std::sync::{Arc, Mutex};
30
31use super::CountedRun;
32use super::host::{Host, SANDBOX_OUTPUT_CAP, Sandbox, reset_sigpipe};
33
34mod bsd;
35mod gnu;
36#[allow(dead_code)]
42pub(crate) mod member;
43#[allow(dead_code)]
44pub(crate) mod sniff;
45
46pub use sniff::Flavour;
47
48const FLAVOR_VAR: &str = "STUFFR_GZIP_FLAVOR";
50
51pub fn default_flavour() -> Flavour {
55 if cfg!(any(
56 target_vendor = "apple",
57 target_os = "freebsd",
58 target_os = "netbsd",
59 target_os = "openbsd",
60 target_os = "dragonfly"
61 )) {
62 Flavour::Bsd
63 } else {
64 Flavour::Gnu
65 }
66}
67
68fn flavour_from(value: Option<OsString>) -> Result<Flavour, OsString> {
71 match value {
72 None => Ok(default_flavour()),
73 Some(v) if v.is_empty() => Ok(default_flavour()),
74 Some(v) if v == "gnu" => Ok(Flavour::Gnu),
75 Some(v) if v == "bsd" => Ok(Flavour::Bsd),
76 Some(v) => Err(v),
77 }
78}
79
80pub fn run(name: &'static str, args: Vec<OsString>) -> ExitCode {
83 reset_sigpipe();
85 let env = |k: &str| std::env::var_os(k);
86 let mut err = io::stderr();
87 let flavour = match flavour_from(env(FLAVOR_VAR)) {
88 Ok(f) => f,
89 Err(v) => {
90 let _ = writeln!(
91 err,
92 "{name}: {FLAVOR_VAR} must be gnu or bsd, not '{}'",
93 v.to_string_lossy()
94 );
95 return ExitCode::from(1);
96 }
97 };
98 let argv0 = std::env::args_os()
101 .next()
102 .map(|a| a.as_encoded_bytes().to_vec())
103 .unwrap_or_else(|| name.as_bytes().to_vec());
104 let code = main(
105 flavour,
106 name,
107 &argv0,
108 &args,
109 &env,
110 &mut Host::Real,
111 &mut err,
112 );
113 ExitCode::from(code)
114}
115
116fn main(
118 flavour: Flavour,
119 name: &'static str,
120 argv0: &[u8],
121 args: &[OsString],
122 env: &dyn Fn(&str) -> Option<OsString>,
123 host: &mut Host,
124 err: &mut dyn Write,
125) -> u8 {
126 match flavour {
127 Flavour::Gnu => gnu::main(name, argv0, args, env, host, err),
128 Flavour::Bsd => bsd::main(name, args, env, host, err),
129 }
130}
131
132#[doc(hidden)]
141pub fn run_in(
142 flavour: Flavour,
143 dir: &Path,
144 name: &'static str,
145 args: &[OsString],
146 stdin: &[u8],
147 env: &dyn Fn(&str) -> Option<OsString>,
148) -> (i32, Vec<u8>, Vec<u8>) {
149 let (code, out, err, _) = run_in_counted(flavour, dir, name, args, stdin, env);
150 (code, out, err)
151}
152
153#[doc(hidden)]
155pub fn run_in_counted(
156 flavour: Flavour,
157 dir: &Path,
158 name: &'static str,
159 args: &[OsString],
160 stdin: &[u8],
161 env: &dyn Fn(&str) -> Option<OsString>,
162) -> CountedRun {
163 let stdout = Arc::new(Mutex::new(Vec::new()));
164 let reads = Arc::new(Mutex::new(BTreeMap::new()));
165 let mut host = Host::Sandbox(Sandbox {
166 dir: dir.to_path_buf(),
167 stdin: Some(stdin.to_vec()),
168 stdout: stdout.clone(),
169 budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
170 reads: reads.clone(),
171 });
172 let mut err = Vec::new();
173 let code = main(
174 flavour,
175 name,
176 name.as_bytes(),
177 args,
178 env,
179 &mut host,
180 &mut err,
181 );
182 drop(host);
183 let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
184 let reads = std::mem::take(&mut *reads.lock().unwrap_or_else(|p| p.into_inner()));
185 (i32::from(code), out, err, reads)
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn the_flavour_variable_selects_or_is_refused() {
194 assert_eq!(flavour_from(None), Ok(default_flavour()));
195 assert_eq!(flavour_from(Some("".into())), Ok(default_flavour()));
196 assert_eq!(flavour_from(Some("gnu".into())), Ok(Flavour::Gnu));
197 assert_eq!(flavour_from(Some("bsd".into())), Ok(Flavour::Bsd));
198 assert_eq!(flavour_from(Some("GNU".into())), Err("GNU".into()));
199 assert_eq!(flavour_from(Some(" gnu".into())), Err(" gnu".into()));
200 }
201
202 const SMALL_STACK: usize = 256 * 1024;
206
207 #[cfg(unix)]
216 #[test]
217 fn recursion_depth_costs_no_stack() {
218 let root = std::env::temp_dir().join(format!("stuffr-deep-{}", std::process::id()));
219 let _ = std::fs::remove_dir_all(&root);
220 std::fs::create_dir_all(&root).unwrap();
221 let root = root.canonicalize().unwrap();
223 let path_max = if cfg!(target_os = "macos") {
227 1024
228 } else {
229 4096
230 };
231 let room = path_max - 1 - root.as_os_str().len() - 64;
232 let levels = (room / 2).min(1000);
233 assert!(levels >= 400, "only {levels} levels fit under {root:?}");
234 let deep = root.join(vec!["d"; levels].join("/"));
235 let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
236 gz.write_all(b"deep\n").unwrap();
237 let gz = gz.finish().unwrap();
238 for flavour in [Flavour::Gnu, Flavour::Bsd] {
239 std::fs::create_dir_all(&deep).unwrap();
240 std::fs::write(deep.join("leaf.gz"), &gz).unwrap();
241 let skipped = if flavour == Flavour::Bsd { 1 } else { 0 };
244 for (name, args, want) in [
245 ("gzip", &["-r", "d"][..], skipped),
246 ("gzip", &["-tr", "d"], 0),
247 ("gunzip", &["-r", "d"], 0),
248 ] {
249 let dir = root.clone();
250 let argv: Vec<OsString> = args.iter().map(OsString::from).collect();
251 let (code, _, err) = std::thread::Builder::new()
253 .name("recursion_depth_costs_no_stack".into())
254 .stack_size(SMALL_STACK)
255 .spawn(move || run_in(flavour, &dir, name, &argv, b"", &|_| None))
256 .unwrap()
257 .join()
258 .expect("the walk panicked");
259 let err = String::from_utf8_lossy(&err);
260 assert_eq!(code, want, "{flavour:?} {name} {args:?}: {err}");
261 }
262 assert_eq!(std::fs::read(deep.join("leaf")).unwrap(), b"deep\n");
263 assert!(!deep.join("leaf.gz").exists());
264 std::fs::remove_dir_all(root.join("d")).unwrap();
265 }
266 let _ = std::fs::remove_dir_all(&root);
267 }
268
269 #[test]
270 fn the_default_flavour_follows_the_platform() {
271 let bsd = cfg!(any(
272 target_os = "macos",
273 target_os = "ios",
274 target_os = "freebsd",
275 target_os = "netbsd",
276 target_os = "openbsd",
277 target_os = "dragonfly"
278 ));
279 assert_eq!(
280 default_flavour(),
281 if bsd { Flavour::Bsd } else { Flavour::Gnu }
282 );
283 }
284}