Skip to content

Commit

Permalink
uucore/fs: fix canonicalize to match GNU behavior of realpath
Browse files Browse the repository at this point in the history
canonicalization
  • Loading branch information
niyaznigmatullin committed Jul 7, 2022
1 parent 04caf39 commit 4558305
Showing 1 changed file with 86 additions and 106 deletions.
192 changes: 86 additions & 106 deletions src/uucore/src/lib/features/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ use libc::{
S_IXUSR,
};
use std::borrow::Cow;
use std::collections::{HashSet, VecDeque};
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::hash::Hash;
use std::io::Error as IOError;
use std::io::Result as IOResult;
use std::io::{Error, ErrorKind};
use std::io::{Error, ErrorKind, Result as IOResult};
#[cfg(unix)]
use std::os::unix::{fs::MetadataExt, io::AsRawFd};
use std::path::{Component, Path, PathBuf};
Expand Down Expand Up @@ -233,47 +233,49 @@ pub fn is_symlink<P: AsRef<Path>>(path: P) -> bool {
fs::symlink_metadata(path).map_or(false, |m| m.file_type().is_symlink())
}

fn resolve<P: AsRef<Path>>(original: P) -> Result<PathBuf, (bool, PathBuf, IOError)> {
const MAX_LINKS_FOLLOWED: u32 = 255;
let mut followed = 0;
let mut result = original.as_ref().to_path_buf();
let mut symlink_is_absolute = false;
let mut first_resolution = None;

loop {
if followed == MAX_LINKS_FOLLOWED {
return Err((
symlink_is_absolute,
// When we hit MAX_LINKS_FOLLOWED we should return the first resolution (that's what GNU does - for whatever reason)
first_resolution.unwrap(),
Error::new(ErrorKind::InvalidInput, "maximum links followed"),
));
}
fn resolve_symlink<P: AsRef<Path>>(path: P) -> IOResult<Option<PathBuf>> {
let result = if fs::symlink_metadata(&path)?.file_type().is_symlink() {
Some(fs::read_link(&path)?)
} else {
None
};
Ok(result)
}

match fs::symlink_metadata(&result) {
Ok(meta) => {
if !meta.file_type().is_symlink() {
break;
}
}
Err(e) => return Err((symlink_is_absolute, result, e)),
}
enum OwningComponent {
Prefix(OsString),
RootDir,
CurDir,
ParentDir,
Normal(OsString),
}

followed += 1;
match fs::read_link(&result) {
Ok(path) => {
result.pop();
symlink_is_absolute = path.is_absolute();
result.push(path);
}
Err(e) => return Err((symlink_is_absolute, result, e)),
impl OwningComponent {
fn as_os_str(&self) -> &OsStr {
match self {
OwningComponent::Prefix(s) => s.as_os_str(),
OwningComponent::RootDir => Component::RootDir.as_os_str(),
OwningComponent::CurDir => Component::CurDir.as_os_str(),
OwningComponent::ParentDir => Component::ParentDir.as_os_str(),
OwningComponent::Normal(s) => s.as_os_str(),
}
}

if first_resolution.is_none() {
first_resolution = Some(result.clone());
fn from(comp: &Component) -> Self {
match comp {
Component::Prefix(_) => OwningComponent::Prefix(comp.as_os_str().to_os_string()),
Component::RootDir => OwningComponent::RootDir,
Component::CurDir => OwningComponent::CurDir,
Component::ParentDir => OwningComponent::ParentDir,
Component::Normal(s) => OwningComponent::Normal(s.to_os_string()),
}
}
Ok(result)
}

impl<'a> Into<OwningComponent> for Component<'a> {
fn into(self) -> OwningComponent {
OwningComponent::from(&self)
}
}

/// Return the canonical, absolute form of a path.
Expand Down Expand Up @@ -307,94 +309,72 @@ pub fn canonicalize<P: AsRef<Path>>(
miss_mode: MissingHandling,
res_mode: ResolveMode,
) -> IOResult<PathBuf> {
// Create an absolute path
const SYMLINKS_TO_LOOK_FOR_LOOPS: i32 = 20;
let original = original.as_ref();
let original = if original.is_absolute() {
original.to_path_buf()
} else {
let current_dir = env::current_dir()?;
dunce::canonicalize(current_dir)?.join(original)
};

let path = if res_mode == ResolveMode::Logical {
normalize_path(&original)
} else {
original
};
let mut parts: VecDeque<OwningComponent> = path.components().map(|part| part.into()).collect();
let mut result = PathBuf::new();
let mut parts = vec![];

// Split path by directory separator; add prefix (Windows-only) and root
// directory to final path buffer; add remaining parts to temporary
// vector for canonicalization.
for part in original.components() {
let mut followed_symlinks = 0;
let mut visited_files = HashSet::new();
while let Some(part) = parts.pop_front() {
match part {
Component::Prefix(_) | Component::RootDir => {
result.push(part.as_os_str());
OwningComponent::Prefix(s) => {
result.push(s);
continue;
}
Component::CurDir => (),
Component::ParentDir => {
if res_mode == ResolveMode::Logical {
parts.pop();
} else {
parts.push(part.as_os_str());
}
OwningComponent::RootDir | OwningComponent::Normal(..) => {
result.push(part.as_os_str());
}
Component::Normal(_) => {
parts.push(part.as_os_str());
OwningComponent::CurDir => {}
OwningComponent::ParentDir => {
result.pop();
}
}
}

// Resolve the symlinks where possible
if !parts.is_empty() {
for part in parts[..parts.len() - 1].iter() {
result.push(part);

//resolve as we go to handle long relative paths on windows
if res_mode == ResolveMode::Physical {
result = normalize_path(&result);
}

if res_mode == ResolveMode::None {
continue;
}

match resolve(&result) {
Err((_, path, e)) => {
if miss_mode == MissingHandling::Missing {
result = path;
} else {
return Err(e);
}
if res_mode == ResolveMode::None {
continue;
}
match resolve_symlink(&result) {
Ok(Some(link_path)) => {
for link_part in link_path.components().rev() {
parts.push_front(link_part.into());
}
Ok(path) => {
result = path;
if followed_symlinks < SYMLINKS_TO_LOOK_FOR_LOOPS {
followed_symlinks += 1;
} else {
let file_info =
FileInformation::from_path(&result.parent().unwrap(), false).unwrap();
let mut path_to_follow = PathBuf::new();
for part in parts.iter() {
path_to_follow.push(part.as_os_str());
}
if !visited_files.insert((file_info, path_to_follow)) {
result.metadata()?; // try to make ELOOP like error, TODO use ErrorKind::FilesystemLoop when stable
return Err(Error::new(
ErrorKind::InvalidInput,
"Too many levels of symbolic links",
));
}
}
result.pop();
}
}

result.push(parts.last().unwrap());

if res_mode == ResolveMode::None {
return Ok(result);
}

match resolve(&result) {
Err((is_absolute, path, err)) => {
// If the resolved symlink is an absolute path and non-existent,
// `realpath` throws no such file error.
Err(e) => {
if miss_mode == MissingHandling::Existing
|| (err.kind() == ErrorKind::NotFound
&& is_absolute
&& miss_mode == MissingHandling::Normal)
|| (miss_mode == MissingHandling::Normal && !parts.is_empty())
{
return Err(err);
} else {
result = path;
return Err(e);
}
}
Ok(path) => {
result = path;
}
}
if res_mode == ResolveMode::Physical {
result = normalize_path(&result);
_ => {}
}
}
Ok(result)
Expand Down

0 comments on commit 4558305

Please sign in to comment.