use std::fs::{self, File, Metadata};
use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use super::args::{Args, OpMode};
use super::message::{Messages, mask_nonprint};
use super::messages as m;
use super::suffix;
use crate::compat::host::{
self, CleanupGuard, Host, InFile, Published, create_temp_armed, publish, strerror,
};
pub(crate) const IO_BUFFER_SIZE: usize = 8192;
enum Src {
File(InFile),
Other(Box<dyn Read>),
}
impl Read for Src {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Src::File(f) => f.read(buf),
Src::Other(r) => r.read(buf),
}
}
}
enum Out {
Raw(File),
Capped(BufWriter<Box<dyn Write + Send>>),
}
struct DestFile {
tmp: PathBuf,
handle: File,
meta: Option<Metadata>,
path: PathBuf,
replace: bool,
dir: Option<File>,
_guard: CleanupGuard,
}
pub(crate) struct FilePair {
pub(crate) src_name: Vec<u8>,
src_path: Option<PathBuf>,
src: Src,
src_meta: Option<Metadata>,
pub(crate) src_eof: bool,
pub(crate) src_has_seen_input: bool,
pub(crate) flush_needed: bool,
flush_timeout: u64,
next_flush: Instant,
real: bool,
out: Option<Out>,
dest_name: Vec<u8>,
dest: Option<DestFile>,
dest_try_sparse: bool,
dest_pending_sparse: u64,
}
fn eexist() -> io::Error {
#[cfg(unix)]
{
io::Error::from_raw_os_error(libc::EEXIST)
}
#[cfg(not(unix))]
{
io::Error::from(io::ErrorKind::AlreadyExists)
}
}
fn dirname(name: &[u8]) -> Vec<u8> {
match name.iter().rposition(|&b| b == b'/') {
None => b".".to_vec(),
Some(i) => {
let head = &name[..i];
let trimmed = head
.iter()
.rposition(|&b| b != b'/')
.map_or(&b""[..], |j| &head[..=j]);
if trimmed.is_empty() {
b"/".to_vec()
} else {
trimmed.to_vec()
}
}
}
}
fn parent_of(p: &Path) -> PathBuf {
match p.parent() {
Some(d) if !d.as_os_str().is_empty() => d.to_path_buf(),
_ => PathBuf::from("."),
}
}
#[cfg(unix)]
fn errno(e: &io::Error) -> i32 {
e.raw_os_error().unwrap_or(0)
}
#[cfg(unix)]
fn was_symlink(e: &io::Error, path: &Path, follow: bool) -> bool {
#[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
{
let _ = (path, follow);
errno(e) == libc::EMLINK
}
#[cfg(target_os = "netbsd")]
{
let _ = (path, follow);
errno(e) == libc::EFTYPE
}
#[cfg(not(any(target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd")))]
{
errno(e) == libc::ELOOP
&& !follow
&& fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_symlink())
}
}
impl FilePair {
pub(crate) fn open_src(
a: &Args,
name: Option<&[u8]>,
host: &mut Host,
msg: &mut Messages,
err: &mut dyn Write,
) -> Option<FilePair> {
Self::open_src_as(a, name, a.to_stdout, a.force, host, msg, err)
}
pub(crate) fn open_src_for_list(
a: &Args,
name: &[u8],
host: &mut Host,
msg: &mut Messages,
err: &mut dyn Write,
) -> Option<FilePair> {
Self::open_src_as(a, Some(name), false, true, host, msg, err)
}
fn open_src_as(
a: &Args,
name: Option<&[u8]>,
to_stdout: bool,
force: bool,
host: &mut Host,
msg: &mut Messages,
err: &mut dyn Write,
) -> Option<FilePair> {
let real = matches!(host, Host::Real);
let mut pair = FilePair {
src_name: name.unwrap_or(m::STDIN_NAME).to_vec(),
src_path: None,
src: Src::Other(Box::new(io::empty())),
src_meta: None,
src_eof: false,
src_has_seen_input: false,
flush_needed: false,
flush_timeout: if a.op == OpMode::Compress {
a.coder.flush_timeout
} else {
0
},
next_flush: Instant::now(),
real,
out: None,
dest_name: Vec::new(),
dest: None,
dest_try_sparse: false,
dest_pending_sparse: 0,
};
let Some(name) = name else {
pair.src = match host {
Host::Real => match host::stdin_file() {
Ok(f) => Src::File(InFile::from(f)),
Err(_) => Src::Other(Box::new(io::stdin())),
},
Host::Sandbox(_) => Src::Other(Box::new(host.stdin())),
};
return Some(pair);
};
if name.is_empty() {
msg.error(err, m::EMPTY_FILENAME, &[]);
return None;
}
let shown = mask_nonprint(name);
let path = host.path(name);
let follow = to_stdout || force || a.keep;
let reg_files_only = !to_stdout;
let mut opts = File::options();
opts.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut flags = libc::O_NOCTTY | libc::O_NONBLOCK;
if !follow {
flags |= libc::O_NOFOLLOW;
}
opts.custom_flags(flags);
}
#[cfg(not(unix))]
if !follow && fs::symlink_metadata(&path).is_ok_and(|m| m.file_type().is_symlink()) {
msg.warning(err, m::IS_SYMLINK, &[&shown]);
return None;
}
let file = match opts.open(&path) {
Ok(f) => f,
Err(e) => {
#[cfg(unix)]
if was_symlink(&e, &path, follow) {
msg.warning(err, m::IS_SYMLINK, &[&shown]);
return None;
}
msg.error(err, m::NAME_COLON, &[&shown, &strerror(&e)]);
return None;
}
};
let meta = match file.metadata() {
Ok(md) => md,
Err(e) => {
msg.error(err, m::NAME_COLON, &[&shown, &strerror(&e)]);
return None;
}
};
if meta.is_dir() {
msg.warning(err, m::IS_DIRECTORY, &[&shown]);
return None;
}
if reg_files_only && !meta.is_file() {
msg.warning(err, m::NOT_REGULAR, &[&shown]);
return None;
}
#[cfg(unix)]
if reg_files_only && !force && !a.keep {
use std::os::unix::fs::MetadataExt;
let mode = meta.mode();
if mode & 0o6000 != 0 {
msg.warning(err, m::SETID, &[&shown]);
return None;
}
if mode & 0o1000 != 0 {
msg.warning(err, m::STICKY, &[&shown]);
return None;
}
if meta.nlink() > 1 {
msg.warning(err, m::HARD_LINKS, &[&shown]);
return None;
}
}
#[cfg(unix)]
{
if !meta.is_file()
&& let Err(e) = wait_for_input(&file)
{
msg.error(err, m::POLL_FAILED, &[&shown, &strerror(&e)]);
return None;
}
clear_nonblock(&file);
}
pair.src = Src::File(host.track(&path, file));
pair.src_path = Some(path);
pair.src_meta = Some(meta);
Some(pair)
}
fn flush_wait(&self) -> Option<i32> {
if self.flush_timeout == 0 || !self.src_has_seen_input || !self.real {
return None;
}
let left = self.next_flush.saturating_duration_since(Instant::now());
Some(i32::try_from(left.as_millis()).unwrap_or(i32::MAX))
}
pub(crate) fn read(
&mut self,
buf: &mut [u8],
msg: &mut Messages,
err: &mut dyn Write,
) -> Option<usize> {
let mut pos = 0;
while pos < buf.len() {
#[cfg(unix)]
if let Some(timeout) = self.flush_wait()
&& let Src::File(f) = &self.src
{
use std::os::fd::AsRawFd;
let mut pfd = libc::pollfd {
fd: f.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let ret = unsafe { libc::poll(&mut pfd, 1, timeout) };
if ret == 0 {
self.flush_needed = true;
return Some(pos);
}
if ret == -1 {
let e = io::Error::last_os_error();
if matches!(errno(&e), libc::EINTR | libc::EAGAIN) {
continue;
}
let name = mask_nonprint(&self.src_name);
msg.error(err, m::POLL_FAILED, &[&name, &strerror(&e)]);
return None;
}
}
match self.src.read(&mut buf[pos..]) {
Ok(0) => {
self.src_eof = true;
break;
}
Ok(n) => {
pos += n;
if !self.src_has_seen_input {
self.src_has_seen_input = true;
self.next_flush =
Instant::now() + Duration::from_millis(self.flush_timeout);
}
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => {
let name = mask_nonprint(&self.src_name);
msg.error(err, m::READ_ERROR, &[&name, &strerror(&e)]);
return None;
}
}
}
Some(pos)
}
pub(crate) fn src_size(&self) -> u64 {
self.src_meta.as_ref().map_or(0, Metadata::len)
}
pub(crate) fn seek_src(&mut self, pos: u64, msg: &mut Messages, err: &mut dyn Write) -> bool {
let result = match &mut self.src {
Src::File(f) => f.seek(SeekFrom::Start(pos)).map(|_| ()),
Src::Other(_) => Err(io::Error::from(io::ErrorKind::Unsupported)),
};
match result {
Ok(()) => {
self.src_eof = false;
true
}
Err(e) => {
let name = mask_nonprint(&self.src_name);
msg.error(err, m::SEEK_ERROR, &[&name, &strerror(&e)]);
false
}
}
}
pub(crate) fn pread(
&mut self,
buf: &mut [u8],
pos: u64,
msg: &mut Messages,
err: &mut dyn Write,
) -> bool {
if !self.seek_src(pos, msg, err) {
return false;
}
match self.read(buf, msg, err) {
None => false,
Some(n) if n != buf.len() => {
let name = mask_nonprint(&self.src_name);
msg.error(err, m::UNEXPECTED_EOF, &[&name]);
false
}
Some(_) => true,
}
}
pub(crate) fn fix_src_pos(&mut self, n: u64) {
if n > 0
&& let Src::File(f) = &mut self.src
&& let Ok(back) = i64::try_from(n)
{
let _ = f.seek(SeekFrom::Current(-back));
}
}
pub(crate) fn open_dest(
&mut self,
a: &Args,
host: &mut Host,
msg: &mut Messages,
err: &mut dyn Write,
) -> bool {
let sparse = a.sparse && a.op == OpMode::Decompress;
if a.to_stdout || self.src_path.is_none() {
self.dest_name = m::STDOUT_NAME.to_vec();
match host {
Host::Real => match stdout_file() {
Ok(f) => {
self.dest_try_sparse = sparse && stdout_can_be_sparse(&f);
self.out = Some(Out::Raw(f));
}
Err(_) => self.out = Some(Out::Capped(host.out_writer(None))),
},
Host::Sandbox(_) => self.out = Some(Out::Capped(host.out_writer(None))),
}
return true;
}
let Some(dest) = suffix::dest_name(
&self.src_name,
a.op,
a.format,
a.suffix.as_deref(),
msg,
err,
) else {
return false;
};
let shown = mask_nonprint(&dest);
let path = host.path(&dest);
let mut dir = None;
if a.coder.synchronous && self.real {
let name = dirname(&dest);
match open_dir(&host.path(&name)) {
Ok(d) => dir = Some(d),
Err(e) => {
msg.error(
err,
m::OPEN_DIR_FAILED,
&[&mask_nonprint(&name), &strerror(&e)],
);
return false;
}
}
}
let replace = a.force
&& fs::metadata(&path).is_ok_and(|o| {
self.src_meta
.as_ref()
.is_some_and(|s| host::same_file(&o, s) == Some(true))
});
if a.force
&& !replace
&& let Err(e) = fs::remove_file(&path)
&& e.kind() != io::ErrorKind::NotFound
{
msg.error(err, m::CANNOT_REMOVE, &[&shown, &strerror(&e)]);
return false;
}
if !replace && let Err(e) = probe_exclusive(&path) {
msg.error(err, m::NAME_COLON, &[&shown, &strerror(&e)]);
return false;
}
let (tmp, file, guard) =
match create_temp_armed(host, &parent_of(&path), "xz", host::XZ_SIGNALS) {
Ok(t) => t,
Err(e) => {
msg.error(err, m::NAME_COLON, &[&shown, &strerror(&e)]);
return false;
}
};
let handle = match file.try_clone() {
Ok(h) => h,
Err(e) => {
let _ = fs::remove_file(&tmp);
msg.error(err, m::NAME_COLON, &[&shown, &strerror(&e)]);
return false;
}
};
self.out = Some(match host {
Host::Real => {
self.dest_try_sparse = sparse;
Out::Raw(file)
}
Host::Sandbox(_) => Out::Capped(host.out_writer(Some(file))),
});
self.dest_name = dest;
self.dest = Some(DestFile {
meta: handle.metadata().ok(),
tmp,
handle,
path,
replace,
dir,
_guard: guard,
});
true
}
pub(crate) fn write_failed(&self, e: &io::Error, msg: &mut Messages, err: &mut dyn Write) {
if e.kind() != io::ErrorKind::BrokenPipe {
let dest: &[u8] = if self.dest_name.is_empty() {
m::STDOUT_NAME
} else {
&self.dest_name
};
let name = mask_nonprint(dest);
msg.error(err, m::WRITE_ERROR, &[&name, &strerror(e)]);
}
}
fn write_buf(&mut self, buf: &[u8], msg: &mut Messages, err: &mut dyn Write) -> bool {
let r = match &mut self.out {
Some(Out::Raw(f)) => f.write_all(buf),
Some(Out::Capped(w)) => w.write_all(buf).and_then(|()| w.flush()),
None => Ok(()),
};
match r {
Ok(()) => true,
Err(e) => {
self.write_failed(&e, msg, err);
false
}
}
}
pub(crate) fn write(&mut self, buf: &[u8], msg: &mut Messages, err: &mut dyn Write) -> bool {
if self.dest_try_sparse {
if buf.len() == IO_BUFFER_SIZE {
if buf.iter().all(|&b| b == 0) && self.dest_pending_sparse < 1 << 62 {
self.dest_pending_sparse += buf.len() as u64;
return true;
}
} else if buf.is_empty() {
return true;
}
if self.dest_pending_sparse > 0 {
if let Err(e) = self.seek_out(self.dest_pending_sparse) {
let name = mask_nonprint(&self.dest_name);
msg.error(err, m::SPARSE_SEEK_FAILED, &[&name, &strerror(&e)]);
return false;
}
self.dest_pending_sparse = 0;
}
}
self.write_buf(buf, msg, err)
}
fn seek_out(&mut self, n: u64) -> io::Result<()> {
let n = i64::try_from(n).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
match &mut self.out {
Some(Out::Raw(f)) => f.seek(SeekFrom::Current(n)).map(|_| ()),
_ => Err(io::Error::from(io::ErrorKind::Unsupported)),
}
}
pub(crate) fn close(
mut self,
a: &Args,
mut success: bool,
msg: &mut Messages,
err: &mut dyn Write,
) {
if success && self.dest_try_sparse && self.dest_pending_sparse > 0 {
match self.seek_out(self.dest_pending_sparse - 1) {
Err(e) => {
let name = mask_nonprint(&self.dest_name);
msg.error(err, m::SPARSE_SEEK_FAILED, &[&name, &strerror(&e)]);
success = false;
}
Ok(()) => {
if !self.write_buf(&[0], msg, err) {
success = false;
}
}
}
}
let out = self.out.take();
if let Some(dest) = self.dest.take() {
let shown = mask_nonprint(&self.dest_name);
if success {
if let Some(src) = &self.src_meta {
copy_attrs(&dest, src, &shown, msg, err);
}
if a.coder.synchronous
&& self.real
&& let Err(e) = dest.handle.sync_all()
{
msg.error(err, m::SYNC_FILE_FAILED, &[&shown, &strerror(&e)]);
success = false;
}
}
drop(dest.handle);
if let Err(e) = close_out(out) {
msg.error(err, m::CLOSE_FAILED, &[&shown, &strerror(&e)]);
success = false;
}
if success {
success = self.publish(
&dest.tmp,
&dest.path,
a.force || dest.replace,
&shown,
msg,
err,
);
if success
&& let Some(dir) = &dest.dir
&& let Err(e) = dir.sync_all()
{
msg.error(err, m::SYNC_DIR_FAILED, &[&shown, &strerror(&e)]);
success = false;
}
} else {
let _ = fs::remove_file(&dest.tmp);
}
} else if let Some(Out::Capped(mut w)) = out {
let _ = w.flush();
}
let src = std::mem::replace(&mut self.src, Src::Other(Box::new(io::empty())));
drop(src);
if success
&& !a.keep
&& let (Some(path), Some(meta)) = (&self.src_path, &self.src_meta)
{
unlink(&self.src_name, path, meta, a.force, msg, err);
}
}
fn publish(
&self,
tmp: &Path,
path: &Path,
replace: bool,
shown: &[u8],
msg: &mut Messages,
err: &mut dyn Write,
) -> bool {
match publish(tmp, path, replace) {
Ok(Published::Done) => true,
Ok(Published::Exists) => {
msg.error(err, m::NAME_COLON, &[shown, &strerror(&eexist())]);
false
}
Err(e) => {
let _ = fs::remove_file(tmp);
msg.error(err, m::NAME_COLON, &[shown, &strerror(&e)]);
false
}
}
}
}
fn close_out(out: Option<Out>) -> io::Result<()> {
match out {
Some(Out::Raw(f)) => {
#[cfg(unix)]
{
use std::os::fd::IntoRawFd;
let fd = f.into_raw_fd();
if unsafe { libc::close(fd) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(not(unix))]
{
drop(f);
Ok(())
}
}
Some(Out::Capped(mut w)) => w.flush(),
None => Ok(()),
}
}
fn probe_exclusive(path: &Path) -> io::Result<()> {
let mut opts = File::options();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600)
.custom_flags(libc::O_NOCTTY | libc::O_NONBLOCK);
}
drop(opts.open(path)?);
let _ = fs::remove_file(path);
Ok(())
}
fn open_dir(path: &Path) -> io::Result<File> {
let mut opts = File::options();
opts.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.custom_flags(libc::O_DIRECTORY | libc::O_NOCTTY | libc::O_NONBLOCK);
}
opts.open(path)
}
fn stdout_file() -> io::Result<File> {
#[cfg(unix)]
{
use std::os::fd::AsFd;
Ok(File::from(io::stdout().as_fd().try_clone_to_owned()?))
}
#[cfg(not(unix))]
{
Err(io::Error::from(io::ErrorKind::Unsupported))
}
}
fn stdout_can_be_sparse(f: &File) -> bool {
#[cfg(unix)]
{
use std::os::fd::AsRawFd;
let Ok(md) = f.metadata() else {
return false;
};
if !md.is_file() {
return false;
}
let flags = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_GETFL) };
if flags == -1 || flags & libc::O_APPEND != 0 {
return false;
}
let mut f = f;
f.stream_position().is_ok_and(|pos| pos == md.len())
}
#[cfg(not(unix))]
{
let _ = f;
false
}
}
#[cfg(unix)]
fn wait_for_input(f: &File) -> io::Result<()> {
use std::os::fd::AsRawFd;
let mut pfd = libc::pollfd {
fd: f.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
loop {
match unsafe { libc::poll(&mut pfd, 1, -1) } {
-1 => {
let e = io::Error::last_os_error();
if !matches!(errno(&e), libc::EINTR | libc::EAGAIN) {
return Err(e);
}
}
0 => {}
_ if pfd.revents != 0 => return Ok(()),
_ => {}
}
}
}
#[cfg(unix)]
fn clear_nonblock(f: &File) {
use std::os::fd::AsRawFd;
let fd = f.as_raw_fd();
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags != -1 && flags & libc::O_NONBLOCK != 0 {
libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK);
}
}
}
fn copy_attrs(
dest: &DestFile,
src: &Metadata,
shown: &[u8],
msg: &mut Messages,
err: &mut dyn Write,
) {
#[cfg(unix)]
{
use std::os::fd::AsRawFd;
use std::os::unix::fs::MetadataExt;
let fd = dest.handle.as_raw_fd();
let last = || strerror(&io::Error::last_os_error());
unsafe {
if libc::fchown(fd, src.uid(), libc::gid_t::MAX) != 0 && libc::geteuid() == 0 {
msg.warning(err, m::CANNOT_SET_OWNER, &[shown, &last()]);
}
}
let src_mode = src.mode();
let dest_gid = dest.meta.as_ref().map_or(src.gid(), MetadataExt::gid);
let group_failed =
dest_gid != src.gid() && unsafe { libc::fchown(fd, libc::uid_t::MAX, src.gid()) } != 0;
let mode = if group_failed {
msg.warning(err, m::CANNOT_SET_GROUP, &[shown, &last()]);
let m = ((src_mode & 0o070) >> 3) & (src_mode & 0o007);
(src_mode & 0o700) | (m << 3) | m
} else {
src_mode & 0o777
};
if unsafe { libc::fchmod(fd, mode as libc::mode_t) } != 0 {
msg.warning(err, m::CANNOT_SET_PERMISSIONS, &[shown, &last()]);
}
}
#[cfg(not(unix))]
{
let _ = (shown, &msg, &err);
}
let mut times = fs::FileTimes::new();
if let Ok(t) = src.accessed() {
times = times.set_accessed(t);
}
if let Ok(t) = src.modified() {
times = times.set_modified(t);
}
let _ = dest.handle.set_times(times);
}
fn unlink(
name: &[u8],
path: &Path,
known: &Metadata,
force: bool,
msg: &mut Messages,
err: &mut dyn Write,
) {
let shown = mask_nonprint(name);
let now = if force {
fs::metadata(path)
} else {
fs::symlink_metadata(path)
};
let same = now.is_ok_and(|n| host::same_file(&n, known) != Some(false));
if !same {
msg.warning(err, m::MOVED, &[&shown]);
return;
}
if let Err(e) = fs::remove_file(path) {
msg.warning(err, m::CANNOT_REMOVE, &[&shown, &strerror(&e)]);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dirname_is_posix() {
assert_eq!(dirname(b"a.xz"), b".");
assert_eq!(dirname(b"d/a.xz"), b"d");
assert_eq!(dirname(b"d//a.xz"), b"d");
assert_eq!(dirname(b"/a.xz"), b"/");
assert_eq!(dirname(b"//a.xz"), b"/");
assert_eq!(dirname(b"x/y/a"), b"x/y");
}
}