From 1dcc362192def3545eb1f9d4c6aece2bd5faaf38 Mon Sep 17 00:00:00 2001 From: martell0x1 Date: Tue, 3 Nov 2026 03:26:01 +0200 Subject: [PATCH 1/7] fix: Applied physical directory checks so realpath doen't resolve lexically --- src/uucore/src/lib/features/fs.rs | 16 ++++++++++++++++ tests/by-util/test_realpath.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 4e4155c67d4..91bc60640f3 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -380,6 +380,22 @@ pub fn canonicalize>( } OwningComponent::CurDir => {} OwningComponent::ParentDir => { + // checking for current component + if !result.as_os_str().is_empty() { + match fs::metadata(&result) { + Ok(meta) if meta.is_dir() => {} + Ok(_) => { + return Err(Error::new(ErrorKind::NotADirectory, "Not a directory")); + } + Err(e) if e.kind() == ErrorKind::NotFound => { + return Err(Error::new( + ErrorKind::NotFound, + "No such file or directory", + )); + } + Err(e) => return Err(e), + } + } result.pop(); } } diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 0a2b6b17214..1c23bd9e126 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -5,6 +5,7 @@ // spell-checker:ignore nusr +use uucore::util_name; use uutests::new_ucmd; use uutests::path_concat; use uutests::util::{TestScenario, get_root_path}; @@ -670,3 +671,28 @@ fn test_realpath_canonicalize_vs_existing() { } } } + +#[test] +fn test_realpath_followed_by_parent_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("regular_file"); + + scene + .ucmd() + .arg("regular_file/..") + .fails() + .code_is(1) + .stderr_contains("Not a directory"); +} + +#[test] +fn test_realpath_directory_followed_by_parent_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("directory"); + + scene.ucmd().arg("directory/..").succeeds(); +} From 432d1d7585fc751286effffefb14bee10175e662 Mon Sep 17 00:00:00 2001 From: martell0x1 Date: Tue, 3 Nov 2026 15:28:28 +0200 Subject: [PATCH 2/7] fix: fixed physical directory component , ignored the check for missing mode and fixed tests --- src/uucore/src/lib/features/fs.rs | 16 ++-------------- tests/by-util/test_realpath.rs | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 91bc60640f3..66593624624 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -381,20 +381,8 @@ pub fn canonicalize>( OwningComponent::CurDir => {} OwningComponent::ParentDir => { // checking for current component - if !result.as_os_str().is_empty() { - match fs::metadata(&result) { - Ok(meta) if meta.is_dir() => {} - Ok(_) => { - return Err(Error::new(ErrorKind::NotADirectory, "Not a directory")); - } - Err(e) if e.kind() == ErrorKind::NotFound => { - return Err(Error::new( - ErrorKind::NotFound, - "No such file or directory", - )); - } - Err(e) => return Err(e), - } + if !result.as_os_str().is_empty() && miss_mode != MissingHandling::Missing { + ensure_is_directory(&result)?; } result.pop(); } diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 1c23bd9e126..77cfec77c07 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -5,7 +5,6 @@ // spell-checker:ignore nusr -use uucore::util_name; use uutests::new_ucmd; use uutests::path_concat; use uutests::util::{TestScenario, get_root_path}; @@ -694,5 +693,22 @@ fn test_realpath_directory_followed_by_parent_dir() { at.mkdir("directory"); - scene.ucmd().arg("directory/..").succeeds(); + scene + .ucmd() + .arg("directory/..") + .succeeds() + .stdout_is(format!("{}\n", at.root_dir_resolved())); +} + +#[test] +fn test_realpath_directory_followed_by_parent_dir_missing_mode() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + scene + .ucmd() + .arg("-m") + .arg("missing/..") + .succeeds() + .stdout_is(format!("{}\n", at.root_dir_resolved())); } From 3f9d8a0fe1ca2eaa32a356a4fba2c3f583a655ad Mon Sep 17 00:00:00 2001 From: martell0x1 Date: Tue, 3 Nov 2026 16:26:41 +0200 Subject: [PATCH 3/7] fix(realpath): validate directory before resolving parent components --- src/uucore/src/lib/features/fs.rs | 25 ++++++++++++++++++++++++ tests/by-util/test_readlink.rs | 16 ++++++++++++++++ tests/by-util/test_realpath.rs | 32 +++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 66593624624..4fb6bbb6ede 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -317,6 +317,28 @@ fn ensure_is_directory(path: &Path) -> IOResult<()> { } } +fn validate_parent_compoenent(path: &Path) -> IOResult<()> { + let mut current = PathBuf::new(); + + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir | Component::Normal(_) => { + current.push(component.as_os_str()); + } + + Component::CurDir => {} + + Component::ParentDir => { + if !current.as_os_str().is_empty() { + ensure_is_directory(¤t)?; + } + current.pop(); + } + } + } + Ok(()) +} + /// Return the canonical, absolute form of a path. /// /// This function is a generalization of [`std::fs::canonicalize`] that @@ -362,6 +384,9 @@ pub fn canonicalize>( dunce::canonicalize(current_dir)?.join(original) }; let path = if res_mode == ResolveMode::Logical { + if miss_mode != MissingHandling::Missing { + validate_parent_compoenent(&original)? + } normalize_path(&original) } else { original diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index c374515df6e..5cdfad5356d 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -563,3 +563,19 @@ fn test_verbose_or_silent() { ) .no_stdout(); } +#[test] +fn test_readlink_canonicalize_regular_file_followed_by_parent_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("regular_file"); + + scene + .ucmd() + .arg("-v") + .arg("-f") + .arg("regular_file/..") + .fails() + .code_is(1) + .stderr_contains("Not a directory"); +} diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 77cfec77c07..37dd2e93e26 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -712,3 +712,35 @@ fn test_realpath_directory_followed_by_parent_dir_missing_mode() { .succeeds() .stdout_is(format!("{}\n", at.root_dir_resolved())); } + +#[test] +fn test_realpath_logical_followed_by_parent_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("regular_file"); + + scene + .ucmd() + .arg("-L") + .arg("regular_file/..") + .fails() + .code_is(1) + .stderr_contains("Not a directory"); +} + +#[test] +fn test_realpath_existing_followed_by_parent_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("regular_file"); + + scene + .ucmd() + .arg("-e") + .arg("regular_file/..") + .fails() + .code_is(1) + .stderr_contains("Not a directory"); +} From c1998f7bfcafbbec9ede5bcb0fa887c21aa0687e Mon Sep 17 00:00:00 2001 From: martell0x1 Date: Tue, 3 Nov 2026 17:52:03 +0200 Subject: [PATCH 4/7] fix(realpath): improve cross-platform regression tests --- src/uucore/src/lib/features/fs.rs | 7 ++++++- tests/by-util/test_realpath.rs | 13 ++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 4fb6bbb6ede..0228c731c2d 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -317,7 +317,12 @@ fn ensure_is_directory(path: &Path) -> IOResult<()> { } } -fn validate_parent_compoenent(path: &Path) -> IOResult<()> { +/// Validates path components preceding each `..` component +/// +/// Ensures that the accumulated path is a directory before traversing +/// to its parent. This prevents paths containing a regular file followed +/// by `..` from being incorrectly accepted after logical normalization.. +fn validate_parent_component(path: &Path) -> IOResult<()> { let mut current = PathBuf::new(); for component in path.components() { diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 37dd2e93e26..50cfd112582 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -16,6 +16,11 @@ use std::path::{MAIN_SEPARATOR, Path}; static GIBBERISH: &str = "supercalifragilisticexpialidocious"; +#[cfg(not(windows))] +static NOT_A_DIRECTORY: &str = "Not a directory"; +#[cfg(windows)] +static NOT_A_DIRECTORY: &str = "The directory name is invalid."; + #[test] #[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_realpath_current_directory() { @@ -683,10 +688,11 @@ fn test_realpath_followed_by_parent_dir() { .arg("regular_file/..") .fails() .code_is(1) - .stderr_contains("Not a directory"); + .stderr_contains(NOT_A_DIRECTORY); } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_realpath_directory_followed_by_parent_dir() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -701,6 +707,7 @@ fn test_realpath_directory_followed_by_parent_dir() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_realpath_directory_followed_by_parent_dir_missing_mode() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -726,7 +733,7 @@ fn test_realpath_logical_followed_by_parent_dir() { .arg("regular_file/..") .fails() .code_is(1) - .stderr_contains("Not a directory"); + .stderr_contains(NOT_A_DIRECTORY); } #[test] @@ -742,5 +749,5 @@ fn test_realpath_existing_followed_by_parent_dir() { .arg("regular_file/..") .fails() .code_is(1) - .stderr_contains("Not a directory"); + .stderr_contains(NOT_A_DIRECTORY); } From 1e809ed2365170d1d0440735641be6ae6e8e1385 Mon Sep 17 00:00:00 2001 From: martell0x1 Date: Tue, 3 Nov 2026 17:59:25 +0200 Subject: [PATCH 5/7] fix(realpath): forget to rename the function call --- src/uucore/src/lib/features/fs.rs | 2 +- tests/by-util/test_readlink.rs | 3 ++- tests/by-util/test_realpath.rs | 9 ++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 0228c731c2d..ced9a19bbbd 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -390,7 +390,7 @@ pub fn canonicalize>( }; let path = if res_mode == ResolveMode::Logical { if miss_mode != MissingHandling::Missing { - validate_parent_compoenent(&original)? + validate_parent_component(&original)? } normalize_path(&original) } else { diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index 5cdfad5356d..e3cd7905e3c 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -577,5 +577,6 @@ fn test_readlink_canonicalize_regular_file_followed_by_parent_dir() { .arg("regular_file/..") .fails() .code_is(1) - .stderr_contains("Not a directory"); + .stderr_contains(NOT_A_DIRECTORY) + .no_stdout(); } diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 50cfd112582..ec5d2dc2293 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -688,7 +688,8 @@ fn test_realpath_followed_by_parent_dir() { .arg("regular_file/..") .fails() .code_is(1) - .stderr_contains(NOT_A_DIRECTORY); + .stderr_contains(NOT_A_DIRECTORY) + .no_stdout(); } #[test] @@ -733,7 +734,8 @@ fn test_realpath_logical_followed_by_parent_dir() { .arg("regular_file/..") .fails() .code_is(1) - .stderr_contains(NOT_A_DIRECTORY); + .stderr_contains(NOT_A_DIRECTORY) + .no_stdout(); } #[test] @@ -749,5 +751,6 @@ fn test_realpath_existing_followed_by_parent_dir() { .arg("regular_file/..") .fails() .code_is(1) - .stderr_contains(NOT_A_DIRECTORY); + .stderr_contains(NOT_A_DIRECTORY) + .no_stdout(); } From 42387c3e9057b040f85d10433fc0b4511d6f52c5 Mon Sep 17 00:00:00 2001 From: martell0x1 Date: Tue, 3 Nov 2026 23:01:32 +0200 Subject: [PATCH 6/7] fix(realpath): ; --- src/uucore/src/lib/features/fs.rs | 36 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index ced9a19bbbd..2bd73c168ff 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -23,7 +23,7 @@ use std::os::unix::fs::MetadataExt; use std::os::windows::ffi::{OsStrExt, OsStringExt}; #[cfg(windows)] use std::os::windows::io::AsRawHandle; -use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf}; +use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR}; #[cfg(unix)] use std::sync::OnceLock; use std::{env, fs}; @@ -31,12 +31,12 @@ use std::{env, fs}; use windows_sys::Win32::Foundation::MAX_PATH; #[cfg(windows)] use windows_sys::Win32::Storage::FileSystem::{ - BY_HANDLE_FILE_INFORMATION, GetDiskFreeSpaceW, GetFileInformationByHandle, GetVolumePathNameW, + GetDiskFreeSpaceW, GetFileInformationByHandle, GetVolumePathNameW, BY_HANDLE_FILE_INFORMATION, }; #[cfg(windows)] -use windows_sys::Win32::System::IO::DeviceIoControl; -#[cfg(windows)] use windows_sys::Win32::System::Ioctl::FSCTL_SET_SPARSE; +#[cfg(windows)] +use windows_sys::Win32::System::IO::DeviceIoControl; /// Maximum number of symlinks followed while resolving a single path. /// @@ -390,7 +390,7 @@ pub fn canonicalize>( }; let path = if res_mode == ResolveMode::Logical { if miss_mode != MissingHandling::Missing { - validate_parent_component(&original)? + validate_parent_component(&original)?; } normalize_path(&original) } else { @@ -577,7 +577,11 @@ pub fn display_permissions_unix(mode: u32, display_file_type: bool) -> String { result.push(if mode & S_IRUSR != 0 { 'r' } else { '-' }); result.push(if mode & S_IWUSR != 0 { 'w' } else { '-' }); result.push(if mode & S_ISUID != 0 { - if mode & S_IXUSR != 0 { 's' } else { 'S' } + if mode & S_IXUSR != 0 { + 's' + } else { + 'S' + } } else if mode & S_IXUSR != 0 { 'x' } else { @@ -587,7 +591,11 @@ pub fn display_permissions_unix(mode: u32, display_file_type: bool) -> String { result.push(if mode & S_IRGRP != 0 { 'r' } else { '-' }); result.push(if mode & S_IWGRP != 0 { 'w' } else { '-' }); result.push(if mode & S_ISGID != 0 { - if mode & S_IXGRP != 0 { 's' } else { 'S' } + if mode & S_IXGRP != 0 { + 's' + } else { + 'S' + } } else if mode & S_IXGRP != 0 { 'x' } else { @@ -597,7 +605,11 @@ pub fn display_permissions_unix(mode: u32, display_file_type: bool) -> String { result.push(if mode & S_IROTH != 0 { 'r' } else { '-' }); result.push(if mode & S_IWOTH != 0 { 'w' } else { '-' }); result.push(if mode & S_ISVTX != 0 { - if mode & S_IXOTH != 0 { 't' } else { 'T' } + if mode & S_IXOTH != 0 { + 't' + } else { + 'T' + } } else if mode & S_IXOTH != 0 { 'x' } else { @@ -691,7 +703,7 @@ pub fn path_is_root_dir>(path: P, dereference: bool) -> bool { /// assert!(are_files_identical(file1.path(), file2.path()).unwrap()); /// ``` pub fn are_files_identical(path1: impl AsRef, path2: impl AsRef) -> IOResult { - use std::fs::{File, metadata}; + use std::fs::{metadata, File}; use std::io::{BufReader, ErrorKind, Read}; let path1 = path1.as_ref(); @@ -1143,7 +1155,7 @@ pub fn get_filename(file: &Path) -> Option<&str> { pub fn replace_link(target: &Path, dest: &Path, symbolic: bool) -> IOResult<()> { #[cfg(all(unix, not(target_os = "redox")))] { - use rustix::fs::{AtFlags, CWD, Mode, OFlags, openat, renameat, unlinkat}; + use rustix::fs::{openat, renameat, unlinkat, AtFlags, Mode, OFlags, CWD}; use std::ffi::OsStr; use std::io::Read; use std::os::unix::ffi::OsStrExt; @@ -1219,7 +1231,7 @@ pub fn replace_link(target: &Path, dest: &Path, symbolic: bool) -> IOResult<()> /// `symlinkat`/`linkat` relative to an open directory. #[cfg(all(unix, not(target_os = "redox")))] fn link_at(target: &Path, dir: Fd, name: &OsStr, symbolic: bool) -> IOResult<()> { - use rustix::fs::{AtFlags, CWD, linkat, symlinkat}; + use rustix::fs::{linkat, symlinkat, AtFlags, CWD}; if symbolic { symlinkat(target, dir, name).map_err(Into::into) @@ -1261,7 +1273,7 @@ mod tests { #[cfg(unix)] use std::os::unix; #[cfg(unix)] - use tempfile::{NamedTempFile, tempdir}; + use tempfile::{tempdir, NamedTempFile}; // `tempfile` is unusable on WASI (`std::env::temp_dir` aborts there), so // tests create a uniquely named directory under the current (preopened, From ebdec2c9d2b59fc74173ae0cf8361544117c1192 Mon Sep 17 00:00:00 2001 From: martell0x1 Date: Wed, 4 Nov 2026 01:12:07 +0200 Subject: [PATCH 7/7] fix(realpath): addes -s option --- src/uucore/src/lib/features/fs.rs | 35 +++++---------- tests/by-util/test_realpath.rs | 71 +++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 24 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 2bd73c168ff..6d442d3c729 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -23,7 +23,7 @@ use std::os::unix::fs::MetadataExt; use std::os::windows::ffi::{OsStrExt, OsStringExt}; #[cfg(windows)] use std::os::windows::io::AsRawHandle; -use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR}; +use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf}; #[cfg(unix)] use std::sync::OnceLock; use std::{env, fs}; @@ -31,12 +31,12 @@ use std::{env, fs}; use windows_sys::Win32::Foundation::MAX_PATH; #[cfg(windows)] use windows_sys::Win32::Storage::FileSystem::{ - GetDiskFreeSpaceW, GetFileInformationByHandle, GetVolumePathNameW, BY_HANDLE_FILE_INFORMATION, + BY_HANDLE_FILE_INFORMATION, GetDiskFreeSpaceW, GetFileInformationByHandle, GetVolumePathNameW, }; #[cfg(windows)] -use windows_sys::Win32::System::Ioctl::FSCTL_SET_SPARSE; -#[cfg(windows)] use windows_sys::Win32::System::IO::DeviceIoControl; +#[cfg(windows)] +use windows_sys::Win32::System::Ioctl::FSCTL_SET_SPARSE; /// Maximum number of symlinks followed while resolving a single path. /// @@ -410,7 +410,6 @@ pub fn canonicalize>( } OwningComponent::CurDir => {} OwningComponent::ParentDir => { - // checking for current component if !result.as_os_str().is_empty() && miss_mode != MissingHandling::Missing { ensure_is_directory(&result)?; } @@ -577,11 +576,7 @@ pub fn display_permissions_unix(mode: u32, display_file_type: bool) -> String { result.push(if mode & S_IRUSR != 0 { 'r' } else { '-' }); result.push(if mode & S_IWUSR != 0 { 'w' } else { '-' }); result.push(if mode & S_ISUID != 0 { - if mode & S_IXUSR != 0 { - 's' - } else { - 'S' - } + if mode & S_IXUSR != 0 { 's' } else { 'S' } } else if mode & S_IXUSR != 0 { 'x' } else { @@ -591,11 +586,7 @@ pub fn display_permissions_unix(mode: u32, display_file_type: bool) -> String { result.push(if mode & S_IRGRP != 0 { 'r' } else { '-' }); result.push(if mode & S_IWGRP != 0 { 'w' } else { '-' }); result.push(if mode & S_ISGID != 0 { - if mode & S_IXGRP != 0 { - 's' - } else { - 'S' - } + if mode & S_IXGRP != 0 { 's' } else { 'S' } } else if mode & S_IXGRP != 0 { 'x' } else { @@ -605,11 +596,7 @@ pub fn display_permissions_unix(mode: u32, display_file_type: bool) -> String { result.push(if mode & S_IROTH != 0 { 'r' } else { '-' }); result.push(if mode & S_IWOTH != 0 { 'w' } else { '-' }); result.push(if mode & S_ISVTX != 0 { - if mode & S_IXOTH != 0 { - 't' - } else { - 'T' - } + if mode & S_IXOTH != 0 { 't' } else { 'T' } } else if mode & S_IXOTH != 0 { 'x' } else { @@ -703,7 +690,7 @@ pub fn path_is_root_dir>(path: P, dereference: bool) -> bool { /// assert!(are_files_identical(file1.path(), file2.path()).unwrap()); /// ``` pub fn are_files_identical(path1: impl AsRef, path2: impl AsRef) -> IOResult { - use std::fs::{metadata, File}; + use std::fs::{File, metadata}; use std::io::{BufReader, ErrorKind, Read}; let path1 = path1.as_ref(); @@ -1155,7 +1142,7 @@ pub fn get_filename(file: &Path) -> Option<&str> { pub fn replace_link(target: &Path, dest: &Path, symbolic: bool) -> IOResult<()> { #[cfg(all(unix, not(target_os = "redox")))] { - use rustix::fs::{openat, renameat, unlinkat, AtFlags, Mode, OFlags, CWD}; + use rustix::fs::{AtFlags, CWD, Mode, OFlags, openat, renameat, unlinkat}; use std::ffi::OsStr; use std::io::Read; use std::os::unix::ffi::OsStrExt; @@ -1231,7 +1218,7 @@ pub fn replace_link(target: &Path, dest: &Path, symbolic: bool) -> IOResult<()> /// `symlinkat`/`linkat` relative to an open directory. #[cfg(all(unix, not(target_os = "redox")))] fn link_at(target: &Path, dir: Fd, name: &OsStr, symbolic: bool) -> IOResult<()> { - use rustix::fs::{linkat, symlinkat, AtFlags, CWD}; + use rustix::fs::{AtFlags, CWD, linkat, symlinkat}; if symbolic { symlinkat(target, dir, name).map_err(Into::into) @@ -1273,7 +1260,7 @@ mod tests { #[cfg(unix)] use std::os::unix; #[cfg(unix)] - use tempfile::{tempdir, NamedTempFile}; + use tempfile::{NamedTempFile, tempdir}; // `tempfile` is unusable on WASI (`std::env::temp_dir` aborts there), so // tests create a uniquely named directory under the current (preopened, diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index ec5d2dc2293..37c7b55b1a3 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -721,6 +721,77 @@ fn test_realpath_directory_followed_by_parent_dir_missing_mode() { .stdout_is(format!("{}\n", at.root_dir_resolved())); } +#[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] +fn test_realpath_strip_followed_by_parent_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("directory"); + at.touch("regular_file"); + + // With `-s` (no symlink resolution), GNU still validates that the + // component preceding `..` is an existing directory. + scene + .ucmd() + .arg("-s") + .arg("directory/..") + .succeeds() + .stdout_is(format!("{}\n", at.root_dir_resolved())); + + scene + .ucmd() + .arg("-s") + .arg("regular_file/..") + .fails() + .code_is(1) + .stderr_contains(NOT_A_DIRECTORY) + .no_stdout(); + + scene + .ucmd() + .arg("-s") + .arg("missing/..") + .fails() + .code_is(1) + .stderr_contains("No such file or directory") + .no_stdout(); + + // `-m` relaxes the directory/existence check, so these succeed even + // combined with `-s`. + scene + .ucmd() + .args(&["-s", "-m"]) + .arg("regular_file/..") + .succeeds() + .stdout_is(format!("{}\n", at.root_dir_resolved())); + + scene + .ucmd() + .args(&["-s", "-m"]) + .arg("missing/..") + .succeeds() + .stdout_is(format!("{}\n", at.root_dir_resolved())); +} + +#[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] +fn test_realpath_missing_mode_regular_file_followed_by_parent_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("regular_file"); + + // `-m` permits missing and non-directory components, so a regular file + // followed by `..` must succeed (matching GNU). + scene + .ucmd() + .arg("-m") + .arg("regular_file/..") + .succeeds() + .stdout_is(format!("{}\n", at.root_dir_resolved())); +} + #[test] fn test_realpath_logical_followed_by_parent_dir() { let scene = TestScenario::new(util_name!());