diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index f855f5cea..6709e7e3a 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -5,9 +5,12 @@ // spell-checker:ignore (ToDO) retcode -use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap::{ + Arg, ArgAction, ArgMatches, Command, + builder::{TypedValueParser, ValueParserFactory}, +}; use std::{ - ffi::OsString, + ffi::{OsStr, OsString}, io::{Write, stdout}, path::{Path, PathBuf}, }; @@ -34,6 +37,39 @@ const OPT_RELATIVE_BASE: &str = "relative-base"; const ARG_FILES: &str = "files"; +/// Custom parser that validates `OsString` is not empty +#[derive(Clone, Debug)] +struct NonEmptyOsStringParser; + +impl TypedValueParser for NonEmptyOsStringParser { + type Value = OsString; + + fn parse_ref( + &self, + _cmd: &Command, + _arg: Option<&Arg>, + value: &OsStr, + ) -> Result { + if value.is_empty() { + let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation); + err.insert( + clap::error::ContextKind::Custom, + clap::error::ContextValue::String("invalid operand: empty string".to_string()), + ); + return Err(err); + } + Ok(value.to_os_string()) + } +} + +impl ValueParserFactory for NonEmptyOsStringParser { + type Parser = Self; + + fn value_parser() -> Self::Parser { + Self + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args).with_exit_code(1)?; @@ -146,21 +182,21 @@ pub fn uu_app() -> Command { Arg::new(OPT_RELATIVE_TO) .long(OPT_RELATIVE_TO) .value_name("DIR") - .value_parser(clap::value_parser!(OsString)) + .value_parser(NonEmptyOsStringParser) .help(translate!("realpath-help-relative-to")), ) .arg( Arg::new(OPT_RELATIVE_BASE) .long(OPT_RELATIVE_BASE) .value_name("DIR") - .value_parser(clap::value_parser!(OsString)) + .value_parser(NonEmptyOsStringParser) .help(translate!("realpath-help-relative-base")), ) .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) .required(true) - .value_parser(clap::value_parser!(OsString)) + .value_parser(NonEmptyOsStringParser) .value_hint(clap::ValueHint::AnyPath), ) } diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index a081b163f..24ca48870 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -489,3 +489,24 @@ fn test_realpath_non_utf8_paths() { assert!(output.contains("test_")); assert!(output.contains(".txt")); } + +#[test] +fn test_realpath_empty_string() { + // Test that empty string arguments are rejected with exit code 1 + new_ucmd!().arg("").fails().code_is(1); + + // Test that empty --relative-base is rejected + new_ucmd!() + .arg("--relative-base=") + .arg("--relative-to=.") + .arg(".") + .fails() + .code_is(1); + + // Test that empty --relative-to is rejected + new_ucmd!() + .arg("--relative-to=") + .arg(".") + .fails() + .code_is(1); +}