use std::ffi::OsString;
use std::io::Write;
use super::messages::{self as m, printf};
pub(super) const MAX_NAME: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum OpMode {
Compress,
Decompress,
Test,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SrcMode {
I2O,
F2O,
F2F,
}
#[derive(Debug)]
pub(super) struct Settings {
pub(super) op: OpMode,
pub(super) src: SrcMode,
pub(super) verbosity: u32,
pub(super) keep: bool,
pub(super) small: bool,
pub(super) force: bool,
pub(super) noisy: bool,
pub(super) level: u32,
pub(super) longest: usize,
pub(super) files: Vec<Vec<u8>>,
}
pub(super) enum Parsed {
Run(Settings),
Exit(u8),
}
pub(super) fn os_bytes(s: &OsString) -> Vec<u8> {
s.as_encoded_bytes().to_vec()
}
fn env_words(value: &[u8], out: &mut Vec<Vec<u8>>) {
let space = |b: &u8| matches!(b, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r');
for word in value.split(space).filter(|w| !w.is_empty()) {
out.push(word[..word.len().min(MAX_NAME)].to_vec());
}
}
pub(super) fn parse(
prog: &str,
args: &[OsString],
env: &dyn Fn(&str) -> Option<OsString>,
err: &mut dyn Write,
) -> Parsed {
let mut list: Vec<Vec<u8>> = Vec::new();
for var in ["BZIP2", "BZIP"] {
if let Some(v) = env(var) {
env_words(&os_bytes(&v), &mut list);
}
}
list.extend(args.iter().map(os_bytes));
let mut files = Vec::new();
let mut longest = 7;
let mut decode = true;
for a in &list {
if a == b"--" {
decode = false;
continue;
}
if a.first() == Some(&b'-') && decode {
continue;
}
longest = longest.max(a.len());
files.push(a.clone());
}
let mut s = Settings {
op: OpMode::Compress,
src: if files.is_empty() {
SrcMode::I2O
} else {
SrcMode::F2F
},
verbosity: 0,
keep: false,
small: false,
force: false,
noisy: true,
level: 9,
longest,
files,
};
match prog {
"bunzip2" => s.op = OpMode::Decompress,
"bzcat" => {
s.op = OpMode::Decompress;
s.src = if s.files.is_empty() {
SrcMode::I2O
} else {
SrcMode::F2O
};
}
_ => {}
}
let p = prog.as_bytes();
let usage = |err: &mut dyn Write| printf(err, m::USAGE, &[m::BZLIB_VERSION.as_bytes(), p]);
let license = |err: &mut dyn Write| {
let _ = err.write_all(m::LICENSE_FIRST_LINE.as_bytes());
let _ = err.write_all(m::LICENSE_REST.as_bytes());
};
for a in &list {
if a == b"--" {
break;
}
if a.first() != Some(&b'-') || a.get(1) == Some(&b'-') {
continue;
}
for &c in &a[1..] {
match c {
b'c' => s.src = SrcMode::F2O,
b'd' => s.op = OpMode::Decompress,
b'z' => s.op = OpMode::Compress,
b'f' => s.force = true,
b't' => s.op = OpMode::Test,
b'k' => s.keep = true,
b's' => s.small = true,
b'q' => s.noisy = false,
b'1'..=b'9' => s.level = u32::from(c - b'0'),
b'V' | b'L' => license(err),
b'v' => s.verbosity += 1,
b'h' => {
usage(err);
return Parsed::Exit(0);
}
_ => {
printf(err, m::BAD_FLAG, &[p, a]);
usage(err);
return Parsed::Exit(1);
}
}
}
}
for a in &list {
match a.as_slice() {
b"--" => break,
b"--stdout" => s.src = SrcMode::F2O,
b"--decompress" => s.op = OpMode::Decompress,
b"--compress" => s.op = OpMode::Compress,
b"--force" => s.force = true,
b"--test" => s.op = OpMode::Test,
b"--keep" => s.keep = true,
b"--small" => s.small = true,
b"--quiet" => s.noisy = false,
b"--version" | b"--license" => license(err),
b"--exponential" => {}
b"--repetitive-best" | b"--repetitive-fast" => printf(err, m::REDUNDANT, &[p, a]),
b"--fast" => s.level = 1,
b"--best" => s.level = 9,
b"--verbose" => s.verbosity += 1,
b"--help" => {
usage(err);
return Parsed::Exit(0);
}
x if x.starts_with(b"--") => {
printf(err, m::BAD_FLAG, &[p, a]);
usage(err);
return Parsed::Exit(1);
}
_ => {}
}
}
s.verbosity = s.verbosity.min(4);
if s.op == OpMode::Compress && s.small && s.level > 2 {
s.level = 2;
}
if s.op == OpMode::Test && s.src == SrcMode::F2O {
printf(err, m::C_AND_T, &[p]);
return Parsed::Exit(1);
}
if s.src == SrcMode::F2O && s.files.is_empty() {
s.src = SrcMode::I2O;
}
Parsed::Run(s)
}
#[cfg(test)]
mod tests {
use super::*;
fn run(prog: &str, args: &[&str], env: &[(&str, &str)]) -> (Option<Settings>, u8, String) {
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let env: Vec<(String, String)> = env
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let lookup = move |k: &str| {
env.iter()
.find(|(n, _)| n == k)
.map(|(_, v)| OsString::from(v))
};
let mut err = Vec::new();
match parse(prog, &args, &lookup, &mut err) {
Parsed::Run(s) => (Some(s), 0, String::from_utf8(err).unwrap()),
Parsed::Exit(c) => (None, c, String::from_utf8(err).unwrap()),
}
}
#[test]
fn modes_follow_the_name_then_the_flags() {
let (s, ..) = run("bzip2", &["a"], &[]);
let s = s.unwrap();
assert_eq!((s.op, s.src, s.level), (OpMode::Compress, SrcMode::F2F, 9));
let s = run("bunzip2", &[], &[]).0.unwrap();
assert_eq!((s.op, s.src), (OpMode::Decompress, SrcMode::I2O));
let s = run("bzcat", &["a", "b"], &[]).0.unwrap();
assert_eq!((s.op, s.src), (OpMode::Decompress, SrcMode::F2O));
let s = run("bzcat", &["-z", "a"], &[]).0.unwrap();
assert_eq!((s.op, s.src), (OpMode::Compress, SrcMode::F2O));
}
#[test]
fn clusters_double_dash_and_the_bare_dash() {
let s = run("bzip2", &["-dkv4", "-", "--", "-x"], &[]).0.unwrap();
assert_eq!(s.op, OpMode::Decompress);
assert!(s.keep);
assert_eq!(s.verbosity, 1);
assert_eq!(s.files, vec![b"-x".to_vec()]);
}
#[test]
fn small_caps_the_compression_level() {
assert_eq!(run("bzip2", &["-s9", "a"], &[]).0.unwrap().level, 2);
assert_eq!(run("bzip2", &["-s1", "a"], &[]).0.unwrap().level, 1);
}
#[test]
fn environment_words_come_first_and_may_be_operands() {
let s = run("bzip2", &["-1"], &[("BZIP2", " -9\t-v x "), ("BZIP", "-k")])
.0
.unwrap();
assert_eq!(s.level, 1);
assert!(s.keep);
assert_eq!(s.verbosity, 1);
assert_eq!(s.files, vec![b"x".to_vec()]);
}
#[test]
fn help_bad_flags_and_c_with_t_stop() {
let (s, code, err) = run("bzip2", &["-h"], &[]);
assert!(s.is_none());
assert_eq!(code, 0);
assert!(err.contains("usage: bzip2 [flags"));
let (_, code, err) = run("bzip2", &["-x"], &[]);
assert_eq!(code, 1);
assert!(err.starts_with("bzip2: Bad flag `-x'\n"));
let (_, code, err) = run("bzip2", &["--nope"], &[]);
assert_eq!(code, 1);
assert!(err.starts_with("bzip2: Bad flag `--nope'\n"));
let (_, code, err) = run("bzcat", &["-t", "a"], &[]);
assert_eq!(code, 1);
assert_eq!(
err,
"bzip2: -c and -t cannot be used together.\n".replace("bzip2", "bzcat")
);
}
#[test]
fn verbosity_is_capped_at_four() {
assert_eq!(run("bzip2", &["-vvvvvvv"], &[]).0.unwrap().verbosity, 4);
}
}