Skip to main content

pacsea/logic/
distro.rs

1//! Distro-related logic helpers (filtering and labels).
2
3/// What: Determine whether results from a repository should be visible under current toggles.
4///
5/// Inputs:
6/// - `repo`: Name of the repository associated with a package result.
7/// - `app`: Application state providing the filter toggles for official repos.
8///
9/// Output:
10/// - `true` when the repository passes the active filters; otherwise `false`.
11///
12/// Details:
13/// - Normalizes repository names and applies special-handling for EOS/CachyOS/Artix/BlackArch classification helpers.
14/// - Repos listed in `repos.conf` use dynamic toggles (`results_filter_show_<canonical>` in `settings.conf`).
15/// - Unknown repositories are only allowed when every official filter is enabled simultaneously.
16#[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        // Fallback for any other Artix repo (shouldn't happen, but safe)
43        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        // Unknown official repo: include only when all official filters are enabled
53        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/// What: Produce a human-friendly label for an official package entry.
70///
71/// Inputs:
72/// - `repo`: Repository reported by the package source.
73/// - `name`: Package name used to detect Manjaro naming conventions.
74/// - `owner`: Optional upstream owner string available from package metadata.
75///
76/// Output:
77/// - Returns a display label describing the ecosystem the package belongs to.
78///
79/// Details:
80/// - Distinguishes `EndeavourOS`, `CachyOS`, `Artix Linux` repos (with specific labels for each Artix repo:
81///   `OMNI`, `UNI`, `LIB32`, `GALAXY`, `WORLD`, `SYSTEM`), `BlackArch`, and detects `Manjaro` branding by name/owner heuristics.
82/// - Falls back to the raw repository string when no special classification matches.
83#[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        // Fallback for any other Artix repo (shouldn't happen, but safe)
104        "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
114/// What: Build a shell snippet to refresh pacman mirrors depending on the detected distro.
115///
116/// Inputs:
117/// - `countries`: Either "Worldwide" to auto-rank mirrors, or a comma-separated list used by the underlying tool
118/// - `count`: Mirror count used by Manjaro fasttrack when `countries` is "Worldwide", or top mirrors number for Artix rate-mirrors
119///
120/// Output:
121/// - Shell command string suitable to run in a terminal
122///
123/// Details:
124/// - On Manjaro (detected via /etc/os-release), ensures pacman-mirrors exists, then:
125///   - Worldwide: `pacman-mirrors --fasttrack {count}`
126///   - Countries: `pacman-mirrors --method rank --country {countries_q}`
127/// - On `Artix` (detected via /etc/os-release), checks for rate-mirrors and `AUR` helper (`yay`/`paru`), prompts for installation if needed, creates backup of mirrorlist, then runs rate-mirrors with country filtering using --entry-country option (only one country allowed, global option must come before the artix command).
128/// - On `EndeavourOS`, ensures `eos-rankmirrors` is installed, runs it, then runs `reflector`.
129/// - On `CachyOS`, ensures `cachyos-rate-mirrors` is installed, runs it, then runs `reflector`.
130/// - Otherwise, attempts to use `reflector` to write `/etc/pacman.d/mirrorlist`; if not found, prints a notice.
131///
132/// # Errors
133///
134/// Returns `Err` when the configured privilege tool cannot be resolved (see [`crate::logic::privilege::active_tool`]).
135pub fn mirror_update_command(countries: &str, count: u16) -> Result<String, String> {
136    let bin = crate::logic::privilege::active_tool()?.binary_name();
137    // Settings/modal input: single-quote once and interpolate the quoted value only,
138    // so shell metacharacters (quotes, `$(`, backticks, newlines) cannot escape.
139    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    /// What: Validate canonical repository toggles deny disabled repositories while permitting enabled ones.
240    ///
241    /// Inputs:
242    /// - `app`: Application state with `core` enabled and other official toggles disabled.
243    ///
244    /// Output:
245    /// - `repo_toggle_for` allows `core` entries but rejects `extra` and `multilib`.
246    ///
247    /// Details:
248    /// - Ensures the per-repository gate respects the individual boolean flags.
249    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    /// What: Ensure unknown official repositories require every official toggle to be enabled.
273    ///
274    /// Inputs:
275    /// - `app`: Application state with all official flags on, then one flag disabled.
276    ///
277    /// Output:
278    /// - Unknown repository accepted when fully enabled and rejected once any flag is turned off.
279    ///
280    /// Details:
281    /// - Exercises the fallback clause guarding unfamiliar repositories.
282    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    /// What: Confirm label helper emits ecosystem-specific aliases for recognised repositories.
307    ///
308    /// Inputs:
309    /// - Repository/name permutations covering `EndeavourOS`, `CachyOS`, `Artix Linux` (with specific repo labels), `Manjaro`, and a generic repo.
310    ///
311    /// Output:
312    /// - Labels reduce to `EOS`, `CachyOS`, `OMNI`, `UNI` (for specific Artix repos), `Manjaro`, and the original repo name respectively.
313    ///
314    /// Details:
315    /// - Validates the Manjaro heuristic via package name and the repo classification helpers.
316    /// - Confirms specific Artix repos return their specific labels (OMNI, UNI, etc.) rather than the generic "Artix" label.
317    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    /// What: Validate `BlackArch` toggle routes through its dedicated flag.
329    ///
330    /// Inputs:
331    /// - `app`: Application state with `BlackArch` enabled, then disabled.
332    ///
333    /// Output:
334    /// - `repo_toggle_for("blackarch", ...)` respects the `results_filter_show_blackarch` flag.
335    ///
336    /// Details:
337    /// - Exercises the `BlackArch` branch in `repo_toggle_for`.
338    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    /// What: Ensure unknown-repo fallback includes `BlackArch` in its all-on requirement.
352    ///
353    /// Inputs:
354    /// - `app`: Application state with all official flags on, then `BlackArch` disabled.
355    ///
356    /// Output:
357    /// - Unknown repo rejected when `BlackArch` flag is off.
358    ///
359    /// Details:
360    /// - Verifies the extended conjunction in the fallback clause.
361    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    /// What: Repos mapped via `repos.conf` respect `results_filter_dynamic`.
386    ///
387    /// Inputs:
388    /// - `app`: Custom repo name `myvendor` with canonical filter `vendor_pkgs` toggled off.
389    ///
390    /// Output:
391    /// - `repo_toggle_for("myvendor", ...)` is `false` when dynamic map disables the filter.
392    ///
393    /// Details:
394    /// - Builtin branches take precedence; this exercises the dynamic `repos.conf` path only.
395    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    /// What: Ensure the Worldwide variant embeds the Manjaro fasttrack workflow.
411    ///
412    /// Inputs:
413    /// - `countries`: `"Worldwide"` to trigger the fasttrack branch.
414    /// - `count`: `8` mirror entries requested.
415    ///
416    /// Output:
417    /// - Command string contains the Manjaro detection guard plus the fasttrack invocation with the requested count.
418    ///
419    /// Details:
420    /// - Verifies the script includes the fasttrack invocation with the requested count.
421    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    /// What: Verify region-specific invocation propagates the provided country list to reflector helpers.
429    ///
430    /// Inputs:
431    /// - `countries`: `"Germany,France"` representing a comma-separated selection.
432    /// - `count`: Arbitrary `5`, unused in the non-Worldwide branch.
433    ///
434    /// Output:
435    /// - Command string includes the quoted country list for both Manjaro rank and reflector fallback branches.
436    ///
437    /// Details:
438    /// - Confirms the `EndeavourOS` clause still emits the reflector call with the customized country list.
439    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    /// What: Confirm `EndeavourOS` and `CachyOS` branches retain their helper invocations and fallback messaging.
457    ///
458    /// Inputs:
459    /// - `countries`: `"Worldwide"` to pass through every branch without country filtering.
460    /// - `count`: `3`, checking the value does not affect non-Manjaro logic.
461    ///
462    /// Output:
463    /// - Command string references both `eos-rankmirrors` and `cachyos-rate-mirrors` along with their retry echo messages.
464    ///
465    /// Details:
466    /// - Guards against accidental removal of distro-specific tooling when modifying the mirrored script.
467    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    /// What: Ensure Artix detection and rate-mirrors workflow is included in Worldwide variant.
482    ///
483    /// Inputs:
484    /// - `countries`: `"Worldwide"` to trigger the Worldwide branch.
485    /// - `count`: `10` top mirrors number for rate-mirrors.
486    ///
487    /// Output:
488    /// - Command string contains Artix detection guard and rate-mirrors invocation without --entry-country.
489    ///
490    /// Details:
491    /// - Verifies Artix branch uses rate-mirrors without --entry-country for Worldwide selection.
492    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    /// What: Verify Artix regional invocation includes country validation and rate-mirrors with --entry-country.
503    ///
504    /// Inputs:
505    /// - `countries`: `"Germany"` representing a single country selection.
506    /// - `count`: `5` top mirrors number for rate-mirrors.
507    ///
508    /// Output:
509    /// - Command string includes Artix detection, country count validation, and rate-mirrors with --entry-country.
510    ///
511    /// Details:
512    /// - Confirms the Artix clause validates single country and uses --entry-country parameter.
513    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    /// What: Confirm Artix branch includes rate-mirrors installation check and AUR helper detection.
524    ///
525    /// Inputs:
526    /// - `countries`: `"Germany"` to trigger Artix branch.
527    /// - `count`: `8` top mirrors number.
528    ///
529    /// Output:
530    /// - Command string includes checks for rate-mirrors, yay, and paru, plus installation prompt.
531    ///
532    /// Details:
533    /// - Validates that the script checks for rate-mirrors and AUR helpers before proceeding.
534    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    /// What: Ensure shell metacharacters in the country selection cannot escape quoting.
548    ///
549    /// Inputs:
550    /// - `countries`: A malicious value embedding a single quote and a shell command.
551    /// - `count`: Arbitrary `5`, unused by the injected fragments.
552    ///
553    /// Output:
554    /// - Command string never contains the payload adjacent to a closing quote; the
555    ///   embedded quote is escaped via the POSIX `'"'"'` idiom.
556    ///
557    /// Details:
558    /// - Guards the mirror script against shell injection through the settings-provided
559    ///   country list (see security rules on `shell_single_quote`).
560    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    /// What: Verify Artix branch creates backup of mirrorlist before updating.
575    ///
576    /// Inputs:
577    /// - `countries`: `"Germany"` to trigger Artix branch.
578    /// - `count`: `5` top mirrors number.
579    ///
580    /// Output:
581    /// - Command string includes backup creation with timestamp.
582    ///
583    /// Details:
584    /// - Ensures mirrorlist backup is created before rate-mirrors execution.
585    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}