Skip to content
48 changes: 40 additions & 8 deletions src/uu/realpath/src/realpath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
// 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,
builder::{TypedValueParser, ValueParserFactory},
};
use std::{
ffi::{OsStr, OsString},
io::{Write, stdout},
io::{Error, ErrorKind, Write, stdout},
path::{Path, PathBuf},
};
use uucore::fs::make_path_relative_to;
Expand All @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems low, no ?

@HackingRepo HackingRepo Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no

const ARG_FILES: &str = "files";

/// Custom parser that validates `OsString` is not empty
Expand Down Expand Up @@ -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<PathBuf> = matches
.get_many::<OsString>(ARG_FILES)
Expand Down Expand Up @@ -294,14 +294,46 @@ fn resolve_path(
relative_to: Option<&Path>,
relative_base: Option<&Path>,
) -> std::io::Result<()> {
let abs = canonicalize(p, can_mode, resolve)?;
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, translate!("File name too long")));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it isn't the way translate works
please look at other places to see how it works

}

// 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 path_str = p.to_string_lossy();
if path_str.ends_with("/.") || path_str.ends_with("/./") {
abs.metadata()?; // raise no such file or directory error
let p_str = p.to_string_lossy();
let has_trailing_dot =
p_str.ends_with("/.") || p_str.ends_with("/..") || p_str == "." || p_str == "..";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have a function already doing that in the code base ?

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("", |(before, _)| before)
.to_string()
};

if !parent_path.is_empty() {
let parent = Path::new(&parent_path);
// 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"));
}
}
}
}

let abs = canonicalize(p, can_mode, resolve)?;

let abs = process_relative(abs, relative_base, relative_to);

print_verbatim(abs)?;
Expand Down
33 changes: 31 additions & 2 deletions tests/by-util/test_realpath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,13 +458,11 @@ fn test_realpath_trailing_slash() {
.args(&["-m", "link_no_dir/"])
.succeeds()
.stdout_contains(format!("{MAIN_SEPARATOR}no_dir\n"));

scene
Comment thread
HackingRepo marked this conversation as resolved.
.ucmd()
.arg("nonexistent/.")
.fails()
.stderr_contains("No such file or directory\n");

scene
.ucmd()
.arg("nonexistent/./")
Expand Down Expand Up @@ -579,3 +577,34 @@ 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");
}
Loading