1use crate::state::SecureString;
4use crate::state::{PackageItem, modal::CascadeMode};
5
6#[derive(Debug, Clone)]
17pub enum ExecutorRequest {
18 Install {
20 items: Vec<PackageItem>,
22 password: Option<SecureString>,
24 dry_run: bool,
26 },
27 Remove {
29 names: Vec<String>,
31 password: Option<SecureString>,
33 cascade: CascadeMode,
35 dry_run: bool,
37 },
38 Downgrade {
40 names: Vec<String>,
42 password: Option<SecureString>,
44 dry_run: bool,
46 },
47 CustomCommand {
49 command: String,
51 password: Option<SecureString>,
53 dry_run: bool,
55 },
56 Update {
58 commands: Vec<String>,
60 password: Option<SecureString>,
62 dry_run: bool,
64 },
65 Scan {
67 package: String,
69 do_clamav: bool,
71 do_trivy: bool,
73 do_semgrep: bool,
75 do_shellcheck: bool,
77 do_virustotal: bool,
79 do_custom: bool,
81 dry_run: bool,
83 },
84}
85
86#[derive(Debug, Clone)]
97pub enum ExecutorOutput {
98 Line(String),
100 ReplaceLastLine(String),
102 Finished {
104 success: bool,
106 exit_code: Option<i32>,
108 failed_command: Option<String>,
110 },
111 Error(String),
113}
114
115pub fn build_install_command_for_executor(
135 items: &[PackageItem],
136 password: Option<&str>,
137 dry_run: bool,
138) -> Result<String, String> {
139 use super::command::{aur_install_body, aur_install_helper_flags};
140 use super::utils::{shell_single_quote, validate_package_names};
141 use crate::state::Source;
142
143 let mut official: Vec<String> = Vec::new();
144 let mut aur: Vec<String> = Vec::new();
145
146 for item in items {
147 match item.source {
148 Source::Official { .. } => official.push(item.name.clone()),
149 Source::Aur => aur.push(item.name.clone()),
150 }
151 }
152 validate_package_names(&official, "executor install command (official)")?;
153 validate_package_names(&aur, "executor install command (AUR)")?;
154 let official_quoted: Vec<String> = official
155 .iter()
156 .map(|name| shell_single_quote(name))
157 .collect();
158 let aur_quoted: Vec<String> = aur.iter().map(|name| shell_single_quote(name)).collect();
159
160 let installed_set = crate::logic::deps::get_installed_packages();
161 let provided_set = crate::logic::deps::get_provided_packages(&installed_set);
162
163 let official_has_reinstall = official.iter().any(|name| {
164 crate::logic::deps::is_package_installed_or_provided(name, &installed_set, &provided_set)
165 });
166 let pacman_flags = if official_has_reinstall {
167 "--noconfirm"
168 } else {
169 "--needed --noconfirm"
170 };
171
172 let aur_has_reinstall = aur.iter().any(|name| {
173 crate::logic::deps::is_package_installed_or_provided(name, &installed_set, &provided_set)
174 });
175 let aur_s_flags = aur_install_helper_flags(aur_has_reinstall);
176 let aur_cli_suffix = if aur_has_reinstall {
177 "--noconfirm"
178 } else {
179 "--needed --noconfirm"
180 };
181
182 let aur_names = aur_quoted.join(" ");
183
184 if dry_run {
185 if !aur.is_empty() && !official.is_empty() {
186 let tool = crate::logic::privilege::active_tool()?;
187 let off_cmd = crate::logic::privilege::build_privilege_command(
188 tool,
189 &format!("pacman -S {pacman_flags} {}", official_quoted.join(" ")),
190 );
191 let aur_cmd = format!(
192 "(paru -S --aur {aur_cli_suffix} {aur_names} || yay -S --aur {aur_cli_suffix} {aur_names})",
193 );
194 let combined = format!("{off_cmd} && {aur_cmd}");
195 let quoted = shell_single_quote(&combined);
196 Ok(format!("echo DRY RUN: {quoted}"))
197 } else if !aur.is_empty() {
198 let aur_cmd = format!(
199 "(paru -S --aur {aur_cli_suffix} {aur_names} || yay -S --aur {aur_cli_suffix} {aur_names})",
200 );
201 let quoted = shell_single_quote(&aur_cmd);
202 Ok(format!("echo DRY RUN: {quoted}"))
203 } else if !official.is_empty() {
204 let tool = crate::logic::privilege::active_tool()?;
205 let cmd = crate::logic::privilege::build_privilege_command(
206 tool,
207 &format!("pacman -S {pacman_flags} {}", official_quoted.join(" ")),
208 );
209 let quoted = shell_single_quote(&cmd);
210 Ok(format!("echo DRY RUN: {quoted}"))
211 } else {
212 Ok("echo DRY RUN: nothing to install".to_string())
213 }
214 } else if !aur.is_empty() && !official.is_empty() {
215 let tool = crate::logic::privilege::active_tool()?;
216 let install_cmd = format!("pacman -S {pacman_flags} {}", official_quoted.join(" "));
217 let official_chain = password.map_or_else(
218 || {
219 let sync = crate::logic::privilege::build_privilege_command(tool, "pacman -Sy");
220 let install = crate::logic::privilege::build_privilege_command(tool, &install_cmd);
221 format!("{sync} && {install}")
222 },
223 |pass| {
224 crate::logic::privilege::build_password_pipe(tool, pass, "pacman -Sy").map_or_else(
225 || {
226 let sync =
227 crate::logic::privilege::build_privilege_command(tool, "pacman -Sy");
228 let install =
229 crate::logic::privilege::build_privilege_command(tool, &install_cmd);
230 format!("{sync} && {install}")
231 },
232 |sync_pipe| {
233 let install_pipe =
234 crate::logic::privilege::build_password_pipe(tool, pass, &install_cmd)
235 .unwrap_or_else(|| {
236 crate::logic::privilege::build_privilege_command(
237 tool,
238 &install_cmd,
239 )
240 });
241 format!("{sync_pipe} && {install_pipe}")
242 },
243 )
244 },
245 );
246 Ok(format!(
247 "{} && {}",
248 official_chain,
249 aur_install_body(aur_s_flags, &aur_names)
250 ))
251 } else if !aur.is_empty() {
252 Ok(aur_install_body(aur_s_flags, &aur_names))
253 } else if !official.is_empty() {
254 let tool = crate::logic::privilege::active_tool()?;
255 let install_cmd = format!("pacman -S {pacman_flags} {}", official_quoted.join(" "));
256 Ok(password.map_or_else(
257 || {
258 let sync = crate::logic::privilege::build_privilege_command(tool, "pacman -Sy");
259 let install = crate::logic::privilege::build_privilege_command(tool, &install_cmd);
260 format!("{sync} && {install}")
261 },
262 |pass| {
263 crate::logic::privilege::build_password_pipe(tool, pass, "pacman -Sy").map_or_else(
264 || {
265 let sync =
266 crate::logic::privilege::build_privilege_command(tool, "pacman -Sy");
267 let install =
268 crate::logic::privilege::build_privilege_command(tool, &install_cmd);
269 format!("{sync} && {install}")
270 },
271 |sync_pipe| {
272 let install_pipe =
273 crate::logic::privilege::build_password_pipe(tool, pass, &install_cmd)
274 .unwrap_or_else(|| {
275 crate::logic::privilege::build_privilege_command(
276 tool,
277 &install_cmd,
278 )
279 });
280 format!("{sync_pipe} && {install_pipe}")
281 },
282 )
283 },
284 ))
285 } else {
286 Ok("echo nothing to install".to_string())
287 }
288}
289
290pub fn build_remove_command_for_executor(
311 names: &[String],
312 password: Option<&str>,
313 cascade: crate::state::modal::CascadeMode,
314 dry_run: bool,
315) -> Result<String, String> {
316 use super::utils::shell_single_quote;
317
318 if names.is_empty() {
319 return Ok(if dry_run {
320 "echo DRY RUN: nothing to remove".to_string()
321 } else {
322 "echo nothing to remove".to_string()
323 });
324 }
325
326 let flag = cascade.flag();
327 let names_str = names.join(" ");
328
329 let tool = crate::logic::privilege::active_tool()?;
330 let base_cmd = format!("pacman {flag} --noconfirm {names_str}");
331
332 if dry_run {
333 let cmd = crate::logic::privilege::build_privilege_command(tool, &base_cmd);
334 let quoted = shell_single_quote(&cmd);
335 Ok(format!("echo DRY RUN: {quoted}"))
336 } else {
337 Ok(password.map_or_else(
338 || crate::logic::privilege::build_privilege_command(tool, &base_cmd),
339 |pass| {
340 crate::logic::privilege::build_password_pipe(tool, pass, &base_cmd).unwrap_or_else(
341 || crate::logic::privilege::build_privilege_command(tool, &base_cmd),
342 )
343 },
344 ))
345 }
346}
347
348pub fn build_downgrade_command_for_executor(
368 names: &[String],
369 _password: Option<&str>,
370 dry_run: bool,
371) -> Result<String, String> {
372 use super::utils::shell_single_quote;
373 if names.is_empty() {
374 return Ok(if dry_run {
375 "echo DRY RUN: nothing to downgrade".to_string()
376 } else {
377 "echo nothing to downgrade".to_string()
378 });
379 }
380
381 let names_str = names.join(" ");
382
383 let tool = crate::logic::privilege::active_tool()?;
384 let bin = tool.binary_name();
385
386 if dry_run {
387 let cmd = crate::logic::privilege::build_privilege_command(
388 tool,
389 &format!("downgrade {names_str}"),
390 );
391 let quoted = shell_single_quote(&cmd);
392 Ok(format!("echo DRY RUN: {quoted}"))
393 } else {
394 Ok(format!(
395 "if (command -v downgrade >/dev/null 2>&1) || pacman -Qi downgrade >/dev/null 2>&1; then {bin} downgrade {names_str}; else echo 'downgrade tool not found. Install \"downgrade\" package.'; fi"
396 ))
397 }
398}
399
400pub fn build_update_command_for_executor(
421 commands: &[String],
422 password: Option<&str>,
423 dry_run: bool,
424) -> Result<String, String> {
425 use super::utils::shell_single_quote;
426
427 if commands.is_empty() {
428 return Ok(if dry_run {
429 "echo DRY RUN: nothing to update".to_string()
430 } else {
431 "echo nothing to update".to_string()
432 });
433 }
434
435 let processed_commands: Vec<String> = if dry_run {
436 let simulate_failure = std::env::var("PACSEA_TEST_SIMULATE_PACMAN_FAILURE").is_ok()
438 && !commands.is_empty()
439 && commands[0].contains("pacman");
440
441 if simulate_failure {
442 tracing::info!(
443 "[DRY-RUN] Simulating pacman failure for testing - first command will fail with exit code 1"
444 );
445 }
446
447 commands
448 .iter()
449 .enumerate()
450 .map(|(idx, c)| {
451 let quoted = shell_single_quote(c);
453 if simulate_failure && idx == 0 {
454 format!("echo DRY RUN: {quoted} && false")
458 } else {
459 format!("echo DRY RUN: {quoted}")
460 }
461 })
462 .collect()
463 } else {
464 let tool = crate::logic::privilege::active_tool()?;
465 let prefix = format!("{} ", tool.binary_name());
466 commands
467 .iter()
468 .map(|cmd| {
469 password.map_or_else(
470 || cmd.clone(),
471 |pass| {
472 cmd.strip_prefix(&prefix).map_or_else(
473 || cmd.clone(),
474 |base_cmd| {
475 crate::logic::privilege::build_password_pipe(tool, pass, base_cmd)
476 .unwrap_or_else(|| cmd.clone())
477 },
478 )
479 },
480 )
481 })
482 .collect()
483 };
484
485 let joined = processed_commands.join(" && ");
486
487 if let Some(pass) = password {
489 let tool = crate::logic::privilege::active_tool()?;
490 if let Some(warmup) = crate::logic::privilege::build_credential_warmup(tool, pass) {
491 return Ok(format!("{warmup} ; {joined}"));
492 }
493 }
494
495 Ok(joined)
496}
497
498#[cfg(not(target_os = "windows"))]
513#[must_use]
514#[allow(clippy::fn_params_excessive_bools, clippy::too_many_arguments)]
515pub fn build_scan_command_for_executor(
516 package: &str,
517 do_clamav: bool,
518 do_trivy: bool,
519 do_semgrep: bool,
520 do_shellcheck: bool,
521 do_virustotal: bool,
522 do_custom: bool,
523 dry_run: bool,
524) -> String {
525 use super::utils::shell_single_quote;
526 use crate::install::scan::pkg::build_scan_cmds_for_pkg_without_sleuth;
527
528 let mut cmds: Vec<String> = Vec::new();
530 cmds.push(format!(
531 "export PACSEA_SCAN_DO_CLAMAV={}",
532 if do_clamav { "1" } else { "0" }
533 ));
534 cmds.push(format!(
535 "export PACSEA_SCAN_DO_TRIVY={}",
536 if do_trivy { "1" } else { "0" }
537 ));
538 cmds.push(format!(
539 "export PACSEA_SCAN_DO_SEMGREP={}",
540 if do_semgrep { "1" } else { "0" }
541 ));
542 cmds.push(format!(
543 "export PACSEA_SCAN_DO_SHELLCHECK={}",
544 if do_shellcheck { "1" } else { "0" }
545 ));
546 cmds.push(format!(
547 "export PACSEA_SCAN_DO_VIRUSTOTAL={}",
548 if do_virustotal { "1" } else { "0" }
549 ));
550 cmds.push(format!(
551 "export PACSEA_SCAN_DO_CUSTOM={}",
552 if do_custom { "1" } else { "0" }
553 ));
554 cmds.push("export PACSEA_PATTERNS_CRIT='/dev/(tcp|udp)/|bash -i *>& *[^ ]*/dev/(tcp|udp)/[0-9]+|exec [0-9]{2,}<>/dev/(tcp|udp)/|rm -rf[[:space:]]+/|dd if=/dev/zero of=/dev/sd[a-z]|[>]{1,2}[[:space:]]*/dev/sd[a-z]|: *\\(\\) *\\{ *: *\\| *: *& *\\};:|/etc/sudoers([[:space:]>]|$)|echo .*[>]{2}.*(/etc/sudoers|/root/.ssh/authorized_keys)|/etc/ld\\.so\\.preload|LD_PRELOAD=|authorized_keys.*[>]{2}|ssh-rsa [A-Za-z0-9+/=]+.*[>]{2}.*authorized_keys|curl .*(169\\.254\\.169\\.254)'".to_string());
556 cmds.push("export PACSEA_PATTERNS_HIGH='eval|base64 -d|wget .*(sh|bash|dash|ksh|zsh)([^A-Za-z]|$)|curl .*(sh|bash|dash|ksh|zsh)([^A-Za-z]|$)|sudo[[:space:]]|chattr[[:space:]]|useradd|adduser|groupadd|systemctl|service[[:space:]]|crontab|/etc/cron\\.|[>]{2}.*(\\.bashrc|\\.bash_profile|/etc/profile|\\.zshrc)|cat[[:space:]]+/etc/shadow|cat[[:space:]]+~/.ssh/id_rsa|cat[[:space:]]+~/.bash_history|systemctl stop (auditd|rsyslog)|service (auditd|rsyslog) stop|scp .*@|curl -F|nc[[:space:]].*<|tar -czv?f|zip -r'".to_string());
557 cmds.push("export PACSEA_PATTERNS_MEDIUM='whoami|uname -a|hostname|id|groups|nmap|netstat -anp|ss -anp|ifconfig|ip addr|arp -a|grep -ri .*secret|find .*-name.*(password|\\.key)|env[[:space:]]*\\|[[:space:]]*grep -i pass|wget https?://|curl https?://'".to_string());
558 cmds.push("export PACSEA_PATTERNS_LOW='http_proxy=|https_proxy=|ALL_PROXY=|yes[[:space:]]+> */dev/null *&|ulimit -n [0-9]{5,}'".to_string());
559
560 cmds.extend(build_scan_cmds_for_pkg_without_sleuth(package));
562
563 let full_cmd = cmds.join(" && ");
564
565 if dry_run {
566 let quoted = shell_single_quote(&full_cmd);
567 format!("echo DRY RUN: {quoted}")
568 } else {
569 full_cmd
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use crate::state::Source;
577
578 fn create_test_package(name: &str, source: Source) -> PackageItem {
590 PackageItem {
591 name: name.into(),
592 version: "1.0.0".into(),
593 description: String::new(),
594 source,
595 popularity: None,
596 out_of_date: None,
597 orphaned: false,
598 }
599 }
600
601 #[test]
602 fn executor_build_install_command_variants() {
616 let tool = crate::logic::privilege::active_tool().expect("privilege tool");
617 let bin = tool.binary_name();
618
619 let official_pkg = create_test_package(
620 "ripgrep",
621 Source::Official {
622 repo: "extra".into(),
623 arch: "x86_64".into(),
624 },
625 );
626
627 let aur_pkg = create_test_package("yay-bin", Source::Aur);
628
629 let installed_set = crate::logic::deps::get_installed_packages();
630 let provided_set = crate::logic::deps::get_provided_packages(&installed_set);
631 let is_installed = crate::logic::deps::is_package_installed_or_provided(
632 "ripgrep",
633 &installed_set,
634 &provided_set,
635 );
636 let cmd1 =
637 build_install_command_for_executor(std::slice::from_ref(&official_pkg), None, false)
638 .expect("build install");
639 let quoted_name = crate::install::shell_single_quote("ripgrep");
640 if is_installed {
641 assert!(
642 cmd1.contains(&format!("{bin} pacman -S --noconfirm {quoted_name}")),
643 "expected quoted package name in: {cmd1}"
644 );
645 assert!(!cmd1.contains("--needed"));
646 } else {
647 assert!(
648 cmd1.contains(&format!(
649 "{bin} pacman -S --needed --noconfirm {quoted_name}"
650 )),
651 "expected quoted package name in: {cmd1}"
652 );
653 }
654 assert!(!cmd1.contains("Press any key to close"));
655
656 let cmd2 = build_install_command_for_executor(
658 std::slice::from_ref(&official_pkg),
659 Some("pass"),
660 false,
661 )
662 .expect("build install");
663 if tool.capabilities().supports_stdin_password {
664 assert!(cmd2.contains("printf "), "expected printf in: {cmd2}");
665 if is_installed {
666 assert!(
667 cmd2.contains(&format!("{bin} -S pacman -S --noconfirm {quoted_name}")),
668 "expected quoted package name in password command: {cmd2}"
669 );
670 } else {
671 assert!(
672 cmd2.contains(&format!(
673 "{bin} -S pacman -S --needed --noconfirm {quoted_name}"
674 )),
675 "expected quoted package name in password command: {cmd2}"
676 );
677 }
678 } else {
679 assert!(
680 cmd2.contains(&format!("{bin} pacman")),
681 "doas fallback should use plain command: {cmd2}"
682 );
683 }
684
685 let cmd3 = build_install_command_for_executor(std::slice::from_ref(&aur_pkg), None, false)
687 .expect("build install");
688 assert!(cmd3.contains("command -v paru"));
689 assert!(cmd3.contains("paru -S --aur"));
690 assert!(!cmd3.contains("Press any key to close"));
691
692 let cmd4 =
694 build_install_command_for_executor(&[official_pkg], None, true).expect("build install");
695 assert!(cmd4.starts_with("echo DRY RUN:"));
696 }
697
698 #[test]
699 fn executor_build_mixed_packages() {
710 let tool = crate::logic::privilege::active_tool().expect("privilege tool");
711 let bin = tool.binary_name();
712
713 let official_pkg = create_test_package(
714 "ripgrep",
715 Source::Official {
716 repo: "extra".into(),
717 arch: "x86_64".into(),
718 },
719 );
720 let aur_pkg = create_test_package("yay-bin", Source::Aur);
721
722 let cmd = build_install_command_for_executor(&[official_pkg, aur_pkg], None, false)
723 .expect("build install");
724 assert!(
725 cmd.contains(&format!("{bin} pacman")),
726 "expected privileged pacman in mixed install: {cmd}"
727 );
728 assert!(
729 cmd.contains("ripgrep"),
730 "expected official pkg in cmd: {cmd}"
731 );
732 assert!(
733 cmd.contains("paru -S --aur") || cmd.contains("yay -S --aur"),
734 "expected helper --aur for AUR targets: {cmd}"
735 );
736 assert!(
737 cmd.contains("yay-bin"),
738 "expected AUR pkg name in helper invocation: {cmd}"
739 );
740 }
741
742 #[test]
743 fn executor_build_empty_list() {
754 let cmd = build_install_command_for_executor(&[], None, false).expect("build install");
755 assert!(cmd.contains("nothing to install") || cmd.is_empty());
756 }
757
758 #[test]
759 fn executor_build_multiple_official() {
770 let pkg1 = create_test_package(
771 "ripgrep",
772 Source::Official {
773 repo: "extra".into(),
774 arch: "x86_64".into(),
775 },
776 );
777 let pkg2 = create_test_package(
778 "fd",
779 Source::Official {
780 repo: "extra".into(),
781 arch: "x86_64".into(),
782 },
783 );
784
785 let installed_set = crate::logic::deps::get_installed_packages();
786 let provided_set = crate::logic::deps::get_provided_packages(&installed_set);
787 let ripgrep_installed = crate::logic::deps::is_package_installed_or_provided(
788 "ripgrep",
789 &installed_set,
790 &provided_set,
791 );
792 let fd_installed = crate::logic::deps::is_package_installed_or_provided(
793 "fd",
794 &installed_set,
795 &provided_set,
796 );
797 let has_reinstall = ripgrep_installed || fd_installed;
798
799 let cmd =
800 build_install_command_for_executor(&[pkg1, pkg2], None, false).expect("build install");
801 assert!(cmd.contains("ripgrep"));
802 assert!(cmd.contains("fd"));
803 let bin = crate::logic::privilege::active_tool()
804 .expect("privilege tool")
805 .binary_name();
806 if has_reinstall {
807 assert!(
808 cmd.contains(&format!("{bin} pacman -S --noconfirm")),
809 "expected '{bin} pacman -S --noconfirm' in: {cmd}"
810 );
811 assert!(!cmd.contains("--needed"));
812 } else {
813 assert!(
814 cmd.contains(&format!("{bin} pacman -S --needed --noconfirm")),
815 "expected '{bin} pacman -S --needed --noconfirm' in: {cmd}"
816 );
817 }
818 }
819
820 #[test]
821 fn executor_build_dry_run() {
832 let pkg = create_test_package(
833 "ripgrep",
834 Source::Official {
835 repo: "extra".into(),
836 arch: "x86_64".into(),
837 },
838 );
839
840 let cmd = build_install_command_for_executor(&[pkg], None, true).expect("build install");
841 assert!(cmd.starts_with("echo DRY RUN:"));
842 }
845
846 #[test]
847 fn executor_build_password_escaping() {
858 let tool = crate::logic::privilege::active_tool().expect("privilege tool");
859 let pkg = create_test_package(
860 "ripgrep",
861 Source::Official {
862 repo: "extra".into(),
863 arch: "x86_64".into(),
864 },
865 );
866
867 let password = "pass'word\"with$special";
868 let cmd = build_install_command_for_executor(&[pkg], Some(password), false)
869 .expect("build install");
870 if tool.capabilities().supports_stdin_password {
871 assert!(cmd.contains("printf"), "expected printf in: {cmd}");
872 assert!(
873 cmd.contains(&format!("{} -S", tool.binary_name())),
874 "expected '{} -S' in: {cmd}",
875 tool.binary_name()
876 );
877 assert!(
879 cmd.contains('\'') || cmd.contains('"'),
880 "password must be shell-escaped in: {cmd}"
881 );
882 } else {
883 assert!(
885 !cmd.contains(password),
886 "password must not be embedded for non-stdin tools: {cmd}"
887 );
888 }
889 }
890
891 #[test]
892 fn executor_build_remove_command_variants() {
903 use crate::state::modal::CascadeMode;
904
905 let tool = crate::logic::privilege::active_tool().expect("privilege tool");
906 let bin = tool.binary_name();
907 let names = vec!["test-pkg1".to_string(), "test-pkg2".to_string()];
908
909 let cmd1 = build_remove_command_for_executor(&names, None, CascadeMode::Basic, false)
911 .expect("build remove");
912 assert!(
913 cmd1.contains(&format!("{bin} pacman -R --noconfirm")),
914 "expected '{bin} pacman -R --noconfirm' in: {cmd1}"
915 );
916 assert!(cmd1.contains("test-pkg1"));
917 assert!(cmd1.contains("test-pkg2"));
918 assert!(!cmd1.contains("Press any key to close"));
919
920 let cmd2 =
922 build_remove_command_for_executor(&names, Some("pass"), CascadeMode::Cascade, false)
923 .expect("build remove");
924 if tool.capabilities().supports_stdin_password {
925 assert!(cmd2.contains("printf "), "expected printf in: {cmd2}");
926 assert!(
927 cmd2.contains(&format!("{bin} -S pacman -Rs --noconfirm")),
928 "expected '{bin} -S pacman -Rs --noconfirm' in: {cmd2}"
929 );
930 } else {
931 assert!(
932 cmd2.contains(&format!("{bin} pacman -Rs --noconfirm")),
933 "expected '{bin} pacman -Rs --noconfirm' in: {cmd2}"
934 );
935 }
936
937 let cmd3 =
939 build_remove_command_for_executor(&names, None, CascadeMode::CascadeWithConfigs, false)
940 .expect("build remove");
941 assert!(
942 cmd3.contains(&format!("{bin} pacman -Rns --noconfirm")),
943 "expected '{bin} pacman -Rns --noconfirm' in: {cmd3}"
944 );
945
946 let cmd4 = build_remove_command_for_executor(&names, None, CascadeMode::Basic, true)
948 .expect("build remove");
949 assert!(cmd4.starts_with("echo DRY RUN:"));
950 assert!(cmd4.contains("pacman -R --noconfirm"));
951
952 let cmd5 = build_remove_command_for_executor(&[], None, CascadeMode::Basic, false)
954 .expect("build remove");
955 assert_eq!(cmd5, "echo nothing to remove");
956 }
957
958 #[test]
959 fn executor_build_downgrade_command_uses_unprivileged_qi_check() {
972 let names = vec!["linux".to_string(), "linux-headers".to_string()];
973 let tool = crate::logic::privilege::active_tool().expect("privilege tool");
974 let bin = tool.binary_name();
975
976 let live =
977 build_downgrade_command_for_executor(&names, None, false).expect("build downgrade");
978 assert!(
979 live.contains("pacman -Qi downgrade"),
980 "expected unprivileged pacman -Qi check in: {live}"
981 );
982 assert!(
983 live.contains(&format!("{bin} downgrade linux linux-headers")),
984 "expected tool-aware downgrade command in: {live}"
985 );
986
987 let dry =
988 build_downgrade_command_for_executor(&names, None, true).expect("build downgrade");
989 assert!(dry.starts_with("echo DRY RUN:"));
990 assert!(dry.contains(&format!("{bin} downgrade linux linux-headers")));
991 }
992}