use std::fs::{self, File, Metadata};
use std::io::{self, Read, Seek, SeekFrom, Write};
use stuffr::lz4_opts::{
EncodeOptions, Encoder, FRAME_CHUNK, LEGACY_BLOCK_SIZE, parse_frame_header,
};
use super::cli::{self, Command, Op, Plan, STDINMARK, Target};
use super::decode;
use super::display::{Display, Timer};
use super::messages::{self as m, printf};
use crate::compat::host::{self, Host, Kind};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Stats {
pub read: u64,
pub written: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Fail {
File,
Exit(i32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Ended {
pub code: i32,
pub pause: bool,
}
const READ_SIZE: usize = 64 << 10;
const WALK_DEPTH_MAX: usize = 256;
enum Origin {
File(host::InFile),
Stdin(File),
Stream(Box<dyn Read>),
}
struct Reader {
origin: Origin,
eof: bool,
failed: bool,
}
impl Reader {
fn read(&mut self, buf: &mut [u8]) -> usize {
while !self.eof {
let r = match &mut self.origin {
Origin::File(f) => f.read(buf),
Origin::Stdin(f) => f.read(buf),
Origin::Stream(r) => r.read(buf),
};
match r {
Ok(0) => self.eof = true,
Ok(n) => return n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(_) => {
self.eof = true;
self.failed = true;
}
}
}
0
}
fn seek_on(&mut self, n: u64) -> io::Result<u64> {
let off = i64::try_from(n).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
match &mut self.origin {
Origin::File(f) => f.seek(SeekFrom::Current(off)),
Origin::Stdin(f) => f.seek(SeekFrom::Current(off)),
Origin::Stream(_) => Err(io::Error::from(io::ErrorKind::Unsupported)),
}
}
}
pub(super) struct Src {
reader: Reader,
buf: Vec<u8>,
at: usize,
taken: u64,
pub(super) seekable: bool,
}
impl Src {
fn new(origin: Origin, seekable: bool) -> Src {
Src {
reader: Reader {
origin,
eof: false,
failed: false,
},
buf: Vec::new(),
at: 0,
taken: 0,
seekable,
}
}
pub(super) fn fill(&mut self, want: usize) -> &[u8] {
if self.buf.len() - self.at < want && !self.reader.eof {
self.buf.drain(..self.at);
self.at = 0;
while self.buf.len() < want && !self.reader.eof {
let have = self.buf.len();
self.buf.resize(have + READ_SIZE.max(want - have), 0);
let n = self.reader.read(&mut self.buf[have..]);
self.buf.truncate(have + n);
}
}
&self.buf[self.at..]
}
pub(super) fn consume(&mut self, n: usize) {
let n = n.min(self.buf.len() - self.at);
self.at += n;
self.taken += n as u64;
}
pub(super) fn taken(&self) -> u64 {
self.taken
}
fn byte(&mut self) -> Option<u8> {
let b = *self.fill(1).first()?;
self.consume(1);
Some(b)
}
fn read_full(&mut self, out: &mut [u8]) -> usize {
let mut n = 0;
let have = (self.buf.len() - self.at).min(out.len());
out[..have].copy_from_slice(&self.buf[self.at..self.at + have]);
self.consume(have);
n += have;
while n < out.len() {
let k = self.reader.read(&mut out[n..]);
if k == 0 {
break;
}
n += k;
self.taken += k as u64;
}
n
}
pub(super) fn skip(&mut self, n: u64) -> bool {
let buffered = (self.buf.len() - self.at) as u64;
if n <= buffered {
self.consume(n as usize);
return true;
}
let rest = n - buffered;
self.consume(buffered as usize);
if self.seekable && self.reader.seek_on(rest).is_ok() {
self.taken += rest;
return true;
}
let mut left = rest;
let mut scratch = vec![0u8; READ_SIZE];
while left > 0 {
let want = left.min(READ_SIZE as u64) as usize;
let k = self.reader.read(&mut scratch[..want]);
if k == 0 {
return false;
}
left -= k as u64;
self.taken += k as u64;
}
true
}
}
enum SinkKind {
Stdout(Option<File>),
Null,
File(io::BufWriter<Box<dyn Write + Send>>),
SparseFile(File),
}
pub(super) struct Sink {
kind: SinkKind,
sparse: bool,
skips: u64,
write_error: (&'static str, i32),
fd: Option<host::OutFd>,
}
fn espipe() -> io::Error {
#[cfg(unix)]
{
io::Error::from_raw_os_error(libc::ESPIPE)
}
#[cfg(not(unix))]
{
io::Error::from(io::ErrorKind::Unsupported)
}
}
impl Sink {
fn raw(&mut self, out: &mut dyn Write, data: &[u8]) -> io::Result<()> {
match &mut self.kind {
SinkKind::Stdout(None) => out.write_all(data),
SinkKind::Stdout(Some(f)) | SinkKind::SparseFile(f) => f.write_all(data),
SinkKind::File(w) => w.write_all(data),
SinkKind::Null => Ok(()),
}
}
fn seek_on(&mut self, out: &mut dyn Write, n: u64) -> io::Result<()> {
let off = i64::try_from(n).unwrap_or(i64::MAX);
match &mut self.kind {
SinkKind::Stdout(Some(f)) | SinkKind::SparseFile(f) => {
f.seek(SeekFrom::Current(off)).map(drop)
}
SinkKind::Stdout(None) => {
let _ = out.flush();
Err(espipe())
}
SinkKind::File(w) => {
let zeros = [0u8; 4096];
let mut left = n;
while left > 0 {
let k = left.min(zeros.len() as u64) as usize;
w.write_all(&zeros[..k])?;
left -= k as u64;
}
Ok(())
}
SinkKind::Null => Ok(()),
}
}
pub(super) fn write(&mut self, run: &mut Run, data: &[u8]) -> Result<(), Fail> {
if let SinkKind::Null = self.kind {
return run
.host
.charge(data.len() as u64)
.map_err(|_| run.exm(self.write_error.1, self.write_error.0, &[]));
}
if !self.sparse {
return self
.raw(run.out, data)
.map_err(|_| run.exm(self.write_error.1, self.write_error.0, &[]));
}
const WORD: usize = 8;
const SEGMENT: usize = 32 << 10;
let words = data.len() / WORD * WORD;
for seg in data[..words].chunks(SEGMENT) {
let zeros = seg
.chunks(WORD)
.take_while(|w| w.iter().all(|&b| b == 0))
.count()
* WORD;
self.skips += zeros as u64;
if zeros != seg.len() {
let n = std::mem::take(&mut self.skips);
if let Err(e) = self.seek_on(run.out, n) {
let code = e.raw_os_error().unwrap_or(0).to_string();
return Err(run.exm(
72,
m::E72_SPARSE_SKIP,
&[code.as_bytes(), &host::strerror(&e)],
));
}
if self.raw(run.out, &seg[zeros..]).is_err() {
return Err(run.exm(self.write_error.1, self.write_error.0, &[]));
}
}
}
let tail = &data[words..];
let zeros = tail.iter().take_while(|&&b| b == 0).count();
self.skips += zeros as u64;
if zeros != tail.len() {
let n = std::mem::take(&mut self.skips);
if self.seek_on(run.out, n).is_err() {
return Err(run.exm(74, m::E74_SPARSE_TAIL_SKIP, &[]));
}
if self.raw(run.out, &tail[zeros..]).is_err() {
return Err(run.exm(self.write_error.1, self.write_error.0, &[]));
}
}
Ok(())
}
fn finish(&mut self, run: &mut Run) -> Result<(), Fail> {
if self.skips > 0 {
let n = std::mem::take(&mut self.skips);
if self.seek_on(run.out, n - 1).is_err() || self.raw(run.out, &[0]).is_err() {
return Err(run.exm(69, m::E69_FINAL_SKIP, &[]));
}
}
self.flush(run.out)
.map_err(|_| run.exm(self.write_error.1, self.write_error.0, &[]))
}
fn flush(&mut self, out: &mut dyn Write) -> io::Result<()> {
match &mut self.kind {
SinkKind::Stdout(None) => out.flush(),
SinkKind::Stdout(Some(f)) | SinkKind::SparseFile(f) => f.flush(),
SinkKind::File(w) => w.flush(),
SinkKind::Null => Ok(()),
}
}
}
fn output_mode(input: Option<&Metadata>, decoding: bool, legacy: bool) -> u32 {
if input.is_some_and(Metadata::is_file) && (decoding || !legacy) {
0o600
} else {
0o666
}
}
pub(super) struct Run<'a> {
pub(super) cmd: &'a Command,
pub(super) host: &'a mut Host,
pub(super) out: &'a mut dyn Write,
pub(super) err: &'a mut dyn Write,
pub(super) level: i32,
stdin: Option<Src>,
pub(super) dict: Option<Vec<u8>>,
pub(super) display: Display,
pub(super) timer: Timer,
pub(super) total: u64,
}
impl<'a> Run<'a> {
pub(super) fn new(
cmd: &'a Command,
host: &'a mut Host,
out: &'a mut dyn Write,
err: &'a mut dyn Write,
level: i32,
) -> Run<'a> {
let timer = Timer::start(matches!(host, Host::Real));
Run {
cmd,
host,
out,
err,
level,
stdin: None,
dict: None,
display: Display::new(level),
timer,
total: 0,
}
}
}
impl Run<'_> {
pub(super) fn exm(&mut self, code: i32, fmt: &str, args: &[&[u8]]) -> Fail {
if self.level >= 1 {
printf(self.err, fmt, args);
}
Fail::Exit(code)
}
pub(super) fn say(&mut self, min: i32, fmt: &str, args: &[&[u8]]) {
if self.level >= min {
printf(self.err, fmt, args);
}
}
fn fail(&mut self, fmt: &str, args: &[&[u8]]) -> Fail {
self.say(1, fmt, args);
Fail::File
}
fn take_stdin(&mut self) -> Src {
if let Some(s) = self.stdin.take() {
return s;
}
match self.host {
Host::Real => match host::stdin_file() {
Ok(f) => {
let seekable = host::kind_of(&f) == Kind::Regular;
Src::new(Origin::Stdin(f), seekable)
}
Err(_) => Src::new(Origin::Stream(Box::new(io::stdin())), false),
},
Host::Sandbox(s) => {
let bytes = s.stdin.take().unwrap_or_default();
Src::new(Origin::Stream(Box::new(io::Cursor::new(bytes))), false)
}
}
}
fn stdin_meta(&self) -> Option<Metadata> {
match self.host {
Host::Real => host::stdin_file().ok()?.metadata().ok(),
Host::Sandbox(_) => None,
}
}
fn confirm(&mut self) -> bool {
let mut src = self.take_stdin();
let yes = matches!(src.byte(), Some(b'y' | b'Y'));
if yes {
while !matches!(src.byte(), None | Some(b'\n')) {}
}
self.stdin = Some(src);
yes
}
fn open_input(&mut self, name: &[u8]) -> Result<(Option<Src>, Option<Metadata>), Fail> {
if name == STDINMARK {
self.say(4, m::USING_STDIN, &[]);
return Ok((None, self.stdin_meta()));
}
let path = self.host.path(name);
let f = match File::open(&path) {
Ok(f) => f,
Err(e) => return Err(self.fail(m::OPEN_INPUT_FAILED, &[name, &host::strerror(&e)])),
};
let meta = f.metadata().ok();
if meta.as_ref().is_some_and(Metadata::is_dir) {
return Err(self.fail(m::IS_A_DIRECTORY, &[name]));
}
let seekable = meta.as_ref().is_some_and(Metadata::is_file);
let f = self.host.track(&path, f);
Ok((Some(Src::new(Origin::File(f), seekable)), meta))
}
fn put_back(&mut self, name: &[u8], src: Src) {
if name == STDINMARK {
self.stdin = Some(src);
}
}
fn open_output(
&mut self,
name: &[u8],
input: Option<&Metadata>,
mode: u32,
) -> Result<(File, bool), Fail> {
let path = self.host.path(name);
if host::output_exists(&path) {
if let Some(i) = input
&& let Ok(existing) = fs::metadata(&path)
&& host::same_file(i, &existing) == Some(true)
{
printf(self.err, m::SAME_FILE, &[name]);
return Err(Fail::File);
}
if !self.cmd.force {
if self.level <= 1 {
printf(self.err, m::ALREADY_EXISTS, &[name]);
return Err(Fail::File);
}
printf(self.err, m::OVERWRITE_PROMPT, &[name]);
if !self.confirm() {
printf(self.err, m::NOT_OVERWRITTEN, &[]);
return Err(Fail::File);
}
}
}
host::create_output(&path, mode)
.map_err(|e| self.fail(m::OPEN_OUTPUT_FAILED, &[name, &host::strerror(&e)]))
}
pub(super) fn stdout_note(&mut self) {
self.say(4, m::USING_STDOUT, &[]);
if self.cmd.sparse.is_none() {
self.say(4, m::SPARSE_DISABLED, &[]);
}
}
fn open_sink(
&mut self,
target: &Target,
input: Option<&Metadata>,
decoding: bool,
) -> Result<Sink, Fail> {
let sparse_asked = decoding && self.cmd.sparse != Some(false);
let mut fd = None;
let write_error = if decoding {
(m::E70_WRITE_DECODED, 70)
} else {
(m::E38_WRITE_COMPRESSED, 38)
};
let (kind, sparse) = match target {
Target::Null => (SinkKind::Null, false),
Target::Stdout => {
if !decoding {
self.stdout_note();
}
let sparse = decoding && self.cmd.sparse == Some(true);
let fd = match (&*self.host, sparse) {
(Host::Real, true) => {
let _ = self.out.flush();
stdout_file()
}
_ => None,
};
(SinkKind::Stdout(fd), sparse)
}
Target::File(name) => {
let mode = output_mode(input, decoding, self.cmd.opts.legacy);
let (f, fresh) = self.open_output(name, input, mode)?;
#[cfg(unix)]
let input_mode = {
use std::os::unix::fs::PermissionsExt;
input.map(|m| m.permissions().mode())
};
#[cfg(not(unix))]
let input_mode = None;
fd = host::OutFd::new(&f, fresh && mode == 0o600, input_mode);
match self.host {
Host::Real if sparse_asked => (SinkKind::SparseFile(f), true),
_ => (SinkKind::File(self.host.out_writer(Some(f))), sparse_asked),
}
}
};
Ok(Sink {
kind,
sparse,
skips: 0,
write_error,
fd,
})
}
fn copy_metadata(target: &Target, input: Option<&Metadata>, fd: Option<&mut host::OutFd>) {
let (Target::File(_), Some(meta), Some(fd)) = (target, input, fd) else {
return;
};
let f = fd.file();
if !meta.is_file() || !f.metadata().is_ok_and(|m| m.is_file()) {
return;
}
if let Ok(t) = meta.modified() {
let _ = f.set_times(fs::FileTimes::new().set_modified(t));
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let _ = std::os::unix::fs::fchown(f, Some(meta.uid()), Some(meta.gid()));
}
let _ = f.set_permissions(meta.permissions());
fd.settle();
}
fn remove_input(&mut self, name: &[u8]) -> Result<(), Fail> {
if !self.cmd.remove {
return Ok(());
}
match fs::remove_file(self.host.path(name)) {
Ok(()) => Ok(()),
Err(e) => Err(self.exm(50, m::E50_REMOVE, &[name, &host::strerror(&e)])),
}
}
fn load_dictionary(&mut self) -> Result<(), Fail> {
let Some(name) = self.cmd.dict_file.clone() else {
return Ok(());
};
let path = self.host.path(&name);
let f = match File::open(&path) {
Ok(f) => f,
Err(e) => {
self.say(1, m::OPEN_INPUT_FAILED, &[&name, &host::strerror(&e)]);
return Err(self.exm(27, m::E27_DICTIONARY, &[]));
}
};
if f.metadata().is_ok_and(|m| m.is_dir()) {
self.say(1, m::IS_A_DIRECTORY, &[&name]);
return Err(self.exm(27, m::E27_DICTIONARY, &[]));
}
const KEEP: usize = 64 << 10;
let mut f = self.host.track(&path, f);
let mut dict = Vec::new();
let mut buf = vec![0u8; READ_SIZE];
loop {
match f.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
dict.extend_from_slice(&buf[..n]);
let excess = dict.len().saturating_sub(KEEP);
dict.drain(..excess);
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(_) => return Err(self.exm(27, m::E27_DICTIONARY, &[])),
}
}
self.dict = Some(dict);
Ok(())
}
pub(super) fn expand(&self, names: &[Vec<u8>]) -> Vec<Vec<u8>> {
if !self.cmd.recursive {
return names.to_vec();
}
let mut out = Vec::new();
for name in names {
let is_dir = name.as_slice() != STDINMARK
&& fs::metadata(self.host.path(name)).is_ok_and(|m| m.is_dir());
if is_dir {
self.walk(name, 0, &mut out);
} else {
out.push(name.clone());
}
}
if out.is_empty() {
return names.to_vec();
}
out
}
fn walk(&self, dir: &[u8], depth: usize, out: &mut Vec<Vec<u8>>) {
if depth >= WALK_DEPTH_MAX {
return;
}
let Ok(entries) = fs::read_dir(self.host.path(dir)) else {
return;
};
for entry in entries.flatten() {
let mut child = dir.to_vec();
child.push(b'/');
child.extend_from_slice(entry.file_name().as_encoded_bytes());
if fs::metadata(self.host.path(&child)).is_ok_and(|m| m.is_dir()) {
self.walk(&child, depth + 1, out);
} else {
out.push(child);
}
}
}
fn compress_one(&mut self, name: &[u8], target: &Target) -> Result<Stats, Fail> {
let (src, meta) = self.open_input(name)?;
let (r, mut fd) = match self.open_sink(target, meta.as_ref(), false) {
Ok(mut sink) => {
let mut src = src.unwrap_or_else(|| self.take_stdin());
let r = self.compress_from(name, &mut src, meta.as_ref(), &mut sink);
self.put_back(name, src);
(r, sink.fd.take())
}
Err(f) => (Err(f), None),
};
let stats = r?;
if !self.cmd.opts.legacy {
Self::copy_metadata(target, meta.as_ref(), fd.as_mut());
}
drop(fd);
self.total += stats.read;
self.show_compress_summary(&stats);
self.remove_input(name)?;
Ok(stats)
}
fn compress_from(
&mut self,
name: &[u8],
src: &mut Src,
meta: Option<&Metadata>,
sink: &mut Sink,
) -> Result<Stats, Fail> {
let opts = EncodeOptions {
content_size: meta
.filter(|m| self.cmd.content_size && m.is_file())
.map(Metadata::len),
dictionary: self.dict.clone(),
..self.cmd.opts.clone()
};
let legacy = opts.legacy;
if self.cmd.content_size && !legacy && opts.content_size.is_none() {
self.say(3, m::CONTENT_SIZE_UNKNOWN, &[]);
}
let Ok(mut enc) = Encoder::new(&opts) else {
return Err(self.exm(38, m::E38_WRITE_COMPRESSED, &[]));
};
let chunk = if legacy {
LEGACY_BLOCK_SIZE
} else {
FRAME_CHUNK
};
let mut buf = vec![0u8; chunk];
let mut packed = Vec::new();
let mut stats = Stats::default();
let mut first = true;
let mut before = 0u64;
let mut streamed = false;
let (mut header, mut trailer) = (0u64, 0u64);
let mut last_job = None;
loop {
let n = src.read_full(&mut buf);
if n == 0 && first && !legacy && src.reader.failed {
return Err(self.exm(
40,
m::E40_FIRST_CHUNK,
&[chunk.to_string().as_bytes(), name],
));
}
first = false;
if n == 0 {
break;
}
stats.read += n as u64;
if enc.update(&buf[..n], &mut packed).is_err() {
return Err(self.exm(38, m::E38_WRITE_COMPRESSED, &[]));
}
sink.write(self, &packed)?;
stats.written += packed.len() as u64;
if !legacy && !streamed && n == chunk {
streamed = true;
let parsed = parse_frame_header(&packed);
debug_assert!(
matches!(parsed, Ok(Some(_))),
"the first job of a streamed frame opens with its header"
);
if let Ok(Some(h)) = parsed {
header = h.header_len as u64;
trailer = if h.content_checksum { 8 } else { 4 };
}
}
packed.clear();
if legacy || streamed {
if n == chunk {
let blocks = stats.written.saturating_sub(header);
self.show_read_progress_at(before, blocks, false);
} else {
last_job = Some(before);
}
}
before = stats.read;
if n < chunk {
break;
}
}
if enc.finish(&mut packed).is_err() {
return Err(self.exm(38, m::E38_WRITE_COMPRESSED, &[]));
}
sink.write(self, &packed)?;
stats.written += packed.len() as u64;
if let Some(before) = last_job {
let blocks = stats.written.saturating_sub(header + trailer);
self.show_read_progress_at(before, blocks, false);
} else if !legacy && !streamed {
self.show_read_progress_at(stats.read, stats.written, true);
}
sink.finish(self)?;
Ok(stats)
}
fn decompress_one(&mut self, name: &[u8], target: &Target) -> Result<(Stats, bool), Fail> {
let pre = if name == STDINMARK {
self.stdin_meta()
} else {
fs::metadata(self.host.path(name)).ok()
};
let mut sink = self.open_sink(target, pre.as_ref(), true)?;
let (src, meta) = match self.open_input(name) {
Ok(v) => v,
Err(f) => {
let _ = sink.flush(self.out);
Self::copy_metadata(target, pre.as_ref(), sink.fd.as_mut());
return Err(f);
}
};
let mut src = src.unwrap_or_else(|| self.take_stdin());
let passthrough = self.cmd.force && *target == Target::Stdout;
let r = decode::stream(self, &mut src, &mut sink, passthrough);
self.put_back(name, src);
let decoded = match r {
Ok(d) => d,
Err(f) => {
let _ = sink.flush(self.out);
return Err(f);
}
};
sink.finish(self)?;
let mut fd = sink.fd.take();
drop(sink);
let stats = Stats {
read: decoded.read,
written: decoded.written,
};
Self::copy_metadata(target, meta.as_ref(), fd.as_mut());
drop(fd);
self.total += stats.written;
self.show_decode_summary(name, &stats);
self.remove_input(name)?;
Ok((stats, !decoded.undecodable))
}
}
fn stdout_file() -> Option<File> {
#[cfg(unix)]
{
use std::os::fd::AsFd;
io::stdout()
.as_fd()
.try_clone_to_owned()
.ok()
.map(File::from)
}
#[cfg(not(unix))]
{
None
}
}
fn run_level(cmd: &Command, plan: &Plan) -> i32 {
if cmd.display_level == cli::DISPLAY_LEVEL_DEFAULT
&& (plan.target == Target::Stdout || cmd.multiple)
{
1
} else {
cmd.display_level
}
}
pub(crate) fn compress_files(
cmd: &Command,
plan: &Plan,
host: &mut Host,
out: &mut dyn Write,
err: &mut dyn Write,
) -> Ended {
run_files(cmd, plan, host, out, err)
}
pub(crate) fn decompress_files(
cmd: &Command,
plan: &Plan,
host: &mut Host,
out: &mut dyn Write,
err: &mut dyn Write,
) -> Ended {
run_files(cmd, plan, host, out, err)
}
fn run_files(
cmd: &Command,
plan: &Plan,
host: &mut Host,
out: &mut dyn Write,
err: &mut dyn Write,
) -> Ended {
let mut run = Run::new(cmd, host, out, err, run_level(cmd, plan));
match files(&mut run, plan) {
Ok(failed) => Ended {
code: i32::try_from(failed).unwrap_or(i32::MAX),
pause: cmd.pause,
},
Err(code) => Ended { code, pause: false },
}
}
fn files(run: &mut Run, plan: &Plan) -> Result<usize, i32> {
let cmd = run.cmd;
let one = |run: &mut Run, name: &[u8], target: &Target| -> Result<bool, Fail> {
match plan.op {
Op::Compress => run.compress_one(name, target).map(|_| true),
Op::Decompress | Op::Test => run.decompress_one(name, target).map(|(_, ok)| ok),
Op::List => Ok(false),
}
};
let tally = |failed: &mut usize, r: Result<bool, Fail>| -> Result<(), i32> {
match r {
Ok(true) => Ok(()),
Ok(false) | Err(Fail::File) => {
*failed += 1;
Ok(())
}
Err(Fail::Exit(code)) => Err(code),
}
};
let mut failed = 0usize;
let done = |run: &mut Run, failed: usize| {
if plan.op == Op::Compress || cmd.from_list || failed > 0 {
run.show_done();
}
};
if !cmd.from_list {
let name = cmd.inputs.first().map_or(STDINMARK, Vec::as_slice);
if plan.derived
&& let Target::File(out_name) = &plan.target
{
let fmt = if plan.op == Op::Compress {
m::COMPRESSED_FILENAME
} else {
m::DECODING_FILE
};
run.say(2, fmt, &[out_name]);
}
run.announce_run(plan, &[]);
if let Err(Fail::Exit(code)) = run.load_dictionary() {
return Err(code);
}
let r = one(run, name, &plan.target);
tally(&mut failed, r)?;
done(run, failed);
return Ok(failed);
}
let names = run.expand(&cmd.inputs);
run.announce_run(plan, &names);
if let Err(Fail::Exit(code)) = run.load_dictionary() {
return Err(code);
}
for name in names {
let target = if plan.target == Target::Stdout {
Some(Target::Stdout)
} else {
cli::list_target(cmd, plan.op, &name)
};
let Some(target) = target else {
run.say(1, m::EXTENSION_MISMATCH, &[&name]);
failed += 1;
continue;
};
let r = one(run, &name, &target);
tally(&mut failed, r)?;
}
done(run, failed);
Ok(failed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_frame_from_a_regular_file_is_created_owner_only() {
let dir = std::env::temp_dir().join(format!("stuffr-lz4-mode-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let file = dir.join("a");
fs::write(&file, b"x").unwrap();
let reg = fs::metadata(&file).unwrap();
let dirm = fs::metadata(&dir).unwrap();
assert_eq!(output_mode(Some(®), false, false), 0o600);
assert_eq!(output_mode(Some(®), true, false), 0o600);
assert_eq!(output_mode(Some(®), true, true), 0o600);
assert_eq!(output_mode(Some(®), false, true), 0o666);
assert_eq!(output_mode(None, false, false), 0o666);
assert_eq!(output_mode(Some(&dirm), true, false), 0o666);
let _ = fs::remove_dir_all(&dir);
}
}