use std::fs::{self, File, Metadata};
use std::io::{self, BufWriter, Read, Write};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use super::args::{Mode, Settings};
use super::list::Lister;
use super::messages::{self as m, PROGRAM, printf, quote, ratio};
use crate::compat::gzip::member::{Engine, Header, compress_member};
use crate::compat::host::{
Host, InFile, Published, create_temp, publish, same_file, stdin_file, strerror,
};
mod decompress;
const KNOWN_SUFFIXES: [&[u8]; 7] = [b".gz", b".z", b"-gz", b"-z", b"_z", b".tgz", b".taz"];
const OS_UNIX: u8 = 3;
struct Stop;
type Step = Result<(), Stop>;
pub(super) fn execute(mut s: Settings, host: &mut Host, err: &mut dyn Write) -> u8 {
let files = std::mem::take(&mut s.files);
let mut g = Gz {
s,
host,
err,
exit: 0,
stdout: None,
stdin_named: false,
list: Lister::default(),
stale: None,
time_stamp: 0,
descend: None,
};
let run = if files.is_empty() {
g.treat_stdin()
} else {
files.iter().try_for_each(|f| g.treat(f))
};
if run.is_ok() && g.s.mode == Mode::List {
let (verbose, quiet) = (g.s.verbose > 0, g.s.quiet);
let mut list = std::mem::take(&mut g.list);
list.finish(g.stdout_writer(), verbose, quiet, files.len());
g.list = list;
}
g.finish();
g.exit
}
struct Gz<'a> {
s: Settings,
host: &'a mut Host,
err: &'a mut dyn Write,
exit: u8,
stdout: Option<BufWriter<Box<dyn Write + Send>>>,
stdin_named: bool,
list: Lister,
stale: Option<PathBuf>,
time_stamp: i64,
descend: Option<Vec<Vec<u8>>>,
}
struct Counting<'w> {
inner: &'w mut dyn Write,
n: u64,
}
impl Write for Counting<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let k = self.inner.write(buf)?;
self.n += k as u64;
Ok(k)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
struct Packed {
bytes_in: u64,
body: u64,
}
impl Gz<'_> {
fn say(&mut self, fmt: &str, args: &[&[u8]]) {
printf(self.err, fmt, args);
}
fn warn(&mut self, fmt: &str, args: &[&[u8]]) {
if !self.s.quiet {
self.say(fmt, args);
}
if self.exit == 0 {
self.exit = 2;
}
}
fn error(&mut self, fmt: &str, args: &[&[u8]]) {
self.say(fmt, args);
self.exit = 1;
}
fn finish(&mut self) {
if let Some(mut out) = self.stdout.take()
&& let Err(e) = out.flush()
{
self.error(m::IO_FAILED, &[PROGRAM, b"stdout", &strerror(&e)]);
}
}
fn treat_stdin(&mut self) -> Step {
if self.s.mode != Mode::Compress {
return self.decompress_stdin();
}
if !self.s.force && (self.host.stdout_is_tty() || self.s.presume_tty) {
if !self.s.quiet {
self.say(m::NOT_TO_TERMINAL, &[PROGRAM, PROGRAM]);
}
self.exit = 1;
return Err(Stop);
}
let meta = match self.host {
Host::Real => stdin_file().ok().and_then(|f| f.metadata().ok()),
Host::Sandbox(_) => None,
};
let name = (!self.s.no_name && self.stdin_named).then(|| m::STDIN_NAME.to_vec());
let mtime = self.mtime_of(b"stdin", meta.as_ref());
let mut input = self.host.stdin();
let header = self.header(name, mtime);
let level = self.s.level;
let r = pack(&header, level, &mut input, self.stdout_writer());
let p = match r {
Ok(p) => p,
Err(e) => {
self.error(m::IO_FAILED, &[PROGRAM, b"stdout", &strerror(&e)]);
return Err(Stop);
}
};
if self.s.verbose > 0 {
let line = format!("{}\n", ratio(p.bytes_in, p.body));
let _ = self.err.write_all(line.as_bytes());
}
Ok(())
}
fn stdout_writer(&mut self) -> &mut dyn Write {
let host = &*self.host;
self.stdout.get_or_insert_with(|| host.out_writer(None))
}
fn treat(&mut self, name: &[u8]) -> Step {
let mut stack: Vec<std::vec::IntoIter<Vec<u8>>> = Vec::new();
let mut next = name.to_vec();
loop {
self.treat_one(&next)?;
if let Some(entries) = self.descend.take() {
stack.push(entries.into_iter());
}
next = loop {
let Some(level) = stack.last_mut() else {
return Ok(());
};
match level.next() {
Some(child) => break child,
None => {
stack.pop();
}
}
};
}
}
fn treat_one(&mut self, name: &[u8]) -> Step {
if name == b"-" {
return self.treat_stdin();
}
if self.s.mode != Mode::Compress {
return self.treat_compressed(name);
}
let qname = quote(name);
let path = self.host.path(name);
let follow = self.s.force || self.s.to_stdout;
let opened =
open_input(self.host, &path, follow).and_then(|f| f.metadata().map(|md| (f, md)));
let (file, meta) = match opened {
Ok(x) => x,
Err(e) => {
self.error(m::OPEN_FAILED, &[PROGRAM, &qname, &strerror(&e)]);
return Ok(());
}
};
if meta.is_dir() {
drop(file);
if self.s.recursive {
return self.recurse(name);
}
self.warn(m::IS_DIRECTORY, &[PROGRAM, &qname]);
return Ok(());
}
if !self.s.to_stdout && !self.file_checks(&qname, &meta) {
return Ok(());
}
if self.s.to_stdout {
return self.compress_to_stdout(name, file, &meta);
}
self.compress_to_file(name, path, file, &meta);
Ok(())
}
fn file_checks(&mut self, qname: &[u8], meta: &Metadata) -> bool {
if !meta.is_file() {
self.warn(m::NOT_REGULAR, &[PROGRAM, qname]);
return false;
}
let mode = mode_of(meta);
if mode & 0o4000 != 0 {
self.warn(m::SET_USER_ID, &[PROGRAM, qname]);
return false;
}
if mode & 0o1000 != 0 && !self.s.force {
self.warn(m::STICKY, &[PROGRAM, qname]);
return false;
}
let others = other_links(meta);
if others > 0 && !self.s.force {
let s: &[u8] = if others > 1 { b"s" } else { b"" };
let n = others.to_string();
self.warn(m::HAS_LINKS, &[PROGRAM, qname, n.as_bytes(), s]);
return false;
}
true
}
fn recurse(&mut self, name: &[u8]) -> Step {
let entries: Vec<Vec<u8>> = match fs::read_dir(self.host.path(name)) {
Ok(rd) => rd
.filter_map(|e| e.ok())
.map(|e| e.file_name().as_encoded_bytes().to_vec())
.collect(),
Err(e) => {
self.error(m::OPEN_FAILED, &[PROGRAM, "e(name), &strerror(&e)]);
return Ok(());
}
};
let children = entries
.into_iter()
.map(|e| {
let mut child = name.to_vec();
if !child.ends_with(b"/") {
child.push(b'/');
}
child.extend_from_slice(&e);
child
})
.collect();
self.descend = Some(children);
Ok(())
}
fn compress_to_stdout(&mut self, name: &[u8], mut file: InFile, meta: &Metadata) -> Step {
let qname = quote(name);
self.stdin_named = true;
self.verbose_name(&qname);
let header = self.file_header(name, meta);
let level = self.s.level;
let r = pack(&header, level, &mut file, self.stdout_writer());
let p = match r {
Ok(p) => p,
Err(e) => {
self.error(m::IO_FAILED, &[PROGRAM, b"stdout", &strerror(&e)]);
return Err(Stop);
}
};
self.verbose_done(p.bytes_in, p.body, m::STDOUT_NAME);
Ok(())
}
fn compress_to_file(&mut self, name: &[u8], path: PathBuf, mut file: InFile, meta: &Metadata) {
let qname = quote(name);
if !self.s.force
&& let Some(suf) = self.suffix_of(name)
{
let shown = if self.s.recursive {
self.s.verbose > 0
} else {
!self.s.quiet
};
if shown {
self.say(m::HAS_SUFFIX, &[PROGRAM, &qname, &suf]);
}
return;
}
let out_name = [name, &self.s.suffix].concat();
let qout = quote(&out_name);
let out_path = self.host.path(&out_name);
let Some(replace) = self.may_write(&out_path, &qout, &path, meta) else {
return;
};
let (tmp, tmp_file) = match create_temp(&parent_of(&out_path), "gzip") {
Ok(t) => t,
Err(e) => {
self.error(m::OPEN_FAILED, &[PROGRAM, &qout, &strerror(&e)]);
return;
}
};
self.stdin_named = true;
self.verbose_name(&qname);
let header = self.file_header(name, meta);
let level = self.s.level;
let packed = {
let mut w = self.host.out_writer(Some(tmp_file));
pack(&header, level, &mut file, &mut w).and_then(|p| w.flush().map(|()| p))
};
let p = match packed {
Ok(p) => p,
Err(e) => {
let _ = fs::remove_file(&tmp);
self.error(m::IO_FAILED, &[PROGRAM, &qout, &strerror(&e)]);
return;
}
};
if self.commit(&tmp, &out_path, &qout, meta, None, replace, &path, &qname) {
self.verbose_done(p.bytes_in, p.body, &qout);
}
}
fn may_write(
&mut self,
out_path: &Path,
qout: &[u8],
input: &Path,
input_meta: &Metadata,
) -> Option<bool> {
if fs::symlink_metadata(out_path).is_err() {
return Some(false);
}
if self.s.force {
let is_input = fs::metadata(out_path)
.is_ok_and(|o| same_file(&o, input_meta).unwrap_or(out_path == input));
if !is_input && let Err(e) = fs::remove_file(out_path) {
self.error(m::IO_FAILED, &[PROGRAM, qout, &strerror(&e)]);
return None;
}
return Some(true);
}
match File::options().write(true).create_new(true).open(out_path) {
Ok(_) => {
let _ = fs::remove_file(out_path);
return Some(false);
}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
Err(e) => {
self.error(m::IO_FAILED, &[PROGRAM, qout, &strerror(&e)]);
return None;
}
}
self.say(m::ALREADY_EXISTS, &[PROGRAM, qout]);
if self.host.stdin_is_tty() {
self.say(m::ASK_OVERWRITE, &[]);
if ask_yes() {
return Some(true);
}
}
self.say(m::NOT_OVERWRITTEN, &[]);
if self.exit == 0 {
self.exit = 2;
}
None
}
#[allow(clippy::too_many_arguments)]
fn commit(
&mut self,
tmp: &Path,
out_path: &Path,
qout: &[u8],
meta: &Metadata,
mtime: Option<std::time::SystemTime>,
replace: bool,
path: &Path,
qname: &[u8],
) -> bool {
let _ = crate::compat::copy_metadata_with_mtime(meta, tmp, mtime);
#[cfg(unix)]
if mode_of(meta) & 0o1000 != 0 {
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(
tmp,
fs::Permissions::from_mode(mode_of(meta) & 0o7777 & !0o1000),
);
}
match publish(tmp, out_path, replace) {
Ok(Published::Done) => {}
Ok(Published::Exists) => {
self.say(m::ALREADY_EXISTS, &[PROGRAM, qout]);
self.say(m::NOT_OVERWRITTEN, &[]);
if self.exit == 0 {
self.exit = 2;
}
return false;
}
Err(e) => {
let _ = fs::remove_file(tmp);
self.error(m::IO_FAILED, &[PROGRAM, qout, &strerror(&e)]);
return false;
}
}
if !self.s.keep
&& still_the_input(path, meta, out_path)
&& let Err(e) = fs::remove_file(path)
{
self.error(m::IO_FAILED, &[PROGRAM, qname, &strerror(&e)]);
return false;
}
true
}
fn suffix_of(&self, name: &[u8]) -> Option<Vec<u8>> {
let base = name.rsplit(|&b| b == b'/').next().unwrap_or(name);
let custom: &[u8] = &self.s.suffix;
let mut order: Vec<&[u8]> = KNOWN_SUFFIXES.to_vec();
if custom.iter().any(u8::is_ascii_uppercase) {
order.insert(0, custom);
} else {
order.push(custom);
}
order.into_iter().find_map(|suf| {
(base.len() > suf.len() && base[base.len() - suf.len()..].eq_ignore_ascii_case(suf))
.then(|| base[base.len() - suf.len()..].to_vec())
})
}
fn header(&self, name: Option<Vec<u8>>, mtime: u32) -> Header {
Header {
text: false,
mtime,
xfl: match self.s.level {
1 => 4,
9 => 2,
_ => 0,
},
os: OS_UNIX,
extra: None,
name,
comment: None,
hcrc: false,
len: 0,
}
}
fn file_header(&mut self, name: &[u8], meta: &Metadata) -> Header {
let base = name.rsplit(|&b| b == b'/').next().unwrap_or(name);
let fname = (!self.s.no_name).then(|| base.to_vec());
let mtime = self.mtime_of(name, Some(meta));
self.header(fname, mtime)
}
fn mtime_of(&mut self, name: &[u8], meta: Option<&Metadata>) -> u32 {
let Some(meta) = meta.filter(|md| md.is_file() && !self.s.no_time) else {
return 0;
};
let secs = meta
.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs());
match secs.and_then(|s| u32::try_from(s).ok()) {
Some(s) => s,
None => {
self.warn(m::TIMESTAMP_RANGE, &[PROGRAM, "e(name)]);
0
}
}
}
fn verbose_name(&mut self, qname: &[u8]) {
if self.s.verbose > 0 {
self.say(m::VERBOSE_NAME, &[qname]);
}
}
fn verbose_done(&mut self, whole: u64, part: u64, out: &[u8]) {
if self.s.verbose > 0 {
let r = ratio(whole, part);
let _ = self.err.write_all(r.as_bytes());
let tail = if self.s.keep {
m::CREATED
} else {
m::REPLACED_WITH
};
self.say(tail, &[out]);
}
}
}
fn parent_of(out_path: &Path) -> PathBuf {
match out_path.parent() {
Some(d) if !d.as_os_str().is_empty() => d.to_path_buf(),
_ => PathBuf::from("."),
}
}
fn still_the_input(path: &Path, meta: &Metadata, out_path: &Path) -> bool {
if let Ok(link) = fs::symlink_metadata(path)
&& link.file_type().is_symlink()
{
let entry = fs::symlink_metadata(out_path).ok();
return match entry.and_then(|o| same_file(&link, &o)) {
Some(is_output) => !is_output,
None => path != out_path,
};
}
let Ok(now) = fs::metadata(path) else {
return path != out_path;
};
let out = fs::metadata(out_path).ok();
match (same_file(&now, meta), out.and_then(|o| same_file(&now, &o))) {
(Some(was_input), Some(is_output)) => was_input && !is_output,
(Some(was_input), None) => was_input,
_ => path != out_path,
}
}
fn pack(h: &Header, level: u32, input: &mut dyn Read, out: &mut dyn Write) -> io::Result<Packed> {
let mut c = Counting { inner: out, n: 0 };
let (_crc, bytes_in) = compress_member(h, level, Engine::Miniz, input, &mut c)?;
let header_len = 10 + h.name.as_ref().map_or(0, |n| n.len() as u64 + 1);
Ok(Packed {
bytes_in,
body: c.n.saturating_sub(header_len + 8),
})
}
fn open_input(host: &Host, path: &Path, follow: bool) -> io::Result<InFile> {
open_file(path, follow).map(|f| host.track(path, f))
}
fn open_file(path: &Path, follow: bool) -> io::Result<File> {
#[cfg(unix)]
{
use std::os::fd::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
let mut flags = libc::O_NONBLOCK;
if !follow {
flags |= libc::O_NOFOLLOW;
}
let f = File::options().read(true).custom_flags(flags).open(path)?;
let fd = f.as_raw_fd();
unsafe {
let fl = libc::fcntl(fd, libc::F_GETFL);
if fl >= 0 {
libc::fcntl(fd, libc::F_SETFL, fl & !libc::O_NONBLOCK);
}
}
Ok(f)
}
#[cfg(not(unix))]
{
let _ = follow;
File::open(path)
}
}
fn mode_of(meta: &Metadata) -> u32 {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
meta.permissions().mode() & 0o7777
}
#[cfg(not(unix))]
{
let _ = meta;
0
}
}
fn other_links(meta: &Metadata) -> u64 {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
meta.nlink().saturating_sub(1)
}
#[cfg(not(unix))]
{
let _ = meta;
0
}
}
fn ask_yes() -> bool {
let mut line = Vec::new();
let mut byte = [0u8; 1];
let mut stdin = io::stdin().lock();
loop {
match stdin.read(&mut byte) {
Ok(1) if byte[0] != b'\n' => line.push(byte[0]),
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
_ => break,
}
}
matches!(line.first(), Some(b'y' | b'Y'))
}
#[cfg(test)]
mod tests {
use super::*;
fn gz(suffix: &[u8]) -> Settings {
Settings {
mode: Mode::Compress,
to_stdout: false,
force: false,
keep: false,
recursive: false,
quiet: false,
verbose: 0,
no_name: false,
no_time: false,
restore: false,
level: 6,
suffix: suffix.to_vec(),
presume_tty: false,
files: vec![],
}
}
fn suffix(custom: &[u8], name: &[u8]) -> Option<String> {
let mut err = Vec::new();
let mut host = Host::Real;
let g = Gz {
s: gz(custom),
host: &mut host,
err: &mut err,
exit: 0,
stdout: None,
stdin_named: false,
list: Lister::default(),
stale: None,
time_stamp: 0,
descend: None,
};
g.suffix_of(name).map(|s| String::from_utf8(s).unwrap())
}
#[test]
fn suffixes_match_the_binary() {
assert_eq!(suffix(b".gz", b"a.gz").as_deref(), Some(".gz"));
assert_eq!(suffix(b".gz", b"A.GZ").as_deref(), Some(".GZ"));
assert_eq!(suffix(b".gz", b"f_Z").as_deref(), Some("_Z"));
assert_eq!(suffix(b".gz", b"f.TAZ").as_deref(), Some(".TAZ"));
assert_eq!(suffix(b".gz", b"f_GZ"), None);
assert_eq!(suffix(b".gz", b"f.bz2"), None);
assert_eq!(suffix(b".gz", b".gz"), None);
assert_eq!(suffix(b".gz", b"d/.gz"), None);
assert_eq!(suffix(b".gz", b"d/y.gz").as_deref(), Some(".gz"));
assert_eq!(suffix(b"z", b"a.gz").as_deref(), Some(".gz"));
assert_eq!(suffix(b"Z", b"a.gz").as_deref(), Some("z"));
assert_eq!(suffix(b"Z", b"a.GZ").as_deref(), Some("Z"));
assert_eq!(suffix(b"z", b"a.GZ").as_deref(), Some(".GZ"));
assert_eq!(suffix(b"Gz", b"a.gz").as_deref(), Some("gz"));
assert_eq!(suffix(b"gz", b"a.gz").as_deref(), Some(".gz"));
assert_eq!(suffix(b"q", b"k.Q").as_deref(), Some("Q"));
assert_eq!(suffix(b".x", b"c.Gz").as_deref(), Some(".Gz"));
assert_eq!(suffix(b".X", b"f.x").as_deref(), Some(".x"));
}
}