From 9526f9d87038f187a29d10013c1162de8c6fc7ef Mon Sep 17 00:00:00 2001 From: SFG545 Date: Thu, 11 Jun 2026 13:13:00 -0500 Subject: [PATCH] Add target command resolution and fallback directory support with tests --- src/main.rs | 117 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index f3f4136..9fe46c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,14 @@ const DEFAULT_JAVA_CACERTS_LINK: &str = "/etc/ssl/certs/java/cacerts"; const DEFAULT_PKI_TLS_CA_BUNDLE_CRT_LINK: &str = "/etc/pki/tls/certs/ca-bundle.crt"; const DEFAULT_PKI_TLS_CA_BUNDLE_TRUST_CRT_LINK: &str = "/etc/pki/tls/certs/ca-bundle.trust.crt"; const DEFAULT_PKI_TLS_JAVA_CACERTS_LINK: &str = "/etc/pki/tls/java/cacerts"; +const TARGET_COMMAND_FALLBACK_DIRS: &[&str] = &[ + "/usr/local/sbin", + "/usr/local/bin", + "/usr/sbin", + "/usr/bin", + "/sbin", + "/bin", +]; const DEFAULT_CERTDATA_URL: &str = "https://hg.mozilla.org/projects/nss/raw-file/tip/lib/ckfw/builtins/certdata.txt"; const DEFAULT_CERTDATA_OUTPUT: &str = "certdata.txt"; @@ -2073,19 +2081,22 @@ fn run_target_command( dry_run: bool, envs: &[(&str, &str)], ) -> Result<()> { + let resolved_program = resolve_target_program(root, program); + let resolved_program_display = resolved_program.display().to_string(); + if dry_run { - print_command_preview(root, program, args, envs); + print_command_preview(root, &resolved_program_display, args, envs); return Ok(()); } // Execute directly on the host or via chroot for image builds. let mut cmd = if is_root_fs(root) { - let mut cmd = Command::new(program); + let mut cmd = Command::new(&resolved_program); cmd.args(args); cmd } else { let mut cmd = Command::new("chroot"); - cmd.arg(root).arg(program).args(args); + cmd.arg(root).arg(&resolved_program).args(args); cmd }; @@ -2096,9 +2107,13 @@ fn run_target_command( let output = cmd.output().with_context(|| { if is_root_fs(root) { - format!("failed to execute `{}`", program) + format!("failed to execute `{}`", resolved_program_display) } else { - format!("failed to execute `chroot {} {}`", root.display(), program) + format!( + "failed to execute `chroot {} {}`", + root.display(), + resolved_program_display + ) } })?; @@ -2138,6 +2153,48 @@ fn run_target_command( ); } +fn resolve_target_program(root: &Path, program: &str) -> PathBuf { + let path_env = std::env::var_os("PATH"); + resolve_target_program_with_path(root, program, path_env.as_deref()) +} + +fn resolve_target_program_with_path( + root: &Path, + program: &str, + path_env: Option<&OsStr>, +) -> PathBuf { + if program.contains('/') { + return PathBuf::from(program); + } + + for candidate in target_command_search_candidates(program, path_env) { + if path_in_root(root, &candidate).exists() { + return candidate; + } + } + + PathBuf::from(program) +} + +fn target_command_search_candidates(program: &str, path_env: Option<&OsStr>) -> Vec { + let mut candidates = Vec::new(); + + if let Some(path_env) = path_env { + for dir in std::env::split_paths(path_env).filter(|dir| dir.is_absolute()) { + candidates.push(dir.join(program)); + } + } + + for dir in TARGET_COMMAND_FALLBACK_DIRS { + let candidate = Path::new(dir).join(program); + if !candidates.iter().any(|existing| existing == &candidate) { + candidates.push(candidate); + } + } + + candidates +} + fn print_command_preview(root: &Path, program: &str, args: &[String], envs: &[(&str, &str)]) { // Keep dry-run output close to the real shell command for easier debugging. let env_prefix = envs @@ -2278,6 +2335,56 @@ CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR assert!(!is_root_fs(Path::new("/mnt/rootfs"))); } + #[test] + fn target_command_candidates_use_absolute_path_entries_and_fallbacks() { + let candidates = target_command_search_candidates( + "trust", + Some(std::ffi::OsStr::new("/custom/bin:relative:/usr/bin")), + ); + + assert_eq!(candidates[0], PathBuf::from("/custom/bin/trust")); + assert_eq!(candidates[1], PathBuf::from("/usr/bin/trust")); + assert!(candidates.contains(&PathBuf::from("/bin/trust"))); + assert!(!candidates.contains(&PathBuf::from("relative/trust"))); + } + + #[test] + fn resolve_target_program_finds_binary_inside_selected_root() { + let root = unique_test_dir("target-command-root"); + let trust_host_path = root.join("custom/bin/trust"); + fs::create_dir_all(trust_host_path.parent().unwrap()).unwrap(); + fs::write(&trust_host_path, b"").unwrap(); + + let resolved = resolve_target_program_with_path( + &root, + "trust", + Some(std::ffi::OsStr::new("/custom/bin")), + ); + + fs::remove_dir_all(&root).unwrap(); + assert_eq!(resolved, PathBuf::from("/custom/bin/trust")); + } + + #[test] + fn resolve_target_program_leaves_missing_program_unqualified() { + let root = unique_test_dir("target-command-missing"); + fs::create_dir_all(&root).unwrap(); + + let resolved = + resolve_target_program_with_path(&root, "missing-tool", Some(std::ffi::OsStr::new(""))); + + fs::remove_dir_all(&root).unwrap(); + assert_eq!(resolved, PathBuf::from("missing-tool")); + } + + fn unique_test_dir(name: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + std::env::temp_dir().join(format!("ca-certs-{name}-{}-{nonce}", std::process::id())) + } + #[test] fn certdata_parser_extracts_revision_and_objects() { let doc = parse_certdata(SAMPLE_CERTDATA).unwrap();