use super::{CanonicalPath, CowComponent, read_link_one};
use crate::filesystem::primitives::{
FollowSymlinks, MaybeOwnedFile, Metadata, OpenOptions, OpenUncheckedError, dir_options, errors,
open_unchecked, path_has_trailing_dot, path_has_trailing_slash, stat_unchecked,
};
#[cfg(any(target_os = "android", target_os = "linux", target_os = "freebsd"))]
use rustix::fs::OFlags;
use std::ffi::OsStr;
use std::path::{Component, Path, PathBuf};
use std::{fs, io, mem};
#[cfg(windows)]
use {
crate::filesystem::primitives::{
SymlinkKind, open_dir_unchecked, path_really_has_trailing_dot,
},
windows_sys::Win32::Storage::FileSystem::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_slash: bool,
follow_with_dot: bool,
reuse: PathBuf,
}
impl<'start> Context<'start> {
fn new(
start: MaybeOwnedFile<'start>,
path: &'start Path,
_options: &OpenOptions,
canonical_path: Option<&'start mut PathBuf>,
) -> Self {
let trailing_slash = path_has_trailing_slash(path);
let trailing_dot = path_has_trailing_dot(path);
let trailing_dotdot = path.ends_with(Component::ParentDir);
let mut components: Vec<CowComponent> = Vec::new();
#[cfg(windows)]
{
for component in path.components().map(CowComponent::borrowed) {
match component {
CowComponent::ParentDir
if !components.is_empty() && components.last().unwrap().is_normal() =>
{
let _ = components.pop();
}
_ => components.push(component),
}
}
components.reverse();
}
#[cfg(not(windows))]
{
if trailing_dot {
components.push(CowComponent::CurDir);
}
components.extend(path.components().rev().map(CowComponent::borrowed));
}
Self {
base: start,
dirs: Vec::with_capacity(components.len()),
components,
canonical_path: CanonicalPath::new(canonical_path),
dir_required: trailing_slash,
#[cfg(not(windows))]
dir_precluded: _options.write || _options.append,
#[cfg(windows)]
dir_precluded: false,
trailing_slash,
follow_with_dot: trailing_dot | trailing_dotdot,
reuse: PathBuf::new(),
}
}
fn check_dot_access(&self) -> io::Result<()> {
#[cfg(not(windows))]
{
#[cfg(any(target_os = "emscripten", target_os = "android"))]
let at_flags = rustix::fs::AtFlags::empty();
#[cfg(not(any(target_os = "emscripten", target_os = "android")))]
let at_flags = rustix::fs::AtFlags::EACCESS;
Ok(rustix::fs::accessat(
&*self.base,
Component::CurDir.as_os_str(),
rustix::fs::Access::EXEC_OK,
at_flags,
)?)
}
#[cfg(windows)]
open_dir_unchecked(&self.base, Component::CurDir.as_ref()).map(|_| ())
}
fn cur_dir(&mut self) -> io::Result<()> {
Ok(())
}
fn parent_dir(&mut self) -> io::Result<()> {
match self.dirs.pop() {
Some(dir) => {
self.check_dot_access()?;
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<()> {
let use_options = if self.components.is_empty() {
options.clone()
} else {
dir_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(any(target_os = "android", target_os = "linux", target_os = "freebsd"))]
if should_emulate_o_path(&use_options) {
match read_link_one(
&file,
Default::default(),
symlink_count,
mem::take(&mut self.reuse),
) {
Ok(destination) => {
return self.push_symlink_destination(destination);
}
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);
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.components.is_empty() && 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 =
read_link_one(&self.base, one, symlink_count, mem::take(&mut self.reuse))?;
self.push_symlink_destination(destination)
}
fn push_symlink_destination(&mut self, destination: PathBuf) -> io::Result<()> {
let at_end = self.components.is_empty();
let trailing_slash = path_has_trailing_slash(&destination);
let trailing_dot = path_has_trailing_dot(&destination);
let trailing_dotdot = destination.ends_with(Component::ParentDir);
#[cfg(windows)]
{
let trailing_dot_really = path_really_has_trailing_dot(&destination);
if trailing_slash
|| (trailing_dot_really && destination.as_os_str() != Component::CurDir.as_os_str())
|| (trailing_dotdot && destination.as_os_str() != Component::ParentDir.as_os_str())
{
return Err(io::Error::from_raw_os_error(123));
}
let mut components: Vec<CowComponent> = Vec::new();
for component in destination.components().map(CowComponent::owned) {
match component {
CowComponent::ParentDir
if !components.is_empty() && components.last().unwrap().is_normal() =>
{
let _ = components.pop();
}
_ => components.push(component),
}
}
self.components.extend(components.into_iter().rev());
}
#[cfg(not(windows))]
{
if trailing_dot {
self.components.push(CowComponent::CurDir);
}
self.components
.extend(destination.components().rev().map(CowComponent::owned));
}
if at_end {
self.follow_with_dot |= trailing_dot | trailing_dotdot;
self.trailing_slash |= trailing_slash;
self.dir_required |= trailing_slash;
}
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.trailing_slash && self.components.is_empty() {
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)?,
}
}
ctx.canonical_path.complete();
if ctx.follow_with_dot {
if ctx.dir_precluded {
return Err(errors::is_directory());
}
ctx.base = MaybeOwnedFile::owned(open_unchecked(
&ctx.base,
Component::CurDir.as_ref(),
options,
)?);
}
Ok(ctx.base)
}
pub(crate) fn stat(start: &fs::File, path: &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.components.is_empty() {
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)?
}
}
}
}
if ctx.follow_with_dot {
if ctx.dir_precluded {
return Err(errors::is_directory());
}
ctx.check_dot_access()?;
}
Metadata::from_file(&ctx.base)
}
#[cfg(any(target_os = "android", target_os = "linux", target_os = "freebsd"))]
fn should_emulate_o_path(use_options: &OpenOptions) -> bool {
(use_options.ext.custom_flags & (OFlags::PATH.bits() as i32)) == (OFlags::PATH.bits() as i32)
&& use_options.follow == FollowSymlinks::Yes
}