use std::borrow::Cow;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::ops::Range;
use std::os::fd::AsFd;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use nix::errno::Errno;
use nix::fcntl;
use nix::sys::stat;
use nix::unistd;
use crate::FileOrigin;
use crate::file_metadata::{Device, FileKind, FileMetadata, FileMode, Xattrs};
use crate::files::File;
pub fn classify_source_error(e: Errno, path: &Path) -> crate::Error {
match e {
Errno::ENOENT => crate::Error::FileNotFound {
file: FileOrigin::Host {
path: Cow::Owned(path.to_path_buf()),
},
},
other => {
let io_err = io::Error::from(other);
crate::Error::Io {
kind: io_err.kind(),
code: io_err.raw_os_error(),
}
}
}
}
pub fn classify_dest_error(e: io::Error, path: &Path) -> crate::Error {
match e.kind() {
io::ErrorKind::AlreadyExists => crate::Error::FileAlreadyExists {
path: FileOrigin::Host {
path: Cow::Owned(path.to_path_buf()),
},
},
io::ErrorKind::NotFound => crate::Error::NoParentDirectory {
file: FileOrigin::Host {
path: Cow::Owned(path.to_path_buf()),
},
},
io::ErrorKind::NotADirectory => crate::Error::NotADirectory {
file: FileOrigin::Host {
path: Cow::Owned(path.to_path_buf()),
},
},
_ => crate::Error::from(e),
}
}
pub fn file_kind_from_stat(stat: &stat::FileStat, path: &Path) -> crate::Result<FileKind> {
let file_type = stat::SFlag::from_bits_truncate(stat.st_mode) & stat::SFlag::S_IFMT;
if file_type == stat::SFlag::S_IFREG {
Ok(FileKind::Regular)
} else if file_type == stat::SFlag::S_IFDIR {
Ok(FileKind::Dir)
} else if file_type == stat::SFlag::S_IFLNK {
let target = fcntl::readlink(path).map_err(|e| {
let io_err = io::Error::from(e);
crate::Error::Io {
kind: io_err.kind(),
code: io_err.raw_os_error(),
}
})?;
Ok(FileKind::Symlink {
target: target.into(),
})
} else if file_type == stat::SFlag::S_IFBLK {
Ok(FileKind::Block {
dev: Device::new(stat::major(stat.st_rdev), stat::minor(stat.st_rdev)),
})
} else if file_type == stat::SFlag::S_IFCHR {
Ok(FileKind::Char {
dev: Device::new(stat::major(stat.st_rdev), stat::minor(stat.st_rdev)),
})
} else if file_type == stat::SFlag::S_IFIFO {
Ok(FileKind::Pipe)
} else {
Err(crate::Error::UnsupportedFileType {
file: FileOrigin::Host {
path: path.to_owned().into(),
},
})
}
}
pub fn stat_time(secs: i64, nsec: i64) -> SystemTime {
if secs >= 0 {
UNIX_EPOCH + Duration::new(secs as u64, nsec as u32)
} else {
UNIX_EPOCH - Duration::new((-secs) as u64, 0) + Duration::new(0, nsec as u32)
}
}
fn is_xattr_not_supported(e: &io::Error) -> bool {
matches!(e.raw_os_error(), Some(code) if code == nix::libc::ENOTSUP || code == nix::libc::EOPNOTSUPP)
}
pub fn read_host_xattrs(path: &Path) -> crate::Result<Xattrs> {
let names = match xattr::list(path) {
Ok(names) => names,
Err(e) if is_xattr_not_supported(&e) => return Ok(Xattrs::new()),
Err(e) => return Err(crate::Error::from(e)),
};
let mut xattrs = Xattrs::new();
for name in names {
match xattr::get(path, &name) {
Ok(Some(value)) => {
xattrs.set(name, value);
}
Ok(None) => {}
Err(e) if is_xattr_not_supported(&e) => return Ok(Xattrs::new()),
Err(e) => return Err(crate::Error::from(e)),
}
}
Ok(xattrs)
}
pub fn archive_metadata(
stat: &stat::FileStat,
btime: Option<SystemTime>,
file: &mut File<'_, '_>,
) -> crate::Result<()> {
file.set_mode(FileMode::from_bits_truncate(stat.st_mode))?;
file.set_accessed(stat_time(stat.st_atime, stat.st_atime_nsec))?;
file.set_modified(stat_time(stat.st_mtime, stat.st_mtime_nsec))?;
file.set_changed(stat_time(stat.st_ctime, stat.st_ctime_nsec))?;
file.set_created(btime)?;
Ok(())
}
pub fn archive_contents(
source: &Path,
file_size: u64,
dest: &mut File<'_, '_>,
) -> crate::Result<()> {
if file_size == 0 {
return Ok(());
}
let host_file = std::fs::File::open(source)?;
match unistd::lseek(host_file.as_fd(), 0, unistd::Whence::SeekData) {
Err(Errno::EINVAL) | Err(Errno::EOPNOTSUPP) => {
let mut reader = host_file;
io::copy(&mut reader, dest)?;
dest.flush()?;
return Ok(());
}
Err(Errno::ENXIO) => {
dest.set_len(file_size)?;
return Ok(());
}
Ok(_) | Err(_) => {
unistd::lseek(host_file.as_fd(), 0, unistd::Whence::SeekSet)
.map_err(|e| crate::Error::from(io::Error::from(e)))?;
}
}
let mut reader = host_file;
let mut pos: u64 = 0;
loop {
let data_start = match unistd::lseek(reader.as_fd(), pos as i64, unistd::Whence::SeekData) {
Ok(offset) => offset as u64,
Err(Errno::ENXIO) => {
dest.set_len(file_size)?;
break;
}
Err(e) => return Err(crate::Error::from(io::Error::from(e))),
};
if data_start > pos {
dest.set_len(data_start)?;
dest.seek(SeekFrom::Start(data_start))?;
}
let hole_start =
match unistd::lseek(reader.as_fd(), data_start as i64, unistd::Whence::SeekHole) {
Ok(offset) => offset as u64,
Err(Errno::ENXIO) => file_size,
Err(e) => return Err(crate::Error::from(io::Error::from(e))),
};
let data_end = hole_start.min(file_size);
reader.seek(SeekFrom::Start(data_start))?;
let bytes_to_copy = data_end - data_start;
io::copy(&mut Read::by_ref(&mut reader).take(bytes_to_copy), dest)?;
pos = data_end;
if pos >= file_size {
break;
}
}
dest.flush()?;
Ok(())
}
pub fn extract_contents_sparse(
source: &mut (impl Read + Seek),
dest: &mut std::fs::File,
holes: &[Range<u64>],
) -> crate::Result<()> {
let file_len = source.seek(SeekFrom::End(0))?;
source.seek(SeekFrom::Start(0))?;
let mut pos: u64 = 0;
for hole in holes {
if hole.start > pos {
let data_len = hole.start - pos;
io::copy(&mut Read::by_ref(source).take(data_len), dest)?;
}
source.seek(SeekFrom::Start(hole.end))?;
dest.seek(SeekFrom::Start(hole.end))?;
pos = hole.end;
}
if pos < file_len {
io::copy(&mut Read::by_ref(source).take(file_len - pos), dest)?;
}
dest.flush()?;
dest.set_len(file_len)?;
Ok(())
}
pub fn create_host_entry(kind: &FileKind, dest: &Path) -> io::Result<Option<std::fs::File>> {
match kind {
FileKind::Regular => {
let file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o666)
.open(dest)?;
Ok(Some(file))
}
FileKind::Dir => {
std::fs::create_dir(dest)?;
Ok(None)
}
FileKind::Symlink { target } => {
std::os::unix::fs::symlink(target, dest)?;
Ok(None)
}
FileKind::Block { dev } => {
stat::mknod(
dest,
stat::SFlag::S_IFBLK,
stat::Mode::empty(),
stat::makedev(dev.major(), dev.minor()),
)
.map_err(io::Error::from)?;
Ok(None)
}
FileKind::Char { dev } => {
stat::mknod(
dest,
stat::SFlag::S_IFCHR,
stat::Mode::empty(),
stat::makedev(dev.major(), dev.minor()),
)
.map_err(io::Error::from)?;
Ok(None)
}
FileKind::Pipe => {
unistd::mkfifo(dest, stat::Mode::empty()).map_err(io::Error::from)?;
Ok(None)
}
}
}
pub fn extract_metadata(
kind: &FileKind,
meta: &FileMetadata,
xattrs: &Xattrs,
dest: &Path,
) -> crate::Result<()> {
let nix_uid = Some(nix::unistd::Uid::from_raw(meta.user().as_raw()));
let nix_gid = Some(nix::unistd::Gid::from_raw(meta.group().as_raw()));
if let Err(e) = unistd::fchownat(
fcntl::AT_FDCWD,
dest,
nix_uid,
nix_gid,
fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
) {
if e != Errno::EPERM {
return Err(crate::Error::from(io::Error::from(e)));
}
}
if !matches!(kind, FileKind::Symlink { .. }) {
let mode = stat::Mode::from_bits_truncate(meta.mode().bits());
if let Err(e) = stat::fchmodat(
fcntl::AT_FDCWD,
dest,
mode,
stat::FchmodatFlags::FollowSymlink,
) && e != Errno::EPERM
{
return Err(crate::Error::from(io::Error::from(e)));
}
}
let atime = system_time_to_timespec(meta.accessed());
let mtime = system_time_to_timespec(meta.modified());
if let Err(e) = stat::utimensat(
fcntl::AT_FDCWD,
dest,
&atime,
&mtime,
stat::UtimensatFlags::NoFollowSymlink,
) && e != Errno::EPERM
{
return Err(crate::Error::from(io::Error::from(e)));
}
if !matches!(kind, FileKind::Symlink { .. }) {
for (name, value) in xattrs {
if let Err(e) = xattr::set(dest, name, value) {
if is_xattr_not_supported(&e) {
break;
}
return Err(crate::Error::from(e));
}
}
}
Ok(())
}
fn system_time_to_timespec(time: SystemTime) -> nix::sys::time::TimeSpec {
match time.duration_since(UNIX_EPOCH) {
Ok(d) => nix::sys::time::TimeSpec::new(d.as_secs() as i64, d.subsec_nanos() as i64),
Err(e) => {
let d = e.duration();
nix::sys::time::TimeSpec::new(-(d.as_secs() as i64), d.subsec_nanos() as i64)
}
}
}
#[derive(Debug)]
pub struct PathPair {
pub host: PathBuf,
pub lbox: PathBuf,
}
pub fn push_dir_children(
host_dir: &Path,
lbox_dir: &Path,
stack: &mut Vec<PathPair>,
) -> crate::Result<()> {
let entries = std::fs::read_dir(host_dir)?;
for entry in entries {
let entry = entry?;
let host_path = entry.path();
let file_name = entry.file_name();
let lbox_path = lbox_dir.join(&file_name);
stack.push(PathPair {
host: host_path,
lbox: lbox_path,
});
}
Ok(())
}