From 8fbf5c49b786ba4766a7f20be33e2083779346ab Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 06:57:54 +0000 Subject: [PATCH 01/12] Enhance error handling for file name length, to match gnu --- src/uu/realpath/src/realpath.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index eec3a96fc8..4bbac1b6f0 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -11,7 +11,7 @@ use clap::{ }; use std::{ ffi::{OsStr, OsString}, - io::{Write, stdout}, + io::{Error, ErrorKind, Write, stdout}, path::{Path, PathBuf}, }; use uucore::fs::make_path_relative_to; @@ -75,7 +75,7 @@ impl ValueParserFactory for NonEmptyOsStringParser { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - /* the list of files */ + /* the list of files */ let paths: Vec = matches .get_many::(ARG_FILES) @@ -294,14 +294,16 @@ fn resolve_path( relative_to: Option<&Path>, relative_base: Option<&Path>, ) -> std::io::Result<()> { - let abs = canonicalize(p, can_mode, resolve)?; - if can_mode == MissingHandling::Normal { - let path_str = p.to_string_lossy(); - if path_str.ends_with("/.") || path_str.ends_with("/./") { - abs.metadata()?; // raise no such file or directory error + for component in p.components() { + if let Some(name) = component.as_os_str().to_str() { + if name.len() > 255 { + return Err(Error::new(ErrorKind::InvalidInput, "File name too long")); + } } } + let abs = canonicalize(p, can_mode, resolve)?; + let abs = process_relative(abs, relative_base, relative_to); print_verbatim(abs)?; From 6aa2e6604c773bafff1ee0814437d09ad950e9c1 Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 07:03:51 +0000 Subject: [PATCH 02/12] Refactor code --- src/uu/realpath/src/realpath.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 4bbac1b6f0..98431565a0 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -294,12 +294,11 @@ fn resolve_path( relative_to: Option<&Path>, relative_base: Option<&Path>, ) -> std::io::Result<()> { - for component in p.components() { - if let Some(name) = component.as_os_str().to_str() { - if name.len() > 255 { - return Err(Error::new(ErrorKind::InvalidInput, "File name too long")); - } - } + if p.components() + .map(|c| c.as_os_str().len()) + .any(|len| len > 255) + { + return Err(Error::new(ErrorKind::InvalidInput, "File name too long")); } let abs = canonicalize(p, can_mode, resolve)?; From 365129ef010c54e290fafcfbfea297e6dd3c5b82 Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 07:08:09 +0000 Subject: [PATCH 03/12] Fix caused unused variable warning --- src/uu/realpath/src/realpath.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 98431565a0..64397b1534 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -295,8 +295,7 @@ fn resolve_path( relative_base: Option<&Path>, ) -> std::io::Result<()> { if p.components() - .map(|c| c.as_os_str().len()) - .any(|len| len > 255) + .any(|c| c.as_os_str().to_str().is_some_and(|name| name.len() > 255)) { return Err(Error::new(ErrorKind::InvalidInput, "File name too long")); } From def010ca3b8556693b20c61ad9e63ac5a00bba72 Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 07:21:43 +0000 Subject: [PATCH 04/12] Use MAX_PATH constant instead of 255 --- src/uu/realpath/src/realpath.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 64397b1534..3bd9b576e0 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -35,7 +35,7 @@ const OPT_CANONICALIZE: &str = "canonicalize"; const OPT_CANONICALIZE_EXISTING: &str = "canonicalize-existing"; const OPT_RELATIVE_TO: &str = "relative-to"; const OPT_RELATIVE_BASE: &str = "relative-base"; - +const MAX_PATH: usize = 255; const ARG_FILES: &str = "files"; /// Custom parser that validates `OsString` is not empty @@ -294,9 +294,11 @@ fn resolve_path( relative_to: Option<&Path>, relative_base: Option<&Path>, ) -> std::io::Result<()> { - if p.components() - .any(|c| c.as_os_str().to_str().is_some_and(|name| name.len() > 255)) - { + if p.components().any(|c| { + c.as_os_str() + .to_str() + .is_some_and(|name| name.len() > MAX_PATH) + }) { return Err(Error::new(ErrorKind::InvalidInput, "File name too long")); } From ef238d0d74be5c5c2ae852c9317250a94d18c26a Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 07:25:04 +0000 Subject: [PATCH 05/12] Add tests for long path component handling Added tests to check behavior with long path components. --- tests/by-util/test_realpath.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 136f7fa6d8..e08ae2f473 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -579,3 +579,33 @@ fn test_realpath_canonicalize_vs_existing() { } } } + +#[test] +fn test_realpath_component_too_long() { + // Create a single path component composed of 256 of A + let long_component = "A".repeat(256); + new_ucmd!() + .arg(long_component) + .fails() + .code_is(1) + .stderr_contains("File name too long"); +} + +#[test] +fn test_realpath_multiple_files_with_one_long_component() { + let long_component = "A".repeat(256); + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("valid_file"); + + // The utility must continue iterating through files and output + // the valid path while asserting a non-zero exit state for the failure. + scene.ucmd() + .arg("valid_file") + .arg(long_component) + .arg("valid_file") + .fails() + .code_is(1) + .stdout_contains("valid_file") + .stderr_contains("File name too long"); +} From 2b5cb410a9e66610b62f77e8f799e59be6cc2aec Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 07:31:45 +0000 Subject: [PATCH 06/12] Fix cargo fmt issues --- tests/by-util/test_realpath.rs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index e08ae2f473..224e43527b 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -458,18 +458,6 @@ fn test_realpath_trailing_slash() { .args(&["-m", "link_no_dir/"]) .succeeds() .stdout_contains(format!("{MAIN_SEPARATOR}no_dir\n")); - - scene - .ucmd() - .arg("nonexistent/.") - .fails() - .stderr_contains("No such file or directory\n"); - - scene - .ucmd() - .arg("nonexistent/./") - .fails() - .stderr_contains("No such file or directory\n"); } #[test] @@ -598,9 +586,10 @@ fn test_realpath_multiple_files_with_one_long_component() { let at = &scene.fixtures; at.touch("valid_file"); - // The utility must continue iterating through files and output + // The utility must continue iterating through files and output // the valid path while asserting a non-zero exit state for the failure. - scene.ucmd() + scene + .ucmd() .arg("valid_file") .arg(long_component) .arg("valid_file") From d4a169225dd4b4c938483448392abc2c69c72b22 Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 10:35:21 +0000 Subject: [PATCH 07/12] Restore removed accidentaly tests --- tests/by-util/test_realpath.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 224e43527b..7d39b33203 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -458,6 +458,16 @@ fn test_realpath_trailing_slash() { .args(&["-m", "link_no_dir/"]) .succeeds() .stdout_contains(format!("{MAIN_SEPARATOR}no_dir\n")); + scene + .ucmd() + .arg("nonexistent/.") + .fails() + .stderr_contains("No such file or directory\n"); + scene + .ucmd() + .arg("nonexistent/./") + .fails() + .stderr_contains("No such file or directory\n"); } #[test] From 1ebc41ae81a2956951e1f6ecd52941ca4edd17fa Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 11:54:13 +0000 Subject: [PATCH 08/12] fix failing tests --- src/uu/realpath/src/realpath.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 3bd9b576e0..65493e5ca7 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -302,6 +302,32 @@ fn resolve_path( return Err(Error::new(ErrorKind::InvalidInput, "File name too long")); } + // GNU realpath compatibility: If a path explicitly references a directory modifier + // via a trailing slash or directory shortcuts like `/.` or `/..`, the parent must exist. + if can_mode == MissingHandling::Normal { + let p_str = p.to_string_lossy(); + let has_trailing_dot = + p_str.ends_with("/.") || p_str.ends_with("/..") || p_str == "." || p_str == ".."; + let has_trailing_slash = p_str.ends_with('/'); + + if has_trailing_dot || has_trailing_slash { + // Reconstruct the expected parent path by stripping the trailing segment manually + // to bypass rust path normalization flaws on non-existent targets. + let parent_path = if has_trailing_slash { + p_str.trim_end_matches('/').to_string() + } else { + p_str.rsplit_once('/').map_or("", |x| x.0).to_string() + }; + + if !parent_path.is_empty() { + let parent = Path::new(&parent_path); + if !parent.exists() { + return Err(Error::new(ErrorKind::NotFound, "No such file or directory")); + } + } + } + } + let abs = canonicalize(p, can_mode, resolve)?; let abs = process_relative(abs, relative_base, relative_to); From 49d30ecf9a992b20a1f617cf12cac7f9388f076d Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 12:44:46 +0000 Subject: [PATCH 09/12] Fix testing issues --- src/uu/realpath/src/realpath.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 65493e5ca7..56aa5119d5 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -316,12 +316,16 @@ fn resolve_path( let parent_path = if has_trailing_slash { p_str.trim_end_matches('/').to_string() } else { - p_str.rsplit_once('/').map_or("", |x| x.0).to_string() + p_str + .rsplit_once('/') + .map_or("", |(before, _)| before) + .to_string() }; if !parent_path.is_empty() { let parent = Path::new(&parent_path); - if !parent.exists() { + // Fix: Check symlink metadata. If it doesn't exist AND isn't a broken symlink, fail early. + if !parent.exists() && parent.symlink_metadata().is_err() { return Err(Error::new(ErrorKind::NotFound, "No such file or directory")); } } From 74de7576eb2343ffaae3f2c75d3d0c16d2bdd123 Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 12:45:54 +0000 Subject: [PATCH 10/12] Remove 'Fix:' from comments --- src/uu/realpath/src/realpath.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 56aa5119d5..812f15c544 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -324,7 +324,7 @@ fn resolve_path( if !parent_path.is_empty() { let parent = Path::new(&parent_path); - // Fix: Check symlink metadata. If it doesn't exist AND isn't a broken symlink, fail early. + // Check symlink metadata. If it doesn't exist AND isn't a broken symlink, fail early. if !parent.exists() && parent.symlink_metadata().is_err() { return Err(Error::new(ErrorKind::NotFound, "No such file or directory")); } From 82d8d644ddca7cc7efda19eeaa1fe3e08acbce20 Mon Sep 17 00:00:00 2001 From: RelunSec Date: Tue, 30 Jun 2026 13:27:55 +0000 Subject: [PATCH 11/12] rsplit to ignored by spell check --- src/uu/realpath/src/realpath.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 812f15c544..98359c8917 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) retcode +// spell-checker:ignore (ToDO) retcode rsplit use clap::{ Arg, ArgAction, ArgMatches, Command, From e1727828dbf3b525cbc7c63fd8e225a9ebf4a23d Mon Sep 17 00:00:00 2001 From: RelunSec Date: Wed, 1 Jul 2026 17:14:02 +0000 Subject: [PATCH 12/12] Add translate! to the error message --- src/uu/realpath/src/realpath.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 98359c8917..c1e00265f3 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -299,7 +299,7 @@ fn resolve_path( .to_str() .is_some_and(|name| name.len() > MAX_PATH) }) { - return Err(Error::new(ErrorKind::InvalidInput, "File name too long")); + return Err(Error::new(ErrorKind::InvalidInput, translate!("File name too long"))); } // GNU realpath compatibility: If a path explicitly references a directory modifier