use std::{
ffi::{CString, OsStr, OsString},
fs::File,
io::Result,
marker::PhantomData,
os::unix::prelude::{AsRawFd, FromRawFd, OsStrExt},
path::Path,
ptr,
};
cfg_if::cfg_if! {
if #[cfg(any(target_os = "aix",
target_os = "macos",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "netbsd",
target_os = "openbsd",
target_os = "illumos",
target_os = "solaris"))] {
use libc::openat as openat64;
} else {
use libc::openat64;
}
}
use cvt::cvt_r;
use libc::{c_int, mkdirat, mode_t};
use crate::{LinkEntryType, OpenOptions, OpenOptionsWriteMode};
pub mod exports {
pub use super::OpenOptionsExt;
#[doc(no_inline)]
pub use libc::mode_t;
}
trait PathFFI {
fn as_cstring(&self) -> Result<CString>;
}
impl PathFFI for Path {
fn as_cstring(&self) -> Result<CString> {
std::ffi::CString::new(self.as_os_str().as_bytes())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}
}
#[derive(Debug, Default)]
pub(crate) struct OpenOptionsImpl {
read: bool,
write: OpenOptionsWriteMode,
truncate: bool,
create: bool,
create_new: bool,
follow: Option<bool>,
mode: Option<mode_t>,
}
impl OpenOptionsImpl {
pub fn read(&mut self, read: bool) {
self.read = read;
}
pub fn write(&mut self, write: OpenOptionsWriteMode) {
self.write = write;
}
pub fn truncate(&mut self, truncate: bool) {
self.truncate = truncate;
}
pub fn create(&mut self, create: bool) {
self.create = create;
}
pub fn create_new(&mut self, create_new: bool) {
self.create_new = create_new;
}
pub fn follow(&mut self, follow: bool) {
self.follow = Some(follow)
}
fn get_flags(&self) -> Result<c_int> {
let data_flags = match (self.read, self.write) {
(false, OpenOptionsWriteMode::Write) => Ok(libc::O_WRONLY),
(false, OpenOptionsWriteMode::Append) => Ok(libc::O_WRONLY | libc::O_APPEND),
(false, OpenOptionsWriteMode::None) => {
Err(std::io::Error::from_raw_os_error(libc::EINVAL))
}
(true, OpenOptionsWriteMode::None) => Ok(libc::O_RDONLY),
(true, OpenOptionsWriteMode::Write) => Ok(libc::O_RDWR),
(true, OpenOptionsWriteMode::Append) => Ok(libc::O_RDWR | libc::O_APPEND),
}?;
let create_flags = if self.create_new {
libc::O_EXCL | libc::O_CREAT
} else if self.truncate {
libc::O_CREAT | libc::O_TRUNC
} else if self.create {
libc::O_CREAT
} else {
0
};
let no_follow_flag = match self.follow {
None | Some(true) => 0,
Some(false) => libc::O_NOFOLLOW,
};
let common_flags = libc::O_CLOEXEC | libc::O_NOCTTY;
Ok(data_flags | create_flags | common_flags | no_follow_flag)
}
pub fn open_at(&self, d: &File, path: &Path) -> Result<File> {
let flags = self.get_flags()?;
self._open_at(d, path, flags)
}
pub fn open_dir_at(&self, d: &File, path: &Path) -> Result<File> {
if matches!((self.read, self.write), (false, OpenOptionsWriteMode::None)) {
return Err(std::io::Error::from_raw_os_error(libc::EINVAL));
}
let no_follow_flag = match self.follow {
None | Some(false) => libc::O_NOFOLLOW,
Some(true) => 0,
};
let flags = libc::O_RDONLY | no_follow_flag | libc::O_CLOEXEC | libc::O_NOCTTY;
self._open_at(d, path, flags)
}
#[cfg(not(any(
target_os = "aix",
target_os = "dragonfly",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd",
target_os = "illumos",
target_os = "solaris"
)))]
pub fn open_path_at(&self, d: &File, path: &Path) -> Result<File> {
let flags =
libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_PATH | libc::O_CLOEXEC | libc::O_NOCTTY;
self._open_at(d, path, flags)
}
fn _open_at(&self, d: &File, path: &Path, flags: i32) -> Result<File> {
let path = path.as_cstring()?;
let mode = self.mode.unwrap_or(0o777);
let fd = cvt_r(|| unsafe { openat64(d.as_raw_fd(), path.as_ptr(), flags, mode as c_int) })?;
Ok(unsafe { File::from_raw_fd(fd) })
}
pub fn mkdir_at(&self, d: &File, path: &Path) -> Result<File> {
let path = path.as_cstring()?;
let mode = self.mode.unwrap_or(0o777);
let mut mkdir_e = None;
match cvt_r(|| unsafe { mkdirat(d.as_raw_fd(), path.as_ptr(), mode) }) {
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
if self.create && !self.create_new {
mkdir_e = Some(e);
Ok(())
} else {
Err(e)
}
}
Err(e) => Err(e),
Ok(_) => Ok(()),
}?;
let flags = libc::O_CLOEXEC | libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_DIRECTORY;
let fd = cvt_r(|| unsafe { openat64(d.as_raw_fd(), path.as_ptr(), flags, mode as c_int) });
match fd {
Err(fd_e) => {
if let (Some(libc::ENOTDIR), Some(mkdir_e)) = (fd_e.raw_os_error(), mkdir_e) {
Err(mkdir_e)
} else {
Err(fd_e)
}
}
Ok(fd) => Ok(unsafe { File::from_raw_fd(fd) }),
}
}
pub fn symlink_at(
&self,
d: &File,
linkname: &Path,
_entry_type: LinkEntryType,
target: &Path,
) -> Result<()> {
let linkname = linkname.as_cstring()?;
let target = target.as_cstring()?;
cvt_r(|| unsafe { libc::symlinkat(target.as_ptr(), d.as_raw_fd(), linkname.as_ptr()) })
.map(|_| ())
}
pub fn rmdir_at(&self, d: &File, p: &Path) -> Result<()> {
self.unlinkat(d, p, libc::AT_REMOVEDIR)
}
pub fn unlink_at(&self, d: &File, p: &Path) -> Result<()> {
self.unlinkat(d, p, 0)
}
fn unlinkat(&self, d: &File, p: &Path, flags: c_int) -> Result<()> {
let path = p.as_cstring()?;
cvt_r(|| unsafe { libc::unlinkat(d.as_raw_fd(), path.as_ptr(), flags) }).map(|_| ())
}
}
pub trait OpenOptionsExt {
fn mode(&mut self, mode: mode_t) -> &mut Self;
}
impl OpenOptionsExt for OpenOptions {
fn mode(&mut self, mode: mode_t) -> &mut Self {
self._impl.mode = Some(mode);
self
}
}
#[derive(Debug)]
pub(crate) struct ReadDirImpl<'a> {
_phantom: PhantomData<&'a mut File>,
dir: Option<ptr::NonNull<libc::DIR>>,
}
unsafe impl<'a> Send for ReadDirImpl<'a> where Box<libc::DIR>: Send {}
unsafe impl<'a> Sync for ReadDirImpl<'a> where Box<libc::DIR>: Sync {}
impl<'a> ReadDirImpl<'a> {
#[allow(clippy::needless_pass_by_ref_mut)]
pub fn new(dir_file: &'a mut File) -> Result<Self> {
let new_fd =
cvt_r(|| unsafe { libc::fcntl(dir_file.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 0) })?;
let mut dir = Some(
ptr::NonNull::new(unsafe { libc::fdopendir(new_fd) }).ok_or_else(|| {
let _droppable = unsafe { File::from_raw_fd(new_fd) };
std::io::Error::last_os_error()
})?,
);
if let Some(d) = dir.as_mut() {
unsafe { libc::rewinddir(d.as_mut()) };
}
Ok(ReadDirImpl {
_phantom: PhantomData,
dir,
})
}
fn close_dir(&mut self) -> Result<()> {
if let Some(ref mut dir) = self.dir {
let result = unsafe { libc::closedir(dir.as_mut()) };
self.dir = None;
cvt_r(|| result)?;
}
Ok(())
}
}
impl Drop for ReadDirImpl<'_> {
fn drop(&mut self) {
let _ = self.close_dir();
}
}
impl Iterator for ReadDirImpl<'_> {
type Item = Result<DirEntryImpl>;
fn next(&mut self) -> Option<Self::Item> {
let dir = unsafe { self.dir?.as_mut() };
nix::Error::clear();
ptr::NonNull::new(unsafe { libc::readdir(dir) })
.map(|e| {
Ok(DirEntryImpl {
name: unsafe {
let c_str = std::ffi::CStr::from_ptr(e.as_ref().d_name.as_ptr());
let os_str = OsStr::from_bytes(c_str.to_bytes());
os_str.to_os_string()
},
})
})
.or_else(|| {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(0) {
None
} else {
Some(Err(err))
}
})
}
}
#[derive(Debug)]
pub(crate) struct DirEntryImpl {
name: OsString,
}
impl DirEntryImpl {
pub fn name(&self) -> &OsStr {
&self.name
}
}
#[cfg(test)]
mod tests {
use std::{
fs::{rename, File},
io::Result,
os::unix::prelude::MetadataExt,
};
use tempfile::TempDir;
use test_log::test;
use crate::{os::unix::OpenOptionsExt, testsupport::open_dir, OpenOptions};
#[test]
fn mkdirat_mode() -> Result<()> {
let tmp = TempDir::new()?;
let parent = tmp.path().join("parent");
let renamed_parent = tmp.path().join("renamed-parent");
std::fs::create_dir(&parent)?;
let parent_file = open_dir(&parent)?;
rename(parent, &renamed_parent)?;
let mut create_opt = OpenOptions::default();
create_opt.mode(0o700);
let child: File = create_opt.mkdir_at(&parent_file, "child")?;
let expected = renamed_parent.join("child");
let metadata = expected.symlink_metadata()?;
assert!(metadata.is_dir());
assert_eq!(child.metadata()?.mode() & 0o777, 0o700);
Ok(())
}
}