1#[must_use]
17pub fn repo_toggle_for(repo: &str, app: &crate::state::AppState) -> bool {
18 let r = repo.to_lowercase();
19 if r == "core" {
20 app.results_filter_show_core
21 } else if r == "extra" {
22 app.results_filter_show_extra
23 } else if r == "multilib" {
24 app.results_filter_show_multilib
25 } else if crate::index::is_eos_repo(&r) {
26 app.results_filter_show_eos
27 } else if crate::index::is_cachyos_repo(&r) {
28 app.results_filter_show_cachyos
29 } else if crate::index::is_artix_omniverse(&r) {
30 app.results_filter_show_artix_omniverse
31 } else if crate::index::is_artix_universe(&r) {
32 app.results_filter_show_artix_universe
33 } else if crate::index::is_artix_lib32(&r) {
34 app.results_filter_show_artix_lib32
35 } else if crate::index::is_artix_galaxy(&r) {
36 app.results_filter_show_artix_galaxy
37 } else if crate::index::is_artix_world(&r) {
38 app.results_filter_show_artix_world
39 } else if crate::index::is_artix_system(&r) {
40 app.results_filter_show_artix_system
41 } else if crate::index::is_artix_repo(&r) {
42 app.results_filter_show_artix
44 } else if crate::index::is_blackarch_repo(&r) {
45 app.results_filter_show_blackarch
46 } else if let Some(filter_key) = app.repo_results_filter_by_name.get(&r) {
47 app.results_filter_dynamic
48 .get(filter_key)
49 .copied()
50 .unwrap_or(true)
51 } else {
52 app.results_filter_show_core
54 && app.results_filter_show_extra
55 && app.results_filter_show_multilib
56 && app.results_filter_show_eos
57 && app.results_filter_show_cachyos
58 && app.results_filter_show_artix
59 && app.results_filter_show_artix_omniverse
60 && app.results_filter_show_artix_universe
61 && app.results_filter_show_artix_lib32
62 && app.results_filter_show_artix_galaxy
63 && app.results_filter_show_artix_world
64 && app.results_filter_show_artix_system
65 && app.results_filter_show_blackarch
66 }
67}
68
69#[must_use]
84pub fn label_for_official(repo: &str, name: &str, owner: &str) -> String {
85 let r = repo.to_lowercase();
86 if crate::index::is_eos_repo(&r) {
87 "EOS".to_string()
88 } else if crate::index::is_cachyos_repo(&r) {
89 "CachyOS".to_string()
90 } else if crate::index::is_artix_omniverse(&r) {
91 "OMNI".to_string()
92 } else if crate::index::is_artix_universe(&r) {
93 "UNI".to_string()
94 } else if crate::index::is_artix_lib32(&r) {
95 "LIB32".to_string()
96 } else if crate::index::is_artix_galaxy(&r) {
97 "GALAXY".to_string()
98 } else if crate::index::is_artix_world(&r) {
99 "WORLD".to_string()
100 } else if crate::index::is_artix_system(&r) {
101 "SYSTEM".to_string()
102 } else if crate::index::is_artix_repo(&r) {
103 "Artix".to_string()
105 } else if crate::index::is_blackarch_repo(&r) {
106 "BlackArch".to_string()
107 } else if crate::index::is_manjaro_name_or_owner(name, owner) {
108 "Manjaro".to_string()
109 } else {
110 repo.to_string()
111 }
112}
113
114pub fn mirror_update_command(countries: &str, count: u16) -> Result<String, String> {
136 let bin = crate::logic::privilege::active_tool()?.binary_name();
137 let countries_q = crate::install::shell_single_quote(countries);
140 if countries.eq("Worldwide") {
141 Ok(format!(
142 "(if grep -q 'Manjaro' /etc/os-release 2>/dev/null; then \
143 ((command -v pacman-mirrors >/dev/null 2>&1) || pacman -Qi pacman-mirrors >/dev/null 2>&1 || {bin} pacman -S --needed --noconfirm pacman-mirrors) && \
144 {bin} pacman-mirrors --fasttrack {count}; \
145 elif grep -q 'EndeavourOS' /etc/os-release 2>/dev/null; then \
146 ((command -v eos-rankmirrors >/dev/null 2>&1) || pacman -Qi eos-rankmirrors >/dev/null 2>&1 || {bin} pacman -S --needed --noconfirm eos-rankmirrors) && {bin} eos-rankmirrors || echo 'eos-rankmirrors failed'; \
147 (command -v reflector >/dev/null 2>&1 && {bin} reflector --verbose --protocol https --sort rate --latest 20 --download-timeout 6 --save /etc/pacman.d/mirrorlist) || echo 'reflector not found; skipping mirror update'; \
148 elif grep -q 'CachyOS' /etc/os-release 2>/dev/null; then \
149 ((command -v cachyos-rate-mirrors >/dev/null 2>&1) || pacman -Qi cachyos-rate-mirrors >/dev/null 2>&1 || {bin} pacman -S --needed --noconfirm cachyos-rate-mirrors) && {bin} cachyos-rate-mirrors || echo 'cachyos-rate-mirrors failed'; \
150 (command -v reflector >/dev/null 2>&1 && {bin} reflector --verbose --protocol https --sort rate --latest 20 --download-timeout 6 --save /etc/pacman.d/mirrorlist) || echo 'reflector not found; skipping mirror update'; \
151 elif grep -q 'Artix' /etc/os-release 2>/dev/null; then \
152 if ! (pacman -Qi rate-mirrors >/dev/null 2>&1 || command -v rate-mirrors >/dev/null 2>&1); then \
153 if ! (pacman -Qi paru >/dev/null 2>&1 || command -v paru >/dev/null 2>&1) && ! (pacman -Qi yay >/dev/null 2>&1 || command -v yay >/dev/null 2>&1); then \
154 echo 'Error: rate-mirrors is not installed and no AUR helper (yay/paru) found.'; \
155 echo 'Please install yay or paru first, or install rate-mirrors manually.'; \
156 exit 1; \
157 fi; \
158 echo 'rate-mirrors is not installed.'; \
159 read -rp 'Do you want to install rate-mirrors from AUR? (y/n): ' install_choice; \
160 if [ \"$install_choice\" != \"y\" ] && [ \"$install_choice\" != \"Y\" ]; then \
161 echo 'Mirror update cancelled.'; \
162 exit 1; \
163 fi; \
164 if pacman -Qi paru >/dev/null 2>&1 || command -v paru >/dev/null 2>&1; then \
165 paru -S --needed --noconfirm rate-mirrors || exit 1; \
166 elif pacman -Qi yay >/dev/null 2>&1 || command -v yay >/dev/null 2>&1; then \
167 yay -S --needed --noconfirm rate-mirrors || exit 1; \
168 fi; \
169 fi; \
170 {bin} cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.backup.$(date +%Y%m%d_%H%M%S) || exit 1; \
171 if [ {countries_q} = \"Worldwide\" ]; then \
172 rate-mirrors --protocol=https --allow-root --country-neighbors-per-country=3 --top-mirrors-number-to-retest={count} --max-jumps=10 artix | {bin} tee /etc/pacman.d/mirrorlist || exit 1; \
173 else \
174 country_count=$(echo {countries_q} | tr ',' '\\n' | wc -l); \
175 if [ \"$country_count\" -ne 1 ]; then \
176 echo 'Error: Only one country is allowed for Artix mirror update.'; \
177 echo 'Please select a single country.'; \
178 exit 1; \
179 fi; \
180 rate-mirrors --protocol=https --allow-root --entry-country={countries_q} --country-neighbors-per-country=3 --top-mirrors-number-to-retest={count} --max-jumps=10 artix | {bin} tee /etc/pacman.d/mirrorlist || exit 1; \
181 fi; \
182 else \
183 (command -v reflector >/dev/null 2>&1 && {bin} reflector --verbose --protocol https --sort rate --latest 20 --download-timeout 6 --save /etc/pacman.d/mirrorlist) || echo 'reflector not found; skipping mirror update'; \
184 fi)"
185 ))
186 } else {
187 Ok(format!(
188 "(if grep -q 'Manjaro' /etc/os-release 2>/dev/null; then \
189 ((command -v pacman-mirrors >/dev/null 2>&1) || pacman -Qi pacman-mirrors >/dev/null 2>&1 || {bin} pacman -S --needed --noconfirm pacman-mirrors) && \
190 {bin} pacman-mirrors --method rank --country {countries_q}; \
191 elif grep -q 'EndeavourOS' /etc/os-release 2>/dev/null; then \
192 ((command -v eos-rankmirrors >/dev/null 2>&1) || pacman -Qi eos-rankmirrors >/dev/null 2>&1 || {bin} pacman -S --needed --noconfirm eos-rankmirrors) && {bin} eos-rankmirrors || echo 'eos-rankmirrors failed'; \
193 (command -v reflector >/dev/null 2>&1 && {bin} reflector --verbose --country {countries_q} --protocol https --sort rate --latest 20 --download-timeout 6 --save /etc/pacman.d/mirrorlist) || echo 'reflector not found; skipping mirror update'; \
194 elif grep -q 'CachyOS' /etc/os-release 2>/dev/null; then \
195 ((command -v cachyos-rate-mirrors >/dev/null 2>&1) || pacman -Qi cachyos-rate-mirrors >/dev/null 2>&1 || {bin} pacman -S --needed --noconfirm cachyos-rate-mirrors) && {bin} cachyos-rate-mirrors || echo 'cachyos-rate-mirrors failed'; \
196 (command -v reflector >/dev/null 2>&1 && {bin} reflector --verbose --country {countries_q} --protocol https --sort rate --latest 20 --download-timeout 6 --save /etc/pacman.d/mirrorlist) || echo 'reflector not found; skipping mirror update'; \
197 elif grep -q 'Artix' /etc/os-release 2>/dev/null; then \
198 country_count=$(echo {countries_q} | tr ',' '\\n' | wc -l); \
199 if [ \"$country_count\" -ne 1 ]; then \
200 echo 'Error: Only one country is allowed for Artix mirror update.'; \
201 echo 'Please select a single country.'; \
202 exit 1; \
203 fi; \
204 if ! (pacman -Qi rate-mirrors >/dev/null 2>&1 || command -v rate-mirrors >/dev/null 2>&1); then \
205 if ! (pacman -Qi paru >/dev/null 2>&1 || command -v paru >/dev/null 2>&1) && ! (pacman -Qi yay >/dev/null 2>&1 || command -v yay >/dev/null 2>&1); then \
206 echo 'Error: rate-mirrors is not installed and no AUR helper (yay/paru) found.'; \
207 echo 'Please install yay or paru first, or install rate-mirrors manually.'; \
208 exit 1; \
209 fi; \
210 echo 'rate-mirrors is not installed.'; \
211 read -rp 'Do you want to install rate-mirrors from AUR? (y/n): ' install_choice; \
212 if [ \"$install_choice\" != \"y\" ] && [ \"$install_choice\" != \"Y\" ]; then \
213 echo 'Mirror update cancelled.'; \
214 exit 1; \
215 fi; \
216 if pacman -Qi paru >/dev/null 2>&1 || command -v paru >/dev/null 2>&1; then \
217 paru -S --needed --noconfirm rate-mirrors || exit 1; \
218 elif pacman -Qi yay >/dev/null 2>&1 || command -v yay >/dev/null 2>&1; then \
219 yay -S --needed --noconfirm rate-mirrors || exit 1; \
220 fi; \
221 fi; \
222 {bin} cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.backup.$(date +%Y%m%d_%H%M%S) || exit 1; \
223 rate-mirrors --protocol=https --allow-root --entry-country={countries_q} --country-neighbors-per-country=3 --top-mirrors-number-to-retest={count} --max-jumps=10 artix | {bin} tee /etc/pacman.d/mirrorlist || exit 1; \
224 else \
225 (command -v reflector >/dev/null 2>&1 && {bin} reflector --verbose --country {countries_q} --protocol https --sort rate --latest 20 --download-timeout 6 --save /etc/pacman.d/mirrorlist) || echo 'reflector not found; skipping mirror update'; \
226 fi)"
227 ))
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use std::collections::HashMap;
234
235 use super::*;
236 use crate::state::AppState;
237
238 #[test]
239 fn repo_toggle_respects_individual_flags() {
250 let app = AppState {
251 results_filter_show_core: true,
252 results_filter_show_extra: false,
253 results_filter_show_multilib: false,
254 results_filter_show_eos: false,
255 results_filter_show_cachyos: false,
256 results_filter_show_artix: false,
257 results_filter_show_artix_omniverse: false,
258 results_filter_show_artix_universe: false,
259 results_filter_show_artix_lib32: false,
260 results_filter_show_artix_galaxy: false,
261 results_filter_show_artix_world: false,
262 results_filter_show_artix_system: false,
263 ..Default::default()
264 };
265
266 assert!(repo_toggle_for("core", &app));
267 assert!(!repo_toggle_for("extra", &app));
268 assert!(!repo_toggle_for("multilib", &app));
269 }
270
271 #[test]
272 fn repo_toggle_unknown_only_with_full_whitelist() {
283 let mut app = AppState {
284 results_filter_show_core: true,
285 results_filter_show_extra: true,
286 results_filter_show_multilib: true,
287 results_filter_show_eos: true,
288 results_filter_show_cachyos: true,
289 results_filter_show_artix: true,
290 results_filter_show_artix_omniverse: true,
291 results_filter_show_artix_universe: true,
292 results_filter_show_artix_lib32: true,
293 results_filter_show_artix_galaxy: true,
294 results_filter_show_artix_world: true,
295 results_filter_show_artix_system: true,
296 ..Default::default()
297 };
298
299 assert!(repo_toggle_for("unlisted", &app));
300
301 app.results_filter_show_multilib = false;
302 assert!(!repo_toggle_for("unlisted", &app));
303 }
304
305 #[test]
306 fn label_for_official_prefers_special_cases() {
318 assert_eq!(label_for_official("endeavouros", "pkg", ""), "EOS");
319 assert_eq!(label_for_official("cachyos-extra", "pkg", ""), "CachyOS");
320 assert_eq!(label_for_official("omniverse", "pkg", ""), "OMNI");
321 assert_eq!(label_for_official("universe", "pkg", ""), "UNI");
322 assert_eq!(label_for_official("blackarch", "pkg", ""), "BlackArch");
323 assert_eq!(label_for_official("extra", "manjaro-kernel", ""), "Manjaro");
324 assert_eq!(label_for_official("core", "glibc", ""), "core");
325 }
326
327 #[test]
328 fn repo_toggle_blackarch_flag() {
339 let mut app = AppState {
340 results_filter_show_blackarch: true,
341 ..Default::default()
342 };
343 assert!(repo_toggle_for("blackarch", &app));
344 assert!(repo_toggle_for("BlackArch", &app));
345
346 app.results_filter_show_blackarch = false;
347 assert!(!repo_toggle_for("blackarch", &app));
348 }
349
350 #[test]
351 fn repo_toggle_unknown_includes_blackarch_flag() {
362 let mut app = AppState {
363 results_filter_show_core: true,
364 results_filter_show_extra: true,
365 results_filter_show_multilib: true,
366 results_filter_show_eos: true,
367 results_filter_show_cachyos: true,
368 results_filter_show_artix: true,
369 results_filter_show_artix_omniverse: true,
370 results_filter_show_artix_universe: true,
371 results_filter_show_artix_lib32: true,
372 results_filter_show_artix_galaxy: true,
373 results_filter_show_artix_world: true,
374 results_filter_show_artix_system: true,
375 results_filter_show_blackarch: true,
376 ..Default::default()
377 };
378 assert!(repo_toggle_for("unlisted", &app));
379
380 app.results_filter_show_blackarch = false;
381 assert!(!repo_toggle_for("unlisted", &app));
382 }
383
384 #[test]
385 fn repo_toggle_respects_repos_conf_dynamic_filter() {
396 let mut by_name = HashMap::new();
397 by_name.insert("myvendor".to_string(), "vendor_pkgs".to_string());
398 let mut dynamic = HashMap::new();
399 dynamic.insert("vendor_pkgs".to_string(), false);
400 let app = AppState {
401 repo_results_filter_by_name: by_name,
402 results_filter_dynamic: dynamic,
403 ..Default::default()
404 };
405 assert!(!repo_toggle_for("myvendor", &app));
406 assert!(!repo_toggle_for("MyVendor", &app));
407 }
408
409 #[test]
410 fn mirror_update_worldwide_includes_fasttrack_path() {
422 let cmd = mirror_update_command("Worldwide", 8).expect("mirror command");
423 assert!(cmd.contains("grep -q 'Manjaro' /etc/os-release"));
424 assert!(cmd.contains("pacman-mirrors --fasttrack 8"));
425 }
426
427 #[test]
428 fn mirror_update_regional_propagates_country_argument() {
440 let bin = crate::logic::privilege::active_tool()
441 .expect("privilege tool")
442 .binary_name();
443 let countries = "Germany,France";
444 let cmd = mirror_update_command(countries, 5).expect("mirror command");
445 assert!(cmd.contains("--country 'Germany,France'"));
446 assert!(cmd.contains("grep -q 'EndeavourOS' /etc/os-release"));
447 assert!(
448 cmd.contains(&format!(
449 "{bin} reflector --verbose --country 'Germany,France'"
450 )),
451 "expected '{bin} reflector ...' in: {cmd}"
452 );
453 }
454
455 #[test]
456 fn mirror_update_includes_distro_specific_helpers() {
468 let bin = crate::logic::privilege::active_tool()
469 .expect("privilege tool")
470 .binary_name();
471 let cmd = mirror_update_command("Worldwide", 3).expect("mirror command");
472 assert!(
473 cmd.contains(&format!("{bin} eos-rankmirrors")),
474 "expected '{bin} eos-rankmirrors' in: {cmd}"
475 );
476 assert!(cmd.contains("cachyos-rate-mirrors"));
477 assert!(cmd.contains("echo 'reflector not found; skipping mirror update'"));
478 }
479
480 #[test]
481 fn mirror_update_worldwide_includes_artix_path() {
493 let cmd = mirror_update_command("Worldwide", 10).expect("mirror command");
494 assert!(cmd.contains("grep -q 'Artix' /etc/os-release"));
495 assert!(cmd.contains("rate-mirrors"));
496 assert!(cmd.contains("--top-mirrors-number-to-retest=10"));
497 assert!(cmd.contains("--allow-root"));
498 assert!(cmd.contains(" artix"));
499 }
500
501 #[test]
502 fn mirror_update_artix_regional_includes_country_validation() {
514 let cmd = mirror_update_command("Germany", 5).expect("mirror command");
515 assert!(cmd.contains("grep -q 'Artix' /etc/os-release"));
516 assert!(cmd.contains("--entry-country='Germany'"));
517 assert!(cmd.contains("Only one country is allowed"));
518 assert!(cmd.contains("country_count=$(echo 'Germany'"));
519 assert!(cmd.contains("--top-mirrors-number-to-retest=5"));
520 }
521
522 #[test]
523 fn mirror_update_artix_includes_installation_checks() {
535 let cmd = mirror_update_command("Germany", 8).expect("mirror command");
536 assert!(cmd.contains("pacman -Qi rate-mirrors"));
537 assert!(cmd.contains("pacman -Qi paru"));
538 assert!(cmd.contains("pacman -Qi yay"));
539 assert!(cmd.contains("command -v rate-mirrors"));
540 assert!(cmd.contains("command -v paru"));
541 assert!(cmd.contains("command -v yay"));
542 assert!(cmd.contains("Do you want to install rate-mirrors from AUR?"));
543 assert!(cmd.contains("Mirror update cancelled"));
544 }
545
546 #[test]
547 fn mirror_update_escapes_shell_metacharacters_in_countries() {
561 let countries = "Germany'; rm -rf /tmp/pwn; echo '";
562 let cmd = mirror_update_command(countries, 5).expect("mirror command");
563 assert!(
564 !cmd.contains("'Germany'; rm"),
565 "raw single quote must not close the shell quoting: {cmd}"
566 );
567 assert!(
568 cmd.contains("'Germany'\"'\"'; rm"),
569 "embedded quote should be escaped with the POSIX idiom: {cmd}"
570 );
571 }
572
573 #[test]
574 fn mirror_update_artix_includes_backup() {
586 let bin = crate::logic::privilege::active_tool()
587 .expect("privilege tool")
588 .binary_name();
589 let cmd = mirror_update_command("Germany", 5).expect("mirror command");
590 assert!(
591 cmd.contains(&format!("{bin} cp /etc/pacman.d/mirrorlist")),
592 "expected '{bin} cp ...' in: {cmd}"
593 );
594 assert!(cmd.contains("mirrorlist.backup"));
595 assert!(cmd.contains("date +%Y%m%d_%H%M%S"));
596 }
597}