use crate::error::{FsError, Result};
use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ReadOptions {
pub require_single_link: bool,
pub share_read_only: bool,
}
impl ReadOptions {
pub const fn new() -> Self {
Self {
require_single_link: false,
share_read_only: false,
}
}
pub const fn require_single_link(mut self, yes: bool) -> Self {
self.require_single_link = yes;
self
}
pub const fn share_read_only(mut self, yes: bool) -> Self {
self.share_read_only = yes;
self
}
}
pub fn final_path(file: &File) -> io::Result<PathBuf> {
final_path_impl(file)
}
#[cfg(target_vendor = "apple")]
fn final_path_impl(file: &File) -> io::Result<PathBuf> {
use std::ffi::OsStr;
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStrExt;
let mut buffer = [0u8; 1024];
let rc = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETPATH, buffer.as_mut_ptr()) };
if rc == -1 {
return Err(io::Error::last_os_error());
}
let len = buffer.iter().position(|b| *b == 0).unwrap_or(buffer.len());
Ok(PathBuf::from(OsStr::from_bytes(&buffer[..len])))
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn final_path_impl(file: &File) -> io::Result<PathBuf> {
use std::os::fd::AsRawFd;
std::fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd()))
}
#[cfg(all(
unix,
not(any(target_vendor = "apple", target_os = "linux", target_os = "android"))
))]
fn final_path_impl(_file: &File) -> io::Result<PathBuf> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"the final path of a handle is not available on this platform",
))
}
#[cfg(windows)]
fn final_path_impl(file: &File) -> io::Result<PathBuf> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::os::windows::io::AsRawHandle;
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetFinalPathNameByHandleW(
handle: *mut std::ffi::c_void,
buffer: *mut u16,
len: u32,
flags: u32,
) -> u32;
}
let mut buf = vec![0u16; 512];
loop {
let n = unsafe {
GetFinalPathNameByHandleW(
file.as_raw_handle().cast(),
buf.as_mut_ptr(),
buf.len() as u32,
0, )
};
if n == 0 {
return Err(io::Error::last_os_error());
}
if (n as usize) < buf.len() {
buf.truncate(n as usize);
return Ok(crate::plain::plain_path(PathBuf::from(
OsString::from_wide(&buf),
)));
}
buf.resize(n as usize + 1, 0);
}
}
#[cfg(not(any(unix, windows)))]
fn final_path_impl(_file: &File) -> io::Result<PathBuf> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"the final path of a handle is not available on this platform",
))
}
pub fn link_count(file: &File) -> io::Result<u64> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
Ok(file.metadata()?.nlink())
}
#[cfg(windows)]
{
crate::file_key::windows_info(file).map(|(_, links)| u64::from(links))
}
#[cfg(not(any(unix, windows)))]
{
let _ = file;
Err(io::Error::new(
io::ErrorKind::Unsupported,
"link counts are not available on this platform",
))
}
}
pub(crate) fn finish_read_open(file: File, shown: &Path, options: &ReadOptions) -> Result<File> {
let meta = file.metadata().map_err(|e| FsError::io(shown, e))?;
if !meta.is_file() {
return Err(FsError::NotAFile(shown.to_path_buf()));
}
#[cfg(unix)]
clear_nonblock(&file).map_err(|e| FsError::io(shown, e))?;
if options.require_single_link && link_count(&file).map_err(|e| FsError::io(shown, e))? != 1 {
return Err(FsError::HardLinked(shown.to_path_buf()));
}
Ok(file)
}
#[cfg(unix)]
pub(crate) fn clear_nonblock(file: &File) -> io::Result<()> {
use std::os::fd::AsRawFd;
unsafe {
let flags = libc::fcntl(file.as_raw_fd(), libc::F_GETFL);
if flags == -1 {
return Err(io::Error::last_os_error());
}
if flags & libc::O_NONBLOCK != 0
&& libc::fcntl(file.as_raw_fd(), libc::F_SETFL, flags & !libc::O_NONBLOCK) == -1
{
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub fn open_read_nofollow(path: &Path, options: &ReadOptions) -> Result<File> {
let file = open_leaf(path, options)?;
finish_read_open(file, path, options)
}
#[cfg(unix)]
fn open_leaf(path: &Path, _options: &ReadOptions) -> Result<File> {
use std::os::unix::fs::OpenOptionsExt;
match std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK)
.open(path)
{
Ok(file) => Ok(file),
Err(e) if e.raw_os_error() == Some(libc::ELOOP) => {
Err(FsError::Symlink(path.to_path_buf()))
}
Err(e) => Err(FsError::io(path, e)),
}
}
#[cfg(windows)]
fn open_leaf(path: &Path, options: &ReadOptions) -> Result<File> {
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
const FILE_SHARE_READ: u32 = 0x1;
const FILE_SHARE_READ_WRITE_DELETE: u32 = 0x7;
let file = std::fs::OpenOptions::new()
.read(true)
.share_mode(if options.share_read_only {
FILE_SHARE_READ
} else {
FILE_SHARE_READ_WRITE_DELETE
})
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)
.map_err(|e| FsError::io(path, e))?;
let meta = file.metadata().map_err(|e| FsError::io(path, e))?;
if meta.file_type().is_symlink() {
return Err(FsError::Symlink(path.to_path_buf()));
}
Ok(file)
}
#[cfg(not(any(unix, windows)))]
fn open_leaf(path: &Path, _options: &ReadOptions) -> Result<File> {
if let Ok(meta) = std::fs::symlink_metadata(path) {
if meta.file_type().is_symlink() {
return Err(FsError::Symlink(path.to_path_buf()));
}
}
File::open(path).map_err(|e| FsError::io(path, e))
}
#[cfg(test)]
mod tests {
use super::*;
fn file_in(dir: &Path, name: &str, bytes: &[u8]) -> PathBuf {
let path = dir.join(name);
std::fs::write(&path, bytes).unwrap();
path
}
#[test]
fn a_regular_file_opens_and_reads() {
let dir = tempfile::tempdir().unwrap();
let path = file_in(dir.path(), "f", b"hello");
let mut file = open_read_nofollow(&path, &ReadOptions::new()).unwrap();
let mut text = String::new();
io::Read::read_to_string(&mut file, &mut text).unwrap();
assert_eq!(text, "hello");
}
#[test]
fn a_missing_file_is_a_not_found_error() {
let dir = tempfile::tempdir().unwrap();
let err = open_read_nofollow(&dir.path().join("absent"), &ReadOptions::new()).unwrap_err();
assert!(err.is_not_found(), "{err:?}");
}
#[test]
fn a_directory_is_not_a_file() {
let dir = tempfile::tempdir().unwrap();
let err = open_read_nofollow(dir.path(), &ReadOptions::new()).unwrap_err();
assert!(
matches!(err, FsError::NotAFile(_) | FsError::Io { .. }),
"{err:?}"
);
}
#[test]
fn a_nul_byte_in_the_path_is_refused() {
let dir = tempfile::tempdir().unwrap();
let err = open_read_nofollow(&dir.path().join("a\0b"), &ReadOptions::new()).unwrap_err();
assert!(matches!(err, FsError::Io { .. }), "{err:?}");
}
#[test]
fn link_count_counts_hard_links() {
let dir = tempfile::tempdir().unwrap();
let path = file_in(dir.path(), "f", b"x");
assert_eq!(link_count(&File::open(&path).unwrap()).unwrap(), 1);
std::fs::hard_link(&path, dir.path().join("g")).unwrap();
assert_eq!(link_count(&File::open(&path).unwrap()).unwrap(), 2);
}
#[test]
fn a_hard_linked_file_is_refused_only_when_a_single_link_is_required() {
let dir = tempfile::tempdir().unwrap();
let path = file_in(dir.path(), "f", b"x");
let strict = ReadOptions::new().require_single_link(true);
assert!(open_read_nofollow(&path, &strict).is_ok());
std::fs::hard_link(&path, dir.path().join("alias")).unwrap();
let err = open_read_nofollow(&path, &strict).unwrap_err();
assert!(matches!(err, FsError::HardLinked(_)), "{err:?}");
assert!(open_read_nofollow(&path, &ReadOptions::new()).is_ok());
}
#[cfg(any(target_vendor = "apple", target_os = "linux", windows))]
#[test]
fn final_path_names_the_real_file() {
let dir = tempfile::tempdir().unwrap();
let path = file_in(dir.path(), "real.txt", b"x");
let file = File::open(&path).unwrap();
let want = crate::plain::canonicalize_plain(&path).unwrap();
let got = final_path(&file).unwrap();
assert_eq!(
got.to_string_lossy().to_lowercase(),
want.to_string_lossy().to_lowercase()
);
}
#[cfg(unix)]
mod unix {
use super::*;
use std::os::unix::fs::symlink;
#[test]
fn a_symlink_at_the_final_component_is_refused_even_when_it_points_inside() {
let dir = tempfile::tempdir().unwrap();
let target = file_in(dir.path(), "target", b"secret");
symlink(&target, dir.path().join("link")).unwrap();
let err =
open_read_nofollow(&dir.path().join("link"), &ReadOptions::new()).unwrap_err();
assert!(matches!(err, FsError::Symlink(_)), "{err:?}");
}
#[test]
fn a_dangling_symlink_is_a_symlink_error() {
let dir = tempfile::tempdir().unwrap();
symlink(dir.path().join("nowhere"), dir.path().join("dangling")).unwrap();
let err =
open_read_nofollow(&dir.path().join("dangling"), &ReadOptions::new()).unwrap_err();
assert!(matches!(err, FsError::Symlink(_)), "{err:?}");
}
#[test]
fn a_fifo_is_refused_without_blocking() {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("pipe");
let c = CString::new(path.as_os_str().as_bytes()).unwrap();
assert_eq!(unsafe { libc::mkfifo(c.as_ptr(), 0o600) }, 0);
let err = open_read_nofollow(&path, &ReadOptions::new()).unwrap_err();
assert!(matches!(err, FsError::NotAFile(_)), "{err:?}");
}
#[test]
fn the_returned_handle_is_blocking_again() {
use std::os::fd::AsRawFd;
let dir = tempfile::tempdir().unwrap();
let path = file_in(dir.path(), "f", b"x");
let file = open_read_nofollow(&path, &ReadOptions::new()).unwrap();
let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
assert_eq!(flags & libc::O_NONBLOCK, 0);
}
}
#[cfg(windows)]
mod windows {
use super::*;
#[test]
fn share_read_only_refuses_a_concurrent_writer_in_both_directions() {
let dir = tempfile::tempdir().unwrap();
let path = file_in(dir.path(), "f", b"x");
let shared = ReadOptions::new().share_read_only(true);
let reader = open_read_nofollow(&path, &shared).unwrap();
let writer = std::fs::OpenOptions::new().write(true).open(&path);
assert_eq!(
writer.unwrap_err().raw_os_error(),
Some(32),
"sharing violation"
);
drop(reader);
let writer = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
let err = open_read_nofollow(&path, &shared).unwrap_err();
assert!(
matches!(&err, FsError::Io { source, .. } if source.raw_os_error() == Some(32)),
"{err:?}"
);
drop(writer);
}
#[test]
fn a_symlink_or_junction_at_the_final_component_is_refused() {
let dir = tempfile::tempdir().unwrap();
let target = file_in(dir.path(), "target", b"x");
let link = dir.path().join("link");
if std::os::windows::fs::symlink_file(&target, &link).is_ok() {
let err = open_read_nofollow(&link, &ReadOptions::new()).unwrap_err();
assert!(matches!(err, FsError::Symlink(_)), "{err:?}");
}
}
}
}