1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

Realpath symlinks handling, solves issue #3669 (#3703)

This commit is contained in:
Niyaz Nigmatullin 2022-07-10 17:49:25 +03:00 committed by GitHub
parent 392ae87a9f
commit 9d285e953d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 243 additions and 218 deletions

View file

@ -351,7 +351,7 @@ fn cat_path(
if let Some(out_info) = out_info { if let Some(out_info) = out_info {
if out_info.file_size() != 0 if out_info.file_size() != 0
&& FileInformation::from_file(&file).as_ref() == Some(out_info) && FileInformation::from_file(&file).ok().as_ref() == Some(out_info)
{ {
return Err(CatError::OutputIsInput); return Err(CatError::OutputIsInput);
} }
@ -367,7 +367,7 @@ fn cat_path(
} }
fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> { fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> {
let out_info = FileInformation::from_file(&std::io::stdout()); let out_info = FileInformation::from_file(&std::io::stdout()).ok();
let mut state = OutputState { let mut state = OutputState {
line_number: 1, line_number: 1,

View file

@ -19,10 +19,6 @@ extern crate uucore;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::format_usage; use uucore::format_usage;
use uucore::fs::FileInformation; use uucore::fs::FileInformation;
#[cfg(windows)]
use winapi::um::fileapi::CreateFileW;
#[cfg(windows)]
use winapi::um::fileapi::GetFileInformationByHandle;
use std::borrow::Cow; use std::borrow::Cow;
@ -35,22 +31,17 @@ use std::collections::HashSet;
use std::env; use std::env;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::ffi::CString; use std::ffi::CString;
#[cfg(windows)]
use std::ffi::OsStr;
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io; use std::io;
use std::io::{stdin, stdout, Write}; use std::io::{stdin, stdout, Write};
use std::mem;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::{FileTypeExt, PermissionsExt}; use std::os::unix::fs::{FileTypeExt, PermissionsExt};
#[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "android"))]
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
use std::path::{Path, PathBuf, StripPrefixError}; use std::path::{Path, PathBuf, StripPrefixError};
use std::str::FromStr; use std::str::FromStr;
use std::string::ToString; use std::string::ToString;
@ -793,49 +784,20 @@ fn preserve_hardlinks(
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
{ {
if !source.is_dir() { if !source.is_dir() {
unsafe { let info = match FileInformation::from_path(source, false) {
let inode: u64; Ok(info) => info,
let nlinks: u64; Err(e) => {
return Err(format!("cannot stat {}: {}", source.quote(), e,).into());
}
};
#[cfg(unix)] #[cfg(unix)]
{ let inode = info.inode();
let src_path = CString::new(source.as_os_str().to_str().unwrap()).unwrap();
let mut stat = mem::zeroed();
if libc::lstat(src_path.as_ptr(), &mut stat) < 0 {
return Err(format!(
"cannot stat {}: {}",
source.quote(),
std::io::Error::last_os_error()
)
.into());
}
inode = stat.st_ino as u64;
nlinks = stat.st_nlink as u64;
}
#[cfg(windows)] #[cfg(windows)]
{ let inode = info.file_index();
let src_path: Vec<u16> = OsStr::new(source).encode_wide().collect();
#[allow(deprecated)] let nlinks = info.number_of_links();
let stat = mem::uninitialized();
let handle = CreateFileW(
src_path.as_ptr(),
winapi::um::winnt::GENERIC_READ,
winapi::um::winnt::FILE_SHARE_READ,
std::ptr::null_mut(),
0,
0,
std::ptr::null_mut(),
);
if GetFileInformationByHandle(handle, stat) != 0 {
return Err(format!(
"cannot get file information {:?}: {}",
source,
std::io::Error::last_os_error()
)
.into());
}
inode = ((*stat).nFileIndexHigh as u64) << 32 | (*stat).nFileIndexLow as u64;
nlinks = (*stat).nNumberOfLinks as u64;
}
for hard_link in hard_links.iter() { for hard_link in hard_links.iter() {
if hard_link.1 == inode { if hard_link.1 == inode {
@ -848,7 +810,6 @@ fn preserve_hardlinks(
} }
} }
} }
}
Ok(()) Ok(())
} }
@ -1227,7 +1188,7 @@ fn symlink_file(
{ {
std::os::windows::fs::symlink_file(source, dest).context(context)?; std::os::windows::fs::symlink_file(source, dest).context(context)?;
} }
if let Some(file_info) = FileInformation::from_path(dest, false) { if let Ok(file_info) = FileInformation::from_path(dest, false) {
symlinked_files.insert(file_info); symlinked_files.insert(file_info);
} }
Ok(()) Ok(())
@ -1245,7 +1206,8 @@ fn backup_dest(dest: &Path, backup_path: &Path) -> CopyResult<PathBuf> {
} }
fn handle_existing_dest(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { fn handle_existing_dest(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
if paths_refer_to_same_file(source, dest)? { let dereference_to_compare = options.dereference || !is_symlink(source);
if paths_refer_to_same_file(source, dest, dereference_to_compare) {
return Err(format!("{}: same file", context_for(source, dest)).into()); return Err(format!("{}: same file", context_for(source, dest)).into());
} }
@ -1253,7 +1215,7 @@ fn handle_existing_dest(source: &Path, dest: &Path, options: &Options) -> CopyRe
let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix); let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix);
if let Some(backup_path) = backup_path { if let Some(backup_path) = backup_path {
if paths_refer_to_same_file(source, &backup_path)? { if paths_refer_to_same_file(source, &backup_path, true) {
return Err(format!( return Err(format!(
"backing up {} might destroy source; {} not copied", "backing up {} might destroy source; {} not copied",
dest.quote(), dest.quote(),
@ -1313,7 +1275,8 @@ fn copy_file(
dest.display() dest.display()
))); )));
} }
if options.dereference && !dest.exists() { let copy_contents = options.dereference || !is_symlink(source);
if copy_contents && !dest.exists() {
return Err(Error::Error(format!( return Err(Error::Error(format!(
"not writing through dangling symlink '{}'", "not writing through dangling symlink '{}'",
dest.display() dest.display()
@ -1542,7 +1505,7 @@ fn copy_link(
} else { } else {
// we always need to remove the file to be able to create a symlink, // we always need to remove the file to be able to create a symlink,
// even if it is writeable. // even if it is writeable.
if dest.is_file() { if is_symlink(dest) || dest.is_file() {
fs::remove_file(dest)?; fs::remove_file(dest)?;
} }
dest.into() dest.into()
@ -1685,12 +1648,15 @@ pub fn localize_to_target(root: &Path, source: &Path, target: &Path) -> CopyResu
Ok(target.join(&local_to_root)) Ok(target.join(&local_to_root))
} }
pub fn paths_refer_to_same_file(p1: &Path, p2: &Path) -> io::Result<bool> { pub fn paths_refer_to_same_file(p1: &Path, p2: &Path, dereference: bool) -> bool {
// We have to take symlinks and relative paths into account. // We have to take symlinks and relative paths into account.
let pathbuf1 = canonicalize(p1, MissingHandling::Normal, ResolveMode::Logical)?; let res1 = FileInformation::from_path(p1, dereference);
let pathbuf2 = canonicalize(p2, MissingHandling::Normal, ResolveMode::Logical)?; let res2 = FileInformation::from_path(p2, dereference);
Ok(pathbuf1 == pathbuf2) match (res1, res2) {
(Ok(info1), Ok(info2)) => info1 == info2,
_ => false,
}
} }
pub fn path_has_prefix(p1: &Path, p2: &Path) -> io::Result<bool> { pub fn path_has_prefix(p1: &Path, p2: &Path) -> io::Result<bool> {

View file

@ -17,12 +17,14 @@ use libc::{
S_IXUSR, S_IXUSR,
}; };
use std::borrow::Cow; use std::borrow::Cow;
#[cfg(unix)]
use std::collections::HashSet;
use std::collections::VecDeque;
use std::env; use std::env;
use std::ffi::{OsStr, OsString};
use std::fs; use std::fs;
use std::hash::Hash; use std::hash::Hash;
use std::io::Error as IOError; use std::io::{Error, ErrorKind, Result as IOResult};
use std::io::Result as IOResult;
use std::io::{Error, ErrorKind};
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::{fs::MetadataExt, io::AsRawFd}; use std::os::unix::{fs::MetadataExt, io::AsRawFd};
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
@ -46,29 +48,23 @@ pub struct FileInformation(
impl FileInformation { impl FileInformation {
/// Get information from a currently open file /// Get information from a currently open file
#[cfg(unix)] #[cfg(unix)]
pub fn from_file(file: &impl AsRawFd) -> Option<Self> { pub fn from_file(file: &impl AsRawFd) -> IOResult<Self> {
if let Ok(x) = nix::sys::stat::fstat(file.as_raw_fd()) { let stat = nix::sys::stat::fstat(file.as_raw_fd())?;
Some(Self(x)) Ok(Self(stat))
} else {
None
}
} }
/// Get information from a currently open file /// Get information from a currently open file
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn from_file(file: &impl AsHandleRef) -> Option<Self> { pub fn from_file(file: &impl AsHandleRef) -> IOResult<Self> {
if let Ok(x) = winapi_util::file::information(file.as_handle_ref()) { let info = winapi_util::file::information(file.as_handle_ref())?;
Some(Self(x)) Ok(Self(info))
} else {
None
}
} }
/// Get information for a given path. /// Get information for a given path.
/// ///
/// If `path` points to a symlink and `dereference` is true, information about /// If `path` points to a symlink and `dereference` is true, information about
/// the link's target will be returned. /// the link's target will be returned.
pub fn from_path(path: impl AsRef<Path>, dereference: bool) -> Option<Self> { pub fn from_path(path: impl AsRef<Path>, dereference: bool) -> IOResult<Self> {
#[cfg(unix)] #[cfg(unix)]
{ {
let stat = if dereference { let stat = if dereference {
@ -76,11 +72,7 @@ impl FileInformation {
} else { } else {
nix::sys::stat::lstat(path.as_ref()) nix::sys::stat::lstat(path.as_ref())
}; };
if let Ok(stat) = stat { Ok(Self(stat?))
Some(Self(stat))
} else {
None
}
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@ -90,11 +82,8 @@ impl FileInformation {
if !dereference { if !dereference {
open_options.custom_flags(winapi::um::winbase::FILE_FLAG_OPEN_REPARSE_POINT); open_options.custom_flags(winapi::um::winbase::FILE_FLAG_OPEN_REPARSE_POINT);
} }
open_options let file = open_options.read(true).open(path.as_ref())?;
.read(true) Self::from_file(&file)
.open(path.as_ref())
.ok()
.and_then(|file| Self::from_file(&file))
} }
} }
@ -109,6 +98,23 @@ impl FileInformation {
self.0.file_size() self.0.file_size()
} }
} }
#[cfg(windows)]
pub fn file_index(&self) -> u64 {
self.0.file_index()
}
pub fn number_of_links(&self) -> u64 {
#[cfg(unix)]
return self.0.st_nlink as u64;
#[cfg(windows)]
return self.0.number_of_links() as u64;
}
#[cfg(unix)]
pub fn inode(&self) -> u64 {
self.0.st_ino as u64
}
} }
#[cfg(unix)] #[cfg(unix)]
@ -233,49 +239,47 @@ pub fn is_symlink<P: AsRef<Path>>(path: P) -> bool {
fs::symlink_metadata(path).map_or(false, |m| m.file_type().is_symlink()) 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)> { fn resolve_symlink<P: AsRef<Path>>(path: P) -> IOResult<Option<PathBuf>> {
const MAX_LINKS_FOLLOWED: u32 = 255; let result = if fs::symlink_metadata(&path)?.file_type().is_symlink() {
let mut followed = 0; Some(fs::read_link(&path)?)
let mut result = original.as_ref().to_path_buf(); } else {
let mut symlink_is_absolute = false; None
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"),
));
}
match fs::symlink_metadata(&result) {
Ok(meta) => {
if !meta.file_type().is_symlink() {
break;
}
}
Err(e) => return Err((symlink_is_absolute, result, e)),
}
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)),
}
if first_resolution.is_none() {
first_resolution = Some(result.clone());
}
}
Ok(result) Ok(result)
} }
enum OwningComponent {
Prefix(OsString),
RootDir,
CurDir,
ParentDir,
Normal(OsString),
}
impl OwningComponent {
fn as_os_str(&self) -> &OsStr {
match self {
Self::Prefix(s) => s.as_os_str(),
Self::RootDir => Component::RootDir.as_os_str(),
Self::CurDir => Component::CurDir.as_os_str(),
Self::ParentDir => Component::ParentDir.as_os_str(),
Self::Normal(s) => s.as_os_str(),
}
}
}
impl<'a> From<Component<'a>> for OwningComponent {
fn from(comp: Component<'a>) -> Self {
match comp {
Component::Prefix(_) => Self::Prefix(comp.as_os_str().to_os_string()),
Component::RootDir => Self::RootDir,
Component::CurDir => Self::CurDir,
Component::ParentDir => Self::ParentDir,
Component::Normal(s) => Self::Normal(s.to_os_string()),
}
}
}
/// Return the canonical, absolute form of a path. /// Return the canonical, absolute form of a path.
/// ///
/// This function is a generalization of [`std::fs::canonicalize`] that /// This function is a generalization of [`std::fs::canonicalize`] that
@ -307,7 +311,7 @@ pub fn canonicalize<P: AsRef<Path>>(
miss_mode: MissingHandling, miss_mode: MissingHandling,
res_mode: ResolveMode, res_mode: ResolveMode,
) -> IOResult<PathBuf> { ) -> IOResult<PathBuf> {
// Create an absolute path const SYMLINKS_TO_LOOK_FOR_LOOPS: i32 = 256;
let original = original.as_ref(); let original = original.as_ref();
let original = if original.is_absolute() { let original = if original.is_absolute() {
original.to_path_buf() original.to_path_buf()
@ -315,86 +319,72 @@ pub fn canonicalize<P: AsRef<Path>>(
let current_dir = env::current_dir()?; let current_dir = env::current_dir()?;
dunce::canonicalize(current_dir)?.join(original) 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 result = PathBuf::new();
let mut parts = vec![]; let mut followed_symlinks = 0;
#[cfg(unix)]
// Split path by directory separator; add prefix (Windows-only) and root let mut visited_files = HashSet::new();
// directory to final path buffer; add remaining parts to temporary while let Some(part) = parts.pop_front() {
// vector for canonicalization.
for part in original.components() {
match part { match part {
Component::Prefix(_) | Component::RootDir => { OwningComponent::Prefix(s) => {
result.push(s);
continue;
}
OwningComponent::RootDir | OwningComponent::Normal(..) => {
result.push(part.as_os_str()); result.push(part.as_os_str());
} }
Component::CurDir => (), OwningComponent::CurDir => {}
Component::ParentDir => { OwningComponent::ParentDir => {
if res_mode == ResolveMode::Logical { result.pop();
parts.pop();
} else {
parts.push(part.as_os_str());
} }
} }
Component::Normal(_) => {
parts.push(part.as_os_str());
}
}
}
// 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 { if res_mode == ResolveMode::None {
continue; continue;
} }
match resolve_symlink(&result) {
match resolve(&result) { Ok(Some(link_path)) => {
Err((_, path, e)) => { for link_part in link_path.components().rev() {
if miss_mode == MissingHandling::Missing { parts.push_front(link_part.into());
result = path; }
if followed_symlinks < SYMLINKS_TO_LOOK_FOR_LOOPS {
followed_symlinks += 1;
} else { } else {
#[cfg(unix)]
let has_loop = {
let file_info =
FileInformation::from_path(&result.parent().unwrap(), false).unwrap();
let mut path_to_follow = PathBuf::new();
for part in &parts {
path_to_follow.push(part.as_os_str());
}
!visited_files.insert((file_info, path_to_follow))
};
#[cfg(not(unix))]
let has_loop = true;
if has_loop {
return Err(Error::new(
ErrorKind::InvalidInput,
"Too many levels of symbolic links",
)); // TODO use ErrorKind::FilesystemLoop when stable
}
}
result.pop();
}
Err(e) => {
if miss_mode == MissingHandling::Existing
|| (miss_mode == MissingHandling::Normal && !parts.is_empty())
{
return Err(e); return Err(e);
} }
} }
Ok(path) => { _ => {}
result = path;
}
}
}
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.
if miss_mode == MissingHandling::Existing
|| (err.kind() == ErrorKind::NotFound
&& is_absolute
&& miss_mode == MissingHandling::Normal)
{
return Err(err);
} else {
result = path;
}
}
Ok(path) => {
result = path;
}
}
if res_mode == ResolveMode::Physical {
result = normalize_path(&result);
} }
} }
Ok(result) Ok(result)

View file

@ -1492,13 +1492,12 @@ fn test_copy_through_just_created_symlink() {
at.mkdir("a"); at.mkdir("a");
at.mkdir("b"); at.mkdir("b");
at.mkdir("c"); at.mkdir("c");
#[cfg(unix)] at.relative_symlink_file("../t", "a/1");
fs::symlink("../t", at.plus("a/1")).unwrap();
#[cfg(target_os = "windows")]
symlink_file("../t", at.plus("a/1")).unwrap();
at.touch("b/1"); at.touch("b/1");
at.write("b/1", "hello");
if create_t { if create_t {
at.touch("t"); at.touch("t");
at.write("t", "world");
} }
ucmd.arg("--no-dereference") ucmd.arg("--no-dereference")
.arg("a/1") .arg("a/1")
@ -1510,6 +1509,9 @@ fn test_copy_through_just_created_symlink() {
} else { } else {
"cp: will not copy 'b/1' through just-created symlink 'c\\1'" "cp: will not copy 'b/1' through just-created symlink 'c\\1'"
}); });
if create_t {
assert_eq!(at.read("a/1"), "world");
}
} }
} }
@ -1536,6 +1538,16 @@ fn test_copy_through_dangling_symlink_no_dereference() {
.no_stdout(); .no_stdout();
} }
#[test]
fn test_copy_through_dangling_symlink_no_dereference_2() {
let (at, mut ucmd) = at_and_ucmd!();
at.touch("file");
at.symlink_file("nonexistent", "target");
ucmd.args(&["-P", "file", "target"])
.fails()
.stderr_only("cp: not writing through dangling symlink 'target'");
}
#[test] #[test]
#[cfg(unix)] #[cfg(unix)]
fn test_cp_archive_on_nonexistent_file() { fn test_cp_archive_on_nonexistent_file() {
@ -1658,3 +1670,52 @@ fn test_cp_overriding_arguments() {
s.fixtures.remove("file2"); s.fixtures.remove("file2");
} }
} }
#[test]
fn test_copy_no_dereference_1() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("a");
at.mkdir("b");
at.touch("a/foo");
at.write("a/foo", "bar");
at.relative_symlink_file("../a/foo", "b/foo");
ucmd.args(&["-P", "a/foo", "b"]).fails();
}
#[test]
fn test_abuse_existing() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("a");
at.mkdir("b");
at.mkdir("c");
at.relative_symlink_file("../t", "a/1");
at.touch("b/1");
at.write("b/1", "hello");
at.relative_symlink_file("../t", "c/1");
at.touch("t");
at.write("t", "i");
ucmd.args(&["-dR", "a/1", "b/1", "c"])
.fails()
.stderr_contains(format!(
"will not copy 'b/1' through just-created symlink 'c{}1'",
if cfg!(windows) { "\\" } else { "/" }
));
assert_eq!(at.read("t"), "i");
}
#[test]
fn test_copy_same_symlink_no_dereference() {
let (at, mut ucmd) = at_and_ucmd!();
at.relative_symlink_file("t", "a");
at.relative_symlink_file("t", "b");
at.touch("t");
ucmd.args(&["-d", "a", "b"]).succeeds();
}
#[test]
fn test_copy_same_symlink_no_dereference_dangling() {
let (at, mut ucmd) = at_and_ucmd!();
at.relative_symlink_file("t", "a");
at.relative_symlink_file("t", "b");
ucmd.args(&["-d", "a", "b"]).succeeds();
}

View file

@ -1,6 +1,6 @@
use crate::common::util::*; use crate::common::util::*;
use std::path::Path; use std::path::{Path, MAIN_SEPARATOR};
static GIBBERISH: &str = "supercalifragilisticexpialidocious"; static GIBBERISH: &str = "supercalifragilisticexpialidocious";
@ -155,8 +155,8 @@ fn test_realpath_dangling() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
at.symlink_file("nonexistent-file", "link"); at.symlink_file("nonexistent-file", "link");
ucmd.arg("link") ucmd.arg("link")
.fails() .succeeds()
.stderr_contains("realpath: link: No such file or directory"); .stdout_contains(at.plus_as_string("nonexistent-file\n"));
} }
#[test] #[test]
@ -166,8 +166,8 @@ fn test_realpath_loop() {
at.symlink_file("3", "2"); at.symlink_file("3", "2");
at.symlink_file("1", "3"); at.symlink_file("1", "3");
ucmd.arg("1") ucmd.arg("1")
.succeeds() .fails()
.stdout_only(at.plus_as_string("2\n")); .stderr_contains("Too many levels of symbolic links");
} }
#[test] #[test]
@ -241,7 +241,6 @@ fn test_realpath_when_symlink_is_absolute_and_enoent() {
} }
#[test] #[test]
#[ignore = "issue #3669"]
fn test_realpath_when_symlink_part_is_missing() { fn test_realpath_when_symlink_part_is_missing() {
let (at, mut ucmd) = at_and_ucmd!(); let (at, mut ucmd) = at_and_ucmd!();
@ -254,10 +253,13 @@ fn test_realpath_when_symlink_part_is_missing() {
at.relative_symlink_file("../dir2/baz", "dir1/foo3"); at.relative_symlink_file("../dir2/baz", "dir1/foo3");
at.symlink_file("dir3/bar", "dir1/foo4"); at.symlink_file("dir3/bar", "dir1/foo4");
let expect1 = format!("dir2{}bar", MAIN_SEPARATOR);
let expect2 = format!("dir2{}baz", MAIN_SEPARATOR);
ucmd.args(&["dir1/foo1", "dir1/foo2", "dir1/foo3", "dir1/foo4"]) ucmd.args(&["dir1/foo1", "dir1/foo2", "dir1/foo3", "dir1/foo4"])
.run() .run()
.stdout_contains(at.plus_as_string("dir2/bar") + "\n") .stdout_contains(expect1 + "\n")
.stdout_contains(at.plus_as_string("dir2/baz") + "\n") .stdout_contains(expect2 + "\n")
.stderr_contains("realpath: dir1/foo2: No such file or directory\n") .stderr_contains("realpath: dir1/foo2: No such file or directory\n")
.stderr_contains("realpath: dir1/foo4: No such file or directory\n"); .stderr_contains("realpath: dir1/foo4: No such file or directory\n");
} }

View file

@ -22,6 +22,8 @@ use std::io::{Read, Result, Write};
use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file}; use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file};
#[cfg(windows)] #[cfg(windows)]
use std::os::windows::fs::{symlink_dir, symlink_file}; use std::os::windows::fs::{symlink_dir, symlink_file};
#[cfg(windows)]
use std::path::MAIN_SEPARATOR;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};
use std::rc::Rc; use std::rc::Rc;
@ -702,11 +704,13 @@ impl AtPath {
} }
pub fn relative_symlink_file(&self, original: &str, link: &str) { pub fn relative_symlink_file(&self, original: &str, link: &str) {
#[cfg(windows)]
let original = original.replace('/', &MAIN_SEPARATOR.to_string());
log_info( log_info(
"symlink", "symlink",
&format!("{},{}", original, &self.plus_as_string(link)), &format!("{},{}", &original, &self.plus_as_string(link)),
); );
symlink_file(original, &self.plus(link)).unwrap(); symlink_file(&original, &self.plus(link)).unwrap();
} }
pub fn symlink_dir(&self, original: &str, link: &str) { pub fn symlink_dir(&self, original: &str, link: &str) {
@ -722,11 +726,13 @@ impl AtPath {
} }
pub fn relative_symlink_dir(&self, original: &str, link: &str) { pub fn relative_symlink_dir(&self, original: &str, link: &str) {
#[cfg(windows)]
let original = original.replace('/', &MAIN_SEPARATOR.to_string());
log_info( log_info(
"symlink", "symlink",
&format!("{},{}", original, &self.plus_as_string(link)), &format!("{},{}", &original, &self.plus_as_string(link)),
); );
symlink_dir(original, &self.plus(link)).unwrap(); symlink_dir(&original, &self.plus(link)).unwrap();
} }
pub fn is_symlink(&self, path: &str) -> bool { pub fn is_symlink(&self, path: &str) -> bool {