use std::ffi::OsString;
use std::io::Write;
use stuffr::lz4_opts::{BlockSizeId, EncodeOptions};
use super::messages::{self as m, printf};
use crate::compat::host::Cores;
pub(crate) const STDINMARK: &[u8] = b"stdin";
const STDOUTMARK: &[u8] = b"stdout";
const NULMARK: &[u8] = b"null";
const DEVNULL: &[u8] = b"/dev/null";
pub(crate) const EXTENSION: &[u8] = b".lz4";
pub(crate) const DISPLAY_LEVEL_DEFAULT: i32 = 2;
pub(crate) const THREADS_MAX: u32 = 200;
const BLOCK_BYTES_MIN: u32 = 32;
const BLOCK_BYTES_MAX: u32 = 4 << 20;
const LEVEL_BEST: i32 = 12;
const LEVEL_LEGACY_HIGH: i32 = 9;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
Auto,
Compress,
Decompress,
Test,
List,
Bench,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Output {
Auto,
Stdout,
Null,
File(Vec<u8>),
}
#[derive(Debug, Clone)]
#[allow(
dead_code,
reason = "keep and legacy_cli are parse results the run does not need"
)]
pub(crate) struct Command {
pub program: Vec<u8>,
pub mode: Mode,
pub opts: EncodeOptions,
pub inputs: Vec<Vec<u8>>,
pub output: Output,
pub multiple: bool,
pub from_list: bool,
pub recursive: bool,
pub force: bool,
pub force_stdout: bool,
pub keep: bool,
pub remove: bool,
pub sparse: Option<bool>,
pub content_size: bool,
pub display_level: i32,
pub threads: u32,
pub threads_reduced_from: Option<u32>,
pub dict_file: Option<Vec<u8>>,
pub legacy_cli: bool,
pub pause: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Stop {
pub code: i32,
pub pause: bool,
}
impl Stop {
const USAGE: Stop = Stop {
code: 1,
pause: false,
};
}
pub(crate) fn program_name(argv0: &[u8]) -> &[u8] {
match argv0.iter().rposition(|&b| b == b'/' || b == b'\\') {
Some(at) => &argv0[at + 1..],
None => argv0,
}
}
pub(crate) fn name_matches(program: &[u8], name: &str) -> bool {
match program.strip_prefix(name.as_bytes()) {
Some(rest) => rest.is_empty() || rest[0] == b'.',
None => false,
}
}
fn at(s: &[u8], p: usize) -> u8 {
s.get(p).copied().unwrap_or(0)
}
fn read_u32(s: &[u8], p: &mut usize) -> u32 {
let mut n: u32 = 0;
while at(s, *p).is_ascii_digit() {
n = n.wrapping_mul(10).wrapping_add(u32::from(at(s, *p) - b'0'));
*p += 1;
}
let shift = match at(s, *p) {
b'K' => 10,
b'M' => 20,
_ => return n,
};
n = n.wrapping_shl(shift);
*p += 1;
if at(s, *p) == b'i' {
*p += 1;
}
if at(s, *p) == b'B' {
*p += 1;
}
n
}
fn read_env(env: &dyn Fn(&str) -> Option<OsString>, var: &str, err: &mut dyn Write) -> Option<u32> {
let v = env(var)?;
let s = v.as_encoded_bytes();
if !at(s, 0).is_ascii_digit() {
printf(err, m::ENV_IGNORED, &[var.as_bytes(), s]);
return None;
}
Some(read_u32(s, &mut 0))
}
pub(crate) fn resolve_threads(requested: u32, cores: Cores) -> (u32, Option<u32>) {
let n = if requested == 0 {
let logical = cores.logical;
(logical - logical / 8).saturating_sub(1).max(1)
} else {
requested
};
if n > THREADS_MAX {
(THREADS_MAX, Some(n))
} else {
(n, None)
}
}
pub(crate) fn print_banner(w: &mut dyn Write) {
printf(w, m::BANNER, &[env!("CARGO_PKG_VERSION").as_bytes()]);
}
fn print_help(err: &mut dyn Write, program: &[u8], legacy: bool, long: bool) {
print_banner(err);
printf(err, m::USAGE, &[program]);
printf(err, m::HELP_REST, &[]);
if legacy {
printf(err, m::HELP_LEGACY, &[]);
}
if long {
printf(err, m::LONG_HELP, &[program; 6]);
if legacy {
printf(err, m::LONG_HELP_LEGACY, &[program; 2]);
}
}
}
pub(crate) fn bad_usage(err: &mut dyn Write, program: &[u8], display_level: i32) -> Stop {
if display_level >= 1 {
printf(err, m::INCORRECT_PARAMETERS, &[]);
printf(err, m::USAGE, &[program]);
}
Stop::USAGE
}
fn fail(err: &mut dyn Write, display_level: i32, fmt: &str, args: &[&[u8]]) -> Stop {
if display_level >= 1 {
printf(err, fmt, args);
}
Stop::USAGE
}
struct State<'a> {
program: &'a [u8],
mode: Mode,
opts: EncodeOptions,
input: Option<Vec<u8>>,
output: Output,
list: Vec<Vec<u8>>,
multiple: bool,
recursive: bool,
force: bool,
force_stdout: bool,
remove: bool,
sparse: Option<bool>,
content_size: bool,
display_level: i32,
threads: u32,
dict_file: Option<Vec<u8>>,
legacy_cli: bool,
pause: bool,
bench: bool,
}
impl State<'_> {
fn operand(&mut self, a: &[u8], err: &mut dyn Write) -> Result<(), Stop> {
if self.multiple {
self.list.push(a.to_vec());
} else if self.input.is_none() {
self.input = Some(a.to_vec());
} else if self.output == Output::Auto {
self.output = match a {
STDOUTMARK => Output::Stdout,
NULMARK | DEVNULL => Output::Null,
_ => Output::File(a.to_vec()),
};
} else {
return Err(fail(err, self.display_level, m::WONT_BE_USED, &[a]));
}
Ok(())
}
fn set_stdout(&mut self) {
self.force_stdout = true;
self.output = Output::Stdout;
}
fn block_size(&mut self, n: u32, err: &mut dyn Write) -> Result<(), Stop> {
let (shown, unit) = if let Some(id) = u8::try_from(n).ok().and_then(BlockSizeId::from_id) {
self.opts.block_size = id;
self.opts.block_bytes = None;
(id.bytes() as u32 >> 10, m::BLOCKS_KB)
} else if n >= BLOCK_BYTES_MIN {
let bytes = n.min(BLOCK_BYTES_MAX);
self.opts.block_bytes = Some(bytes);
if bytes >= 1024 {
(bytes >> 10, m::BLOCKS_KB)
} else {
(bytes, m::BLOCKS_BYTES)
}
} else {
return Err(bad_usage(err, self.program, self.display_level));
};
if self.display_level >= 2 {
printf(err, unit, &[shown.to_string().as_bytes()]);
}
Ok(())
}
}
fn next_field<'a>(
args: &'a [OsString],
i: &mut usize,
st: &State,
err: &mut dyn Write,
) -> Result<&'a [u8], Stop> {
*i += 1;
let Some(next) = args.get(*i) else {
return Err(fail(err, st.display_level, m::MISSING_ARGUMENT, &[]));
};
let next = next.as_encoded_bytes();
if at(next, 0) == b'-' {
return Err(fail(err, st.display_level, m::SEPARATED_ARGUMENT, &[]));
}
Ok(next)
}
fn long_option(
a: &[u8],
args: &[OsString],
i: &mut usize,
st: &mut State,
out: &mut dyn Write,
err: &mut dyn Write,
) -> Result<(), Stop> {
match a {
b"--compress" => st.mode = Mode::Compress,
b"--decompress" | b"--uncompress" => st.mode = Mode::Decompress,
b"--test" => st.mode = Mode::Test,
b"--list" => {
st.mode = Mode::List;
st.multiple = true;
}
b"--favor-decSpeed" => st.opts.favor_dec_speed = true,
b"--force" => st.force = true,
b"--no-force" => st.force = false,
b"--stdout" | b"--to-stdout" => st.set_stdout(),
b"--multiple" => st.multiple = true,
b"--frame-crc" => st.opts.content_checksum = true,
b"--no-frame-crc" => st.opts.content_checksum = false,
b"--no-crc" => {
st.opts.content_checksum = false;
st.opts.block_checksum = false;
}
b"--content-size" => st.content_size = true,
b"--no-content-size" => st.content_size = false,
b"--sparse" => st.sparse = Some(true),
b"--no-sparse" => st.sparse = Some(false),
b"--verbose" => st.display_level += 1,
b"--quiet" => st.display_level -= 1,
b"--help" => {
print_help(err, st.program, st.legacy_cli, false);
return Err(Stop {
code: 0,
pause: st.pause,
});
}
b"--version" => {
print_banner(out);
return Err(Stop {
code: 0,
pause: st.pause,
});
}
b"--keep" => st.remove = false,
b"--rm" => st.remove = true,
b"--best" => st.opts.level = LEVEL_BEST,
_ => {
if let Some(rest) = a.strip_prefix(b"--fast") {
let n = match rest {
[] => 1,
[b'=', v @ ..] => read_u32(v, &mut 0),
_ => 0,
};
if n == 0 {
return Err(bad_usage(err, st.program, st.display_level));
}
st.opts.level = (n as i32).wrapping_neg();
} else if let Some(rest) = a.strip_prefix(b"--threads") {
let v = match rest {
[b'=', v @ ..] => v,
_ => next_field(args, i, st, err)?,
};
let mut p = 0;
let n = read_u32(v, &mut p);
if p != v.len() {
return Err(fail(err, st.display_level, m::NUMERIC_ONLY, &[]));
}
st.threads = n;
} else {
return Err(bad_usage(err, st.program, st.display_level));
}
}
}
Ok(())
}
fn short_options(
a: &[u8],
args: &[OsString],
i: &mut usize,
st: &mut State,
out: &mut dyn Write,
err: &mut dyn Write,
) -> Result<(), Stop> {
let clean = |st: &State| Stop {
code: 0,
pause: st.pause,
};
let mut p = 1;
while p < a.len() {
if st.legacy_cli {
let level = match &a[p..] {
b"c0" => Some(0),
b"c1" => Some(LEVEL_LEGACY_HIGH),
b"c2" | b"hc" => Some(LEVEL_BEST),
b"y" => {
st.force = true;
return Ok(());
}
_ => None,
};
if let Some(level) = level {
st.opts.level = level;
return Ok(());
}
}
let c = a[p];
if c.is_ascii_digit() {
st.opts.level = read_u32(a, &mut p) as i32;
continue;
}
p += 1;
match c {
b'V' => {
print_banner(out);
return Err(clean(st));
}
b'h' => {
print_help(err, st.program, st.legacy_cli, false);
return Err(clean(st));
}
b'H' => {
print_help(err, st.program, st.legacy_cli, true);
return Err(clean(st));
}
b'z' => st.mode = Mode::Compress,
b'd' => st.mode = Mode::Decompress,
b't' => st.mode = Mode::Test,
b'l' => st.opts.legacy = true,
b'c' => st.set_stdout(),
b'f' => st.force = true,
b'k' => st.remove = false,
b'v' => st.display_level += 1,
b'q' => st.display_level -= 1,
b'm' => st.multiple = true,
b'r' => {
st.recursive = true;
st.multiple = true;
}
b'p' => st.pause = true,
b'S' => {}
b'D' => {
let name = if p < a.len() {
a[p..].to_vec()
} else {
*i += 1;
match args.get(*i) {
Some(next) => next.as_encoded_bytes().to_vec(),
None => return Err(bad_usage(err, st.program, st.display_level)),
}
};
st.dict_file = Some(name);
return Ok(());
}
b'B' => {
loop {
match at(a, p) {
b'D' => st.opts.block_linked = true,
b'I' => st.opts.block_linked = false,
b'X' => st.opts.block_checksum = true,
d if d.is_ascii_digit() => {
let n = read_u32(a, &mut p);
st.block_size(n, err)?;
continue;
}
_ => break,
}
p += 1;
}
}
b'T' => st.threads = read_u32(a, &mut p),
b'b' => {
st.mode = Mode::Bench;
st.bench = true;
if at(a, p).is_ascii_digit() {
read_u32(a, &mut p);
}
}
b'e' | b'i' => {
read_u32(a, &mut p);
}
_ => return Err(bad_usage(err, st.program, st.display_level)),
}
}
Ok(())
}
pub(crate) fn parse(
program: &[u8],
env: &dyn Fn(&str) -> Option<OsString>,
args: &[OsString],
cores: Cores,
out: &mut dyn Write,
err: &mut dyn Write,
) -> Result<Command, Stop> {
let unlz4 = name_matches(program, "unlz4");
let lz4cat = name_matches(program, "lz4cat");
let mut st = State {
program,
mode: if unlz4 || lz4cat {
Mode::Decompress
} else {
Mode::Auto
},
opts: EncodeOptions::default(),
input: None,
output: Output::Auto,
list: Vec::new(),
multiple: lz4cat,
recursive: false,
force: lz4cat,
force_stdout: false,
remove: false,
sparse: None,
content_size: false,
display_level: if lz4cat { 1 } else { DISPLAY_LEVEL_DEFAULT },
threads: 0,
dict_file: None,
legacy_cli: name_matches(program, "lz4c"),
pause: false,
bench: false,
};
if lz4cat {
st.set_stdout();
}
if let Some(level) = read_env(env, "LZ4_CLEVEL", err) {
st.opts.level = level as i32;
}
if let Some(n) = read_env(env, "LZ4_NBWORKERS", err) {
st.threads = n;
}
let mut all_operands = false;
let mut i = 0;
while i < args.len() {
let a = args[i].as_encoded_bytes();
if all_operands || at(a, 0) != b'-' {
st.operand(a, err)?;
} else if a == b"-" {
if st.input.is_none() {
st.input = Some(STDINMARK.to_vec());
} else {
st.output = Output::Stdout;
}
} else if a == b"--" {
all_operands = true;
} else if at(a, 1) == b'-' {
long_option(a, args, &mut i, &mut st, out, err)?;
} else {
short_options(a, args, &mut i, &mut st, out, err)?;
}
i += 1;
}
if st.bench {
if st.display_level >= 1 {
printf(err, m::BENCH_REFUSED, &[]);
}
return Err(Stop::USAGE);
}
let from_list = st.multiple && !st.list.is_empty();
let (inputs, output) = if from_list {
let output = match st.output {
Output::Stdout => Output::Stdout,
Output::Null => Output::Null,
Output::Auto | Output::File(_) => Output::Auto,
};
(std::mem::take(&mut st.list), output)
} else {
let input = match st.input.take() {
Some(name) if !st.multiple => name,
_ => STDINMARK.to_vec(),
};
(vec![input], st.output.clone())
};
let (threads, threads_reduced_from) = resolve_threads(st.threads, cores);
Ok(Command {
program: program.to_vec(),
mode: st.mode,
opts: st.opts,
inputs,
output,
multiple: st.multiple,
from_list,
recursive: st.recursive,
force: st.force,
force_stdout: st.force_stdout,
keep: !st.remove,
remove: st.remove,
sparse: st.sparse,
content_size: st.content_size,
display_level: st.display_level,
threads,
threads_reduced_from,
dict_file: st.dict_file,
legacy_cli: st.legacy_cli,
pause: st.pause,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Op {
Compress,
Decompress,
Test,
List,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Target {
Stdout,
Null,
File(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Plan {
pub op: Op,
pub target: Target,
pub derived: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Refusal {
StdinConsole,
StdoutConsole,
NoOutputName,
}
pub(crate) fn op_for(mode: Mode, first: &[u8]) -> Op {
match mode {
Mode::Auto if first.ends_with(EXTENSION) => Op::Decompress,
Mode::Auto | Mode::Compress | Mode::Bench => Op::Compress,
Mode::Decompress => Op::Decompress,
Mode::Test => Op::Test,
Mode::List => Op::List,
}
}
fn strip_extension(name: &[u8]) -> Option<Vec<u8>> {
name.strip_suffix(EXTENSION)
.filter(|stem| !stem.is_empty())
.map(<[u8]>::to_vec)
}
pub(crate) fn plan(cmd: &Command, stdin_tty: bool, stdout_tty: bool) -> Result<Plan, Refusal> {
let first = cmd.inputs.first().map_or(STDINMARK, Vec::as_slice);
let is_stdin = first == STDINMARK;
if is_stdin && stdin_tty {
return Err(Refusal::StdinConsole);
}
let op = op_for(cmd.mode, first);
let mut derived = false;
let target = match (op, &cmd.output) {
(Op::Test, _) => Target::Null,
(_, Output::Stdout) => Target::Stdout,
(_, Output::Null) => Target::Null,
(_, Output::File(name)) => Target::File(name.clone()),
(_, Output::Auto) if is_stdin => Target::Stdout,
(_, Output::Auto) if cmd.from_list => Target::Null,
(Op::Decompress, Output::Auto) => {
derived = true;
Target::File(strip_extension(first).ok_or(Refusal::NoOutputName)?)
}
(Op::Compress, Output::Auto) => {
derived = true;
Target::File([first, EXTENSION].concat())
}
(Op::List, Output::Auto) => Target::Stdout,
};
if target == Target::Stdout && stdout_tty && !cmd.force_stdout && op != Op::List {
return Err(Refusal::StdoutConsole);
}
Ok(Plan {
op,
target,
derived,
})
}
pub(crate) fn list_target(cmd: &Command, op: Op, name: &[u8]) -> Option<Target> {
match op {
Op::Test => Some(Target::Stdout),
Op::Decompress => match cmd.output {
Output::Stdout | Output::Null => Some(Target::Stdout),
Output::Auto | Output::File(_) => strip_extension(name).map(Target::File),
},
Op::Compress if cmd.output == Output::Stdout => Some(Target::Stdout),
Op::Compress => Some(Target::File([name, EXTENSION].concat())),
Op::List => Some(Target::Stdout),
}
}
pub(crate) fn refuse(err: &mut dyn Write, cmd: &Command, refusal: Refusal) -> i32 {
if cmd.display_level >= 1 {
match refusal {
Refusal::StdinConsole => printf(err, m::STDIN_CONSOLE, &[]),
Refusal::StdoutConsole => printf(err, m::STDOUT_CONSOLE, &[]),
Refusal::NoOutputName => {
printf(err, m::CANNOT_DETERMINE, &[]);
}
}
}
if refusal == Refusal::NoOutputName {
return bad_usage(err, &cmd.program, cmd.display_level).code;
}
1
}
#[cfg(test)]
mod tests;