use super::{readlink_one, CanonicalPath, CowComponent};
use crate::fs::{
dir_path_options, errors, open_unchecked, path_has_trailing_dot, path_requires_dir,
stat_unchecked, FollowSymlinks, MaybeOwnedFile, Metadata, OpenOptions, OpenUncheckedError,
};
use std::{
ffi::OsStr,
fs, io, mem,
path::{Component, Path, PathBuf},
};
#[cfg(windows)]
use {crate::fs::SymlinkKind, winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY};
pub(crate) fn open(start: &fs::File, path: &Path, options: &OpenOptions) -> io::Result<fs::File> {
let mut symlink_count = 0;
let start = MaybeOwnedFile::borrowed(start);
let maybe_owned = internal_open(start, path, options, &mut symlink_count, None)?;
maybe_owned.into_file(options)
}
struct Context<'start> {
base: MaybeOwnedFile<'start>,
dirs: Vec<MaybeOwnedFile<'start>>,
components: Vec<CowComponent<'start>>,
canonical_path: CanonicalPath<'start>,
dir_required: bool,
dir_precluded: bool,
trailing_dot: bool,
reuse: PathBuf,
#[cfg(racy_asserts)]
start_clone: MaybeOwnedFile<'start>,
}
impl<'start> Context<'start> {
fn new(
start: MaybeOwnedFile<'start>,
path: &'start Path,
_options: &OpenOptions,
canonical_path: Option<&'start mut PathBuf>,
) -> Self {
let components = path
.components()
.rev()
.map(CowComponent::borrowed)
.collect::<Vec<_>>();
#[cfg(racy_asserts)]
let start_clone = MaybeOwnedFile::owned(start.try_clone().unwrap());
Self {
base: start,
dirs: Vec::with_capacity(components.len()),
components,
canonical_path: CanonicalPath::new(canonical_path),
dir_required: path_requires_dir(path),
#[cfg(not(windows))]
dir_precluded: _options.write || _options.append,
#[cfg(windows)]
dir_precluded: false,
trailing_dot: path_has_trailing_dot(path),
reuse: PathBuf::new(),
#[cfg(racy_asserts)]
start_clone,
}
}
fn at_last_component(&self) -> bool {
self.components.is_empty() && !self.trailing_dot
}
fn check_access(&self, component: Component) -> io::Result<()> {
#[cfg(not(windows))]
{
#[cfg(any(target_os = "emscripten", target_os = "android"))]
let at_flags = posish::fs::AtFlags::empty();
#[cfg(not(any(target_os = "emscripten", target_os = "android")))]
let at_flags = posish::fs::AtFlags::EACCESS;
posish::fs::accessat(
&*self.base,
component.as_os_str(),
posish::fs::Access::EXEC_OK,
at_flags,
)
}
#[cfg(windows)]
crate::fs::open_dir_unchecked(&self.base, component.as_os_str().as_ref()).map(|_| ())
}
fn cur_dir(&mut self) -> io::Result<()> {
if self.at_last_component() {
if self.dir_precluded {
return Err(errors::is_directory());
}
if !self.base.metadata()?.is_dir() {
return Err(errors::is_not_directory());
}
self.canonical_path.push(Component::CurDir.as_os_str());
}
self.check_access(Component::CurDir)?;
Ok(())
}
fn parent_dir(&mut self) -> io::Result<()> {
#[cfg(racy_asserts)]
if !self.dirs.is_empty() {
assert_different_file!(&self.start_clone, &self.base);
}
if self.at_last_component() && self.dir_precluded {
return Err(errors::is_directory());
}
match self.dirs.pop() {
Some(dir) => {
self.check_access(Component::ParentDir)?;
self.base = dir;
}
None => return Err(errors::escape_attempt()),
}
assert!(self.canonical_path.pop());
Ok(())
}
fn normal(
&mut self,
one: &OsStr,
options: &OpenOptions,
symlink_count: &mut u8,
) -> io::Result<()> {
if self.components.is_empty() && self.dir_required && self.dir_precluded {
return Err(errors::is_directory());
}
let use_options = if self.at_last_component() {
options.clone()
} else {
dir_path_options()
};
let dir_required = self.dir_required || use_options.dir_required;
#[allow(clippy::redundant_clone)]
match open_unchecked(
&self.base,
one.as_ref(),
use_options
.clone()
.follow(FollowSymlinks::No)
.dir_required(dir_required),
) {
Ok(file) => {
#[cfg(target_os = "linux")]
if should_emulate_o_path(&use_options) {
match readlink_one(
&file,
Default::default(),
symlink_count,
mem::take(&mut self.reuse),
) {
Ok(destination) => {
self.dir_required |=
self.components.is_empty() && path_requires_dir(&destination);
self.trailing_dot |= path_has_trailing_dot(&destination);
self.components
.extend(destination.components().rev().map(CowComponent::owned));
self.reuse = destination;
return Ok(());
}
Err(err) if err.kind() == io::ErrorKind::NotFound => (),
Err(err) => return Err(err),
}
}
let prev_base = self.base.descend_to(MaybeOwnedFile::owned(file));
self.dirs.push(prev_base);
self.canonical_path.push(one);
if self.trailing_dot {
self.check_access(Component::CurDir)?;
}
Ok(())
}
#[cfg(not(windows))]
Err(OpenUncheckedError::Symlink(err, ())) => {
self.maybe_last_component_symlink(one, symlink_count, options.follow, err)
}
#[cfg(windows)]
Err(OpenUncheckedError::Symlink(err, SymlinkKind::Dir)) => {
self.dir_required |= self.components.is_empty();
self.maybe_last_component_symlink(one, symlink_count, options.follow, err)
}
#[cfg(windows)]
Err(OpenUncheckedError::Symlink(err, SymlinkKind::File)) => {
self.dir_precluded = true;
self.maybe_last_component_symlink(one, symlink_count, options.follow, err)
}
Err(OpenUncheckedError::NotFound(err)) => Err(err),
Err(OpenUncheckedError::Other(err)) => {
if self.at_last_component() && err.kind() != io::ErrorKind::InvalidInput {
self.canonical_path.push(one);
self.canonical_path.complete();
}
Err(err)
}
}
}
fn symlink(&mut self, one: &OsStr, symlink_count: &mut u8) -> io::Result<()> {
let destination = readlink_one(&self.base, one, symlink_count, mem::take(&mut self.reuse))?;
self.dir_required |= self.components.is_empty() && path_requires_dir(&destination);
self.trailing_dot |= path_has_trailing_dot(&destination);
self.components
.extend(destination.components().rev().map(CowComponent::owned));
self.reuse = destination;
Ok(())
}
fn maybe_last_component_symlink(
&mut self,
one: &OsStr,
symlink_count: &mut u8,
follow: FollowSymlinks,
err: io::Error,
) -> io::Result<()> {
if follow == FollowSymlinks::No && self.at_last_component() {
self.canonical_path.push(one);
self.canonical_path.complete();
return Err(err);
}
self.symlink(one, symlink_count)
}
}
pub(super) fn internal_open<'start>(
start: MaybeOwnedFile<'start>,
path: &'start Path,
options: &OpenOptions,
symlink_count: &mut u8,
canonical_path: Option<&'start mut PathBuf>,
) -> io::Result<MaybeOwnedFile<'start>> {
if path.as_os_str().is_empty() {
return Err(errors::no_such_file_or_directory());
}
let mut ctx = Context::new(start, path, options, canonical_path);
while let Some(c) = ctx.components.pop() {
match c {
CowComponent::PrefixOrRootDir => return Err(errors::escape_attempt()),
CowComponent::CurDir => ctx.cur_dir()?,
CowComponent::ParentDir => ctx.parent_dir()?,
CowComponent::Normal(one) => ctx.normal(&one, options, symlink_count)?,
}
}
#[cfg(racy_asserts)]
check_internal_open(&ctx, path, options);
ctx.canonical_path.complete();
Ok(ctx.base)
}
pub(crate) fn stat<'start>(
start: &fs::File,
path: &'start Path,
follow: FollowSymlinks,
) -> io::Result<Metadata> {
if path.as_os_str().is_empty() {
return Err(errors::no_such_file_or_directory());
}
let mut options = OpenOptions::new();
options.follow(follow);
let mut symlink_count = 0;
let mut ctx = Context::new(MaybeOwnedFile::borrowed(start), path, &options, None);
assert!(!ctx.dir_precluded);
while let Some(c) = ctx.components.pop() {
match c {
CowComponent::PrefixOrRootDir => return Err(errors::escape_attempt()),
CowComponent::CurDir => ctx.cur_dir()?,
CowComponent::ParentDir => ctx.parent_dir()?,
CowComponent::Normal(one) => {
if ctx.at_last_component() {
let stat = stat_unchecked(&ctx.base, one.as_ref(), FollowSymlinks::No)?;
if options.follow == FollowSymlinks::No || !stat.file_type().is_symlink() {
if stat.is_dir() {
if ctx.dir_precluded {
return Err(errors::is_directory());
}
} else if ctx.dir_required {
return Err(errors::is_not_directory());
}
return Ok(stat);
}
#[cfg(windows)]
if stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 {
ctx.dir_required = true;
} else {
ctx.dir_precluded = true;
}
ctx.symlink(&one, &mut symlink_count)?
} else {
ctx.normal(&one, &options, &mut symlink_count)?
}
}
}
}
ctx.base.metadata().map(Metadata::from_std)
}
#[cfg(target_os = "linux")]
fn should_emulate_o_path(use_options: &OpenOptions) -> bool {
(use_options.ext.custom_flags & libc::O_PATH) == libc::O_PATH
&& use_options.follow == FollowSymlinks::Yes
}
#[cfg(racy_asserts)]
fn check_internal_open(ctx: &Context, path: &Path, options: &OpenOptions) {
match open_unchecked(
&ctx.start_clone,
ctx.canonical_path.debug.as_ref(),
options
.clone()
.create(false)
.create_new(false)
.truncate(false),
) {
Ok(unchecked_file) => {
assert_same_file!(
&ctx.base,
&unchecked_file,
"path resolution inconsistency: start='{:?}', path='{}'; canonical_path='{}'",
ctx.start_clone,
path.display(),
ctx.canonical_path.debug.display(),
);
}
Err(_unchecked_error) => {
}
}
}