#![deny(unused_results)]
#![deny(missing_docs)]
#![forbid(unsafe_code)]
const TEMPFILE_ATTEMPTS: u32 = 100;
use libc;
use nix;
use openat;
use rand::Rng;
use std::ffi::OsStr;
use std::fs::File;
use std::io::prelude::*;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::io::AsRawFd;
use std::os::unix::prelude::FileExt as UnixFileExt;
use std::path::Path;
use std::{fs, io};
pub trait OpenatDirExt {
fn open_file_optional<P: openat::AsPath>(&self, p: P) -> io::Result<Option<fs::File>>;
fn read_to_string<P: openat::AsPath>(&self, p: P) -> io::Result<String>;
fn read_to_string_optional<P: openat::AsPath>(&self, p: P) -> io::Result<Option<String>>;
fn remove_file_optional<P: openat::AsPath>(&self, p: P) -> io::Result<bool>;
fn remove_dir_optional<P: openat::AsPath>(&self, p: P) -> io::Result<bool>;
fn sub_dir_optional<P: openat::AsPath>(&self, p: P) -> io::Result<Option<openat::Dir>>;
fn metadata_optional<P: openat::AsPath>(&self, p: P) -> io::Result<Option<openat::Metadata>>;
fn get_file_type(&self, e: &openat::Entry) -> io::Result<openat::SimpleType>;
fn exists<P: openat::AsPath>(&self, p: P) -> io::Result<bool>;
fn ensure_dir<P: openat::AsPath>(&self, p: P, mode: libc::mode_t) -> io::Result<()>;
fn ensure_dir_all<P: openat::AsPath>(&self, p: P, mode: libc::mode_t) -> io::Result<()>;
fn remove_all<P: openat::AsPath>(&self, p: P) -> io::Result<bool>;
fn copy_file<S: openat::AsPath, D: openat::AsPath>(&self, s: S, d: D) -> io::Result<()>;
fn copy_file_at<S: openat::AsPath, D: openat::AsPath>(
&self,
s: S,
target_dir: &openat::Dir,
d: D,
) -> io::Result<()>;
fn new_file_writer<'a>(&'a self, mode: libc::mode_t) -> io::Result<FileWriter>;
fn write_file_with<P: AsRef<Path>, F, T, E>(
&self,
destname: P,
mode: libc::mode_t,
gen_content_fn: F,
) -> Result<T, E>
where
F: FnOnce(&mut std::io::BufWriter<std::fs::File>) -> Result<T, E>,
E: From<io::Error>,
{
let mut w = self.new_file_writer(mode)?;
gen_content_fn(&mut w.writer).and_then(|t| {
w.complete(destname)?;
Ok(t)
})
}
fn write_file_with_sync<P: AsRef<Path>, F, T, E>(
&self,
destname: P,
mode: libc::mode_t,
gen_content_fn: F,
) -> Result<T, E>
where
F: FnOnce(&mut std::io::BufWriter<std::fs::File>) -> Result<T, E>,
E: From<io::Error>,
{
let mut w = self.new_file_writer(mode)?;
gen_content_fn(&mut w.writer).and_then(|t| {
w.complete_with(destname, |f| f.sync_all())?;
Ok(t)
})
}
fn write_file_contents<P: AsRef<Path>, C: AsRef<[u8]>>(
&self,
destname: P,
mode: libc::mode_t,
contents: C,
) -> io::Result<()> {
self.write_file_with(destname, mode, |w| w.write_all(contents.as_ref()))
}
}
impl OpenatDirExt for openat::Dir {
fn open_file_optional<P: openat::AsPath>(&self, p: P) -> io::Result<Option<fs::File>> {
match self.open_file(p) {
Ok(f) => Ok(Some(f)),
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
Ok(None)
} else {
Err(e)
}
}
}
}
fn read_to_string<P: openat::AsPath>(&self, p: P) -> io::Result<String> {
impl_read_to_string(self.open_file(p)?)
}
fn read_to_string_optional<P: openat::AsPath>(&self, p: P) -> io::Result<Option<String>> {
if let Some(f) = self.open_file_optional(p)? {
Ok(Some(impl_read_to_string(f)?))
} else {
Ok(None)
}
}
fn remove_file_optional<P: openat::AsPath>(&self, p: P) -> io::Result<bool> {
Ok(impl_remove_file_optional(self, p)?)
}
fn remove_dir_optional<P: openat::AsPath>(&self, p: P) -> io::Result<bool> {
match self.remove_dir(p) {
Ok(_) => Ok(true),
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
Ok(false)
} else {
Err(e)
}
}
}
}
fn metadata_optional<P: openat::AsPath>(&self, p: P) -> io::Result<Option<openat::Metadata>> {
match self.metadata(p) {
Ok(d) => Ok(Some(d)),
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
Ok(None)
} else {
Err(e)
}
}
}
}
fn sub_dir_optional<P: openat::AsPath>(&self, p: P) -> io::Result<Option<openat::Dir>> {
match self.sub_dir(p) {
Ok(d) => Ok(Some(d)),
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
Ok(None)
} else {
Err(e)
}
}
}
}
fn get_file_type(&self, e: &openat::Entry) -> io::Result<openat::SimpleType> {
if let Some(ftype) = e.simple_type() {
Ok(ftype)
} else {
Ok(self.metadata(e.file_name())?.simple_type())
}
}
fn exists<P: openat::AsPath>(&self, p: P) -> io::Result<bool> {
match self.metadata(p) {
Ok(_) => Ok(true),
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
Ok(false)
} else {
Err(e)
}
}
}
}
fn ensure_dir<P: openat::AsPath>(&self, p: P, mode: libc::mode_t) -> io::Result<()> {
match self.create_dir(p, mode) {
Ok(_) => Ok(()),
Err(e) => {
if e.kind() == io::ErrorKind::AlreadyExists {
Ok(())
} else {
Err(e)
}
}
}
}
fn ensure_dir_all<P: openat::AsPath>(&self, p: P, mode: libc::mode_t) -> io::Result<()> {
let p = to_cstr(p)?;
let p = p.as_ref();
let p = Path::new(OsStr::from_bytes(p.to_bytes()));
match self.create_dir(p, mode) {
Ok(_) => {}
Err(e) => match e.kind() {
io::ErrorKind::AlreadyExists => {}
io::ErrorKind::NotFound => impl_ensure_dir_all(self, p, mode)?,
_ => return Err(e),
},
}
Ok(())
}
fn copy_file<S: openat::AsPath, D: openat::AsPath>(&self, s: S, d: D) -> io::Result<()> {
let src = self.open_file(s)?;
impl_copy_regfile(&src, self, d)
}
fn copy_file_at<S: openat::AsPath, D: openat::AsPath>(
&self,
s: S,
target_dir: &openat::Dir,
d: D,
) -> io::Result<()> {
let src = self.open_file(s)?;
impl_copy_regfile(&src, target_dir, d)
}
fn remove_all<P: openat::AsPath>(&self, p: P) -> io::Result<bool> {
impl_remove_all(self, p)
}
fn new_file_writer<'a>(&'a self, mode: libc::mode_t) -> io::Result<FileWriter> {
let (tmpf, name) = if let Some(tmpf) = self.new_unnamed_file(mode).ok() {
(tmpf, None)
} else {
let (tmpf, name) = tempfile_in(self, ".tmp", ".tmp", mode)?;
(tmpf, Some(name))
};
Ok(FileWriter::new(self, tmpf, name))
}
}
fn impl_read_to_string(mut f: File) -> io::Result<String> {
let mut buf = String::new();
let _ = f.read_to_string(&mut buf)?;
Ok(buf)
}
fn map_nix_error(e: nix::Error) -> io::Error {
match e.as_errno() {
Some(os_err) => io::Error::from_raw_os_error(os_err as i32),
_ => io::Error::new(io::ErrorKind::Other, e),
}
}
fn copy_regfile_inner(
src: &File,
srcmeta: &std::fs::Metadata,
dest: &mut FileWriter,
) -> io::Result<()> {
let destf = dest.writer.get_mut();
let _ = src.copy_to(destf)?;
let nixmode = nix::sys::stat::Mode::from_bits_truncate(srcmeta.permissions().mode());
nix::sys::stat::fchmod(destf.as_raw_fd(), nixmode).map_err(map_nix_error)?;
Ok(())
}
fn impl_copy_regfile<D: openat::AsPath>(
src: &File,
target_dir: &openat::Dir,
d: D,
) -> io::Result<()> {
let d = to_cstr(d)?;
let d = OsStr::from_bytes(d.as_ref().to_bytes());
let meta = src.metadata()?;
let mut w = target_dir.new_file_writer(0o600)?;
copy_regfile_inner(src, &meta, &mut w).and_then(|t| {
w.complete(d)?;
Ok(t)
})
}
fn impl_remove_file_optional<P: openat::AsPath>(d: &openat::Dir, path: P) -> io::Result<bool> {
match d.remove_file(path) {
Ok(_) => Ok(true),
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
Ok(false)
} else {
Err(e)
}
}
}
}
pub(crate) fn random_name(rng: &mut rand::rngs::ThreadRng, prefix: &str, suffix: &str) -> String {
let mut tmpname = prefix.to_string();
for _ in 0..8 {
tmpname.push(rng.sample(rand::distributions::Alphanumeric));
}
tmpname.push_str(suffix);
tmpname
}
pub(crate) fn tempfile_in(
d: &openat::Dir,
prefix: &str,
suffix: &str,
mode: libc::mode_t,
) -> io::Result<(fs::File, String)> {
for _ in 0..TEMPFILE_ATTEMPTS {
let tmpname = random_name(&mut rand::thread_rng(), prefix, suffix);
match d.new_file(tmpname.as_str(), mode) {
Ok(f) => return Ok((f, tmpname)),
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
Err(e) => Err(e)?,
}
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"Exhausted {} attempts to create temporary file",
TEMPFILE_ATTEMPTS
),
))
}
pub(crate) fn impl_ensure_dir_all(d: &openat::Dir, p: &Path, mode: libc::mode_t) -> io::Result<()> {
if let Some(parent) = p.parent() {
if parent.as_os_str().len() > 0 {
impl_ensure_dir_all(d, parent, mode)?;
}
}
d.ensure_dir(p, mode)?;
Ok(())
}
pub(crate) fn remove_children(d: &openat::Dir, iter: openat::DirIter) -> io::Result<()> {
for entry in iter {
let entry = entry?;
match d.get_file_type(&entry)? {
openat::SimpleType::Dir => {
let subd = d.sub_dir(&entry)?;
remove_children(&subd, subd.list_dir(".")?)?;
let _ = d.remove_dir_optional(&entry)?;
}
_ => {
let _ = d.remove_file_optional(entry.file_name())?;
}
}
}
Ok(())
}
fn impl_remove_all<P: openat::AsPath>(d: &openat::Dir, p: P) -> io::Result<bool> {
let cp = to_cstr(p)?;
let cp = cp.as_ref();
match impl_remove_file_optional(d, cp) {
Ok(b) => Ok(b),
Err(e) => {
if let Some(ecode) = e.raw_os_error() {
match ecode {
libc::ENOENT => Ok(false),
libc::EISDIR => {
let iter = d.list_dir(cp)?;
let subd = d.sub_dir(cp)?;
remove_children(&subd, iter)?;
d.remove_dir(cp)?;
Ok(true)
}
_ => Err(e),
}
} else {
unreachable!("Unexpected non-OS error from openat::sub_dir: {}", e)
}
}
}
}
pub struct FileWriter<'a> {
pub writer: std::io::BufWriter<std::fs::File>,
pub tmp_prefix: String,
pub tmp_suffix: String,
dir: &'a openat::Dir,
tempname: Option<String>,
}
impl<'a> FileWriter<'a> {
fn new(dir: &'a openat::Dir, f: std::fs::File, tempname: Option<String>) -> Self {
Self {
writer: std::io::BufWriter::new(f),
tempname,
tmp_prefix: ".tmp.".to_string(),
tmp_suffix: ".tmp".to_string(),
dir,
}
}
fn linkat(
dir: &openat::Dir,
fd: fs::File,
rng: &mut rand::rngs::ThreadRng,
prefix: &str,
suffix: &str,
) -> io::Result<String> {
for _ in 0..TEMPFILE_ATTEMPTS {
let tmpname = random_name(rng, prefix, suffix);
match dir.link_file_at(&fd, tmpname.as_str()) {
Ok(()) => {
return Ok(tmpname);
}
Err(e) => {
if e.kind() == io::ErrorKind::AlreadyExists {
continue;
} else {
return Err(e);
}
}
}
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"Exhausted {} attempts to create temporary file",
TEMPFILE_ATTEMPTS
),
))
}
pub fn complete_with<P: AsRef<Path>, F>(self, dest: P, f: F) -> io::Result<()>
where
F: Fn(&fs::File) -> io::Result<()>,
{
let dest = dest.as_ref();
let dir = self.dir;
let prefix = self.tmp_prefix;
let suffix = self.tmp_suffix;
let fd = self.writer.into_inner()?;
f(&fd)?;
let mut rng = rand::thread_rng();
let tmpname = if let Some(t) = self.tempname {
t
} else {
Self::linkat(&dir, fd, &mut rng, prefix.as_str(), suffix.as_str())?
};
let tmpname = tmpname.as_str();
match self.dir.local_rename(tmpname, dest) {
Ok(()) => Ok(()),
Err(e) => {
let _ = self.dir.remove_file(tmpname);
Err(e)
}
}
}
pub fn complete<P: AsRef<Path>>(self, dest: P) -> io::Result<()> {
self.complete_with(dest, |_f| Ok(()))
}
}
pub(crate) fn fallback_file_copy(src: &File, dest: &File) -> io::Result<u64> {
let mut off: u64 = 0;
let mut buf = [0u8; 8192];
loop {
let n = src.read_at(&mut buf, off)?;
if n == 0 {
return Ok(off);
}
dest.write_all_at(&buf[0..n], off)?;
off += n as u64;
}
}
pub trait FileExt {
fn copy_to(&self, to: &File) -> io::Result<u64>;
}
impl FileExt for File {
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn copy_to(&self, to: &File) -> io::Result<u64> {
fallback_file_copy(self, to)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn copy_to(&self, to: &File) -> io::Result<u64> {
use nix::errno::Errno;
use nix::fcntl::copy_file_range;
use std::sync::atomic::{AtomicBool, Ordering};
static HAS_COPY_FILE_RANGE: AtomicBool = AtomicBool::new(true);
let len = self.metadata()?.len();
let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
let mut written = 0u64;
while written < len {
let copy_result = if has_copy_file_range {
let bytes_to_copy = std::cmp::min(len - written, usize::MAX as u64) as usize;
let copy_result =
copy_file_range(self.as_raw_fd(), None, to.as_raw_fd(), None, bytes_to_copy);
if let Err(ref copy_err) = copy_result {
match copy_err.as_errno() {
Some(Errno::ENOSYS) | Some(Errno::EPERM) => {
HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed);
}
_ => {}
}
}
copy_result
} else {
Err(nix::Error::from_errno(Errno::ENOSYS))
};
match copy_result {
Ok(ret) => written += ret as u64,
Err(err) => {
match err.as_errno() {
Some(os_err)
if os_err == Errno::ENOSYS
|| os_err == Errno::EXDEV
|| os_err == Errno::EINVAL
|| os_err == Errno::EPERM =>
{
assert_eq!(written, 0);
return fallback_file_copy(self, to);
}
Some(os_err) => return Err(io::Error::from_raw_os_error(os_err as i32)),
_ => return Err(io::Error::new(io::ErrorKind::Other, err)),
}
}
}
}
Ok(written)
}
}
fn to_cstr<P: openat::AsPath>(path: P) -> io::Result<P::Buffer> {
path.to_path()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "nul byte in file name"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::{Path, PathBuf};
use std::{error, result};
use tempfile;
type Result<T> = result::Result<T, Box<dyn error::Error>>;
#[test]
fn open_file_optional() -> Result<()> {
let td = tempfile::tempdir()?;
let d = openat::Dir::open(td.path())?;
assert!(d.open_file_optional("foo")?.is_none());
d.write_file("foo", 0o644)?.sync_all()?;
assert!(d.open_file_optional("foo")?.is_some());
Ok(())
}
#[test]
fn read_to_string() -> Result<()> {
let td = tempfile::tempdir()?;
let d = openat::Dir::open(td.path())?;
assert!(d.read_to_string("foo").is_err());
d.write_file_contents("foo", 0o644, "bar")?;
assert_eq!(d.read_to_string("foo")?, "bar");
Ok(())
}
#[test]
fn read_to_string_optional() -> Result<()> {
let td = tempfile::tempdir()?;
let d = openat::Dir::open(td.path())?;
assert!(d.read_to_string_optional("foo")?.is_none());
d.write_file_contents("foo", 0o644, "bar")?;
assert!(d.read_to_string_optional("foo")?.is_some());
assert_eq!(d.read_to_string_optional("foo")?.unwrap(), "bar");
Ok(())
}
#[test]
fn remove_file_optional() -> Result<()> {
let td = tempfile::tempdir()?;
let d = openat::Dir::open(td.path())?;
d.write_file("foo", 0o644)?.sync_all()?;
assert!(d.open_file_optional("foo")?.is_some());
let removed = d.remove_file_optional("foo")?;
assert!(removed);
assert!(d.open_file_optional("foo")?.is_none());
Ok(())
}
#[test]
fn metadata_optional() -> Result<()> {
let td = tempfile::tempdir()?;
let d = openat::Dir::open(td.path())?;
assert!(d.metadata_optional("foo")?.is_none());
d.write_file("foo", 0o644)?.sync_all()?;
assert!(d.metadata_optional("foo")?.is_some());
Ok(())
}
#[test]
fn get_file_type() -> Result<()> {
let td = tempfile::tempdir()?;
let d = openat::Dir::open(td.path())?;
d.write_file("foo", 0o644)?.sync_all()?;
for x in d.list_dir(".")? {
let x = x?;
assert_eq!("foo", x.file_name());
let t = d.get_file_type(&x)?;
assert_eq!(openat::SimpleType::File, t);
}
Ok(())
}
#[test]
fn ensure_dir_all() -> Result<()> {
let td = tempfile::tempdir()?;
let d = openat::Dir::open(td.path())?;
let mode = 0o755;
let p = Path::new("foo/bar/baz");
d.ensure_dir_all(p, mode)?;
assert_eq!(d.metadata(p)?.stat().st_mode & !libc::S_IFMT, mode);
d.ensure_dir_all(p, mode)?;
d.ensure_dir_all("foo/bar", mode)?;
d.ensure_dir_all("foo", mode)?;
d.ensure_dir_all("bar", 0o700)?;
assert_eq!(d.metadata("bar")?.stat().st_mode & !libc::S_IFMT, 0o700);
Ok(())
}
fn find_test_file(tempdir: &Path) -> Result<PathBuf> {
for p in ["/proc/self/exe", "/usr/bin/bash"].iter() {
let p = Path::new(p);
if p.exists() {
return Ok(p.into());
}
}
let fallback = tempdir.join("testfile-fallback");
std::fs::write(&fallback, "some test data")?;
Ok(fallback)
}
#[test]
fn copy_fallback() -> Result<()> {
use std::io::Read;
let td = tempfile::tempdir()?;
let src_p = find_test_file(td.path())?;
let dest_p = td.path().join("bash");
{
let src = File::open(&src_p)?;
let dest = File::create(&dest_p)?;
let _ = fallback_file_copy(&src, &dest)?;
}
let mut src = File::open(&src_p)?;
let mut srcbuf = Vec::new();
let _ = src.read_to_end(&mut srcbuf)?;
let mut destbuf = Vec::new();
let mut dest = File::open(&dest_p)?;
let _ = dest.read_to_end(&mut destbuf)?;
assert_eq!(srcbuf.len(), destbuf.len());
assert_eq!(&srcbuf, &destbuf);
Ok(())
}
#[test]
fn copy_file_at() {
let src_td = tempfile::tempdir().unwrap();
let src_dir = openat::Dir::open(src_td.path()).unwrap();
src_dir
.write_file_contents("foo", 0o644, "test content")
.unwrap();
let dst_td = tempfile::tempdir().unwrap();
let dst_dir = openat::Dir::open(dst_td.path()).unwrap();
assert_eq!(dst_dir.exists("bar").unwrap(), false);
src_dir.copy_file_at("foo", &dst_dir, "bar").unwrap();
assert_eq!(dst_dir.exists("bar").unwrap(), true);
let srcbuf = {
let mut src = src_dir.open_file("foo").unwrap();
let mut srcbuf = Vec::new();
let _ = src.read_to_end(&mut srcbuf).unwrap();
srcbuf
};
let destbuf = {
let mut destbuf = Vec::new();
let mut dest = dst_dir.open_file("bar").unwrap();
let _ = dest.read_to_end(&mut destbuf).unwrap();
destbuf
};
assert_eq!(&srcbuf, b"test content");
assert_eq!(&srcbuf, &destbuf);
}
}