diff --git a/src/find/matchers/exec.rs b/src/find/matchers/exec.rs old mode 100644 new mode 100755 index 51337953..6fec4b98 --- a/src/find/matchers/exec.rs +++ b/src/find/matchers/exec.rs @@ -5,6 +5,7 @@ // https://opensource.org/licenses/MIT. use std::cell::RefCell; +use std::env; use std::error::Error; use std::ffi::OsString; use std::io::{stderr, Write}; @@ -13,11 +14,30 @@ use std::process::Command; use super::{Matcher, MatcherIO, WalkEntry}; +fn check_path_entries_absolute(path: Option) -> Result<(), Box> { + if let Some(path_dirs) = path { + for dir_entry in env::split_paths(&path_dirs) { + if !dir_entry.is_absolute() || dir_entry.as_os_str().is_empty() { + return Err(format!( + "The PATH environment variable contains non-absolute paths or empty paths. \ + Segment that caused the error: '{}'", + dir_entry.as_os_str().to_string_lossy() + ) + .into()); + } + } + } + + Ok(()) +} + +#[derive(Debug)] enum Arg { FileArg(Vec), LiteralArg(OsString), } +#[derive(Debug)] pub struct SingleExecMatcher { executable: String, args: Vec, @@ -30,6 +50,19 @@ impl SingleExecMatcher { args: &[&str], exec_in_parent_dir: bool, ) -> Result> { + Self::new_with_path(executable, args, exec_in_parent_dir, env::var_os("PATH")) + } + + fn new_with_path( + executable: &str, + args: &[&str], + exec_in_parent_dir: bool, + path: Option, + ) -> Result> { + if exec_in_parent_dir { + check_path_entries_absolute(path)?; + } + let transformed_args = args .iter() .map(|&a| { @@ -98,6 +131,7 @@ impl Matcher for SingleExecMatcher { } } +#[derive(Debug)] pub struct MultiExecMatcher { executable: String, args: Vec, @@ -112,6 +146,19 @@ impl MultiExecMatcher { args: &[&str], exec_in_parent_dir: bool, ) -> Result> { + Self::new_with_path(executable, args, exec_in_parent_dir, env::var_os("PATH")) + } + + fn new_with_path( + executable: &str, + args: &[&str], + exec_in_parent_dir: bool, + path: Option, + ) -> Result> { + if exec_in_parent_dir { + check_path_entries_absolute(path)?; + } + let transformed_args = args.iter().map(OsString::from).collect(); Ok(Self { @@ -217,7 +264,406 @@ impl Matcher for MultiExecMatcher { } } +/// Only tests related to path checking here, because we need to call out to an external executable. +/// See `tests/exec_unit_tests.rs` instead. #[cfg(test)] -/// No tests here, because we need to call out to an external executable. See -/// `tests/exec_unit_tests.rs` instead. -mod tests {} +mod check_path_tests { + use super::*; + use std::path::MAIN_SEPARATOR; + + #[cfg(unix)] + use std::os::unix::ffi::OsStringExt; + #[cfg(unix)] + const PATH_SEPARATOR: char = ':'; + #[cfg(windows)] + const PATH_SEPARATOR: char = ';'; + #[cfg(windows)] + use std::os::windows::ffi::OsStringExt; + + // Helper to create platform-specific absolute paths + fn abs_path(component: &str) -> String { + format!("{}{}", MAIN_SEPARATOR, component) + } + + // Helper to create platform-specific relative paths + fn rel_path(component: &str) -> String { + format!(".{}{}", MAIN_SEPARATOR, component) + } + + mod single_exec_matcher_tests { + use super::*; + + #[test] + fn single_exec_matcher_valid_path() { + #[cfg(unix)] + let path = format!("{}{}{}", abs_path("usr"), PATH_SEPARATOR, abs_path("bin")); + #[cfg(windows)] + let path = format!( + "C:{}{}C:{}", + abs_path("usr"), + PATH_SEPARATOR, + abs_path("bin") + ); + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + assert!(result.is_ok()); + } + + #[test] + fn single_exec_matcher_multiple_valid_paths() { + #[cfg(unix)] + let path = format!( + "{}{}{}{}{}", + abs_path("a"), + PATH_SEPARATOR, + abs_path("b"), + PATH_SEPARATOR, + abs_path("c") + ); + #[cfg(windows)] + let path = format!( + "C:{}{}C:{}{}C:{}", + abs_path("a"), + PATH_SEPARATOR, + abs_path("b"), + PATH_SEPARATOR, + abs_path("c") + ); + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + assert!(result.is_ok()); + } + + #[test] + fn single_exec_matcher_relative_path() { + let path = format!( + "{}{}{}", + abs_path("usr"), + PATH_SEPARATOR, + rel_path("relative") + ); + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + assert!(result.is_err()); + } + + #[test] + fn single_exec_matcher_empty_path() { + let path = format!("{}{}{}", abs_path("usr"), PATH_SEPARATOR, ""); + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + assert!(result.is_err()); + } + + #[test] + fn single_exec_matcher_empty_string_path() { + let result = + SingleExecMatcher::new_with_path("echo", &["test"], true, Some(OsString::from(""))); + assert!(result.is_err()); + } + + #[test] + fn single_exec_matcher_valid_then_invalid_path() { + let path = format!( + "{}{}{}{}{}", + abs_path("valid1"), + PATH_SEPARATOR, + rel_path("invalid"), + PATH_SEPARATOR, + abs_path("valid2") + ); + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + assert!(result.is_err()); + } + + #[test] + fn single_exec_matcher_undefined_path() { + let result = SingleExecMatcher::new_with_path("echo", &["test"], true, None); + assert!(result.is_ok()); + } + + #[test] + fn single_exec_matcher_no_validation_when_not_needed() { + let path = format!("{}{}{}", "relative", PATH_SEPARATOR, ""); + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + false, + Some(OsString::from(path)), + ); + assert!(result.is_ok()); + } + + #[test] + fn single_exec_matcher_relative_path_error_message() { + let relative_component = rel_path("relative"); + #[cfg(unix)] + let path = format!( + "{}{}{}", + abs_path("usr"), + PATH_SEPARATOR, + relative_component + ); + #[cfg(windows)] + let path = format!( + "C:{}{}C:{}", + abs_path("usr"), + PATH_SEPARATOR, + relative_component + ); + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + let err = result.unwrap_err(); + let err_msg = err.to_string(); + assert!( + err_msg.contains(&relative_component), + "Error message should contain relative path component" + ); + } + + #[test] + fn single_exec_matcher_empty_path_error_message() { + #[cfg(unix)] + let path = format!("{}{}{}", abs_path("usr"), PATH_SEPARATOR, ""); + #[cfg(windows)] + let path = format!("C:{}{}{}", abs_path("usr"), PATH_SEPARATOR, ""); + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + let err = result.unwrap_err(); + let err_msg = err.to_string(); + assert!( + err_msg.contains("''"), + "Error message should contain empty path indicator" + ); + } + + // Platform-specific non-UTF8 tests + #[cfg(unix)] + #[test] + #[ignore] + fn single_exec_matcher_does_not_reject_non_utf8_unix() { + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from_vec(vec![ + b'\x2F', b'\x00', b'\x75', b'\x00', b'\x73', b'\x00', b'\x72', b'\x00', + b'\x2F', b'\x00', b'\x62', b'\x00', b'\x69', b'\x00', b'\x6E', b'\x00', + b'\x3A', b'\x00', b'\x2F', b'\x00', b'\x62', b'\x00', b'\x69', b'\x00', + b'\x6E', b'\x00', + ])), + ); + assert!(result.is_ok()); + } + + #[cfg(windows)] + #[test] + #[ignore] + fn single_exec_matcher_does_not_reject_non_utf8_windows() { + let result = SingleExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from_wide(&[ + 0x0043, 0x003A, 0x005C, 0x0076, 0x0061, 0x006C, 0x0069, 0x0064, 0x003B, 0xD800, + 0xDC00, + ])), + ); + assert!(result.is_ok()); + } + } + + mod multi_exec_matcher_test { + use super::*; + + // Tests mirroring the single_exec tests with MultiExecMatcher + // (Same test structure as above but using MultiExecMatcher) + #[test] + fn multi_exec_matcher_valid_path() { + #[cfg(unix)] + let path = format!("{}{}{}", abs_path("usr"), PATH_SEPARATOR, abs_path("bin")); + #[cfg(windows)] + let path = format!( + "C:{}{}C:{}", + abs_path("usr"), + PATH_SEPARATOR, + abs_path("bin") + ); + + let result = MultiExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + assert!(result.is_ok()); + } + + #[test] + fn multi_exec_matcher_multiple_valid_paths() { + #[cfg(unix)] + let path = format!( + "{}{}{}{}{}", + abs_path("a"), + PATH_SEPARATOR, + abs_path("b"), + PATH_SEPARATOR, + abs_path("c") + ); + #[cfg(windows)] + let path = format!( + "C:{}{}C:{}{}C:{}", + abs_path("a"), + PATH_SEPARATOR, + abs_path("b"), + PATH_SEPARATOR, + abs_path("c") + ); + let result = MultiExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path.clone())), + ); + + assert!(result.is_ok()); + } + + #[test] + fn multi_exec_matcher_relative_path() { + let path = format!( + "{}{}{}", + abs_path("usr"), + PATH_SEPARATOR, + rel_path("relative") + ); + let result = MultiExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + assert!(result.is_err()); + } + + #[test] + fn multi_exec_matcher_empty_path() { + let path = format!("{}{}{}", abs_path("usr"), PATH_SEPARATOR, ""); + let result = MultiExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + assert!(result.is_err()); + } + + #[test] + fn multi_exec_matcher_empty_string_path() { + let result = + MultiExecMatcher::new_with_path("echo", &["test"], true, Some(OsString::from(""))); + assert!(result.is_err()); + } + + #[test] + fn multi_exec_matcher_valid_then_invalid_path() { + let path = format!( + "{}{}{}{}{}", + abs_path("valid1"), + PATH_SEPARATOR, + rel_path("invalid"), + PATH_SEPARATOR, + abs_path("valid2") + ); + let result = MultiExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from(path)), + ); + assert!(result.is_err()); + } + + #[test] + fn multi_exec_matcher_undefined_path() { + let result = MultiExecMatcher::new_with_path("echo", &["test"], true, None); + assert!(result.is_ok()); + } + + #[test] + fn multi_exec_matcher_no_validation_when_not_needed() { + let path = format!("{}{}{}", "relative", PATH_SEPARATOR, ""); + let result = MultiExecMatcher::new_with_path( + "echo", + &["test"], + false, + Some(OsString::from(path)), + ); + assert!(result.is_ok()); + } + + // Platform-specific non-UTF8 tests + #[cfg(unix)] + #[test] + #[ignore] + fn multi_exec_matcher_does_not_reject_non_utf8_unix() { + let result = MultiExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from_vec(vec![ + b'\x2F', b'\x00', b'\x75', b'\x00', b'\x73', b'\x00', b'\x72', b'\x00', + b'\x2F', b'\x00', b'\x62', b'\x00', b'\x69', b'\x00', b'\x6E', b'\x00', + b'\x3A', b'\x00', b'\x2F', b'\x00', b'\x62', b'\x00', b'\x69', b'\x00', + b'\x6E', b'\x00', + ])), + ); + assert!(result.is_ok()); + } + + #[cfg(windows)] + #[test] + #[ignore] + fn multi_exec_matcher_does_not_reject_non_utf8_windows() { + let result = MultiExecMatcher::new_with_path( + "echo", + &["test"], + true, + Some(OsString::from_wide(&[ + 0x0043, 0x003A, 0x005C, 0x0076, 0x0061, 0x006C, 0x0069, 0x0064, 0x003B, 0xD800, + 0xDC00, + ])), + ); + assert!(result.is_ok()); + } + } +}