1use std::collections::HashSet;
4use std::path::Path;
5use std::process::Command;
6
7use crate::state::types::{RepositoryKeyTrust, RepositoryModalRow, RepositoryPacmanStatus};
8use crate::theme::resolve_repos_config_path;
9
10use super::config::load_resolve_repos_from_str;
11use super::pacman_conf::{PacmanRepoPresence, scan_pacman_conf_path};
12
13pub fn build_repositories_modal_fields(
26 repos_path: Option<&Path>,
27 pacman_conf_path: &Path,
28) -> (Vec<RepositoryModalRow>, Option<String>, Vec<String>) {
29 let scan = scan_pacman_conf_path(pacman_conf_path);
30 let pacman_warnings = scan.warnings.clone();
31 let key_index = pacman_trusted_key_index();
32
33 let mut repos_conf_error: Option<String> = None;
34 let mut rows: Vec<RepositoryModalRow> = Vec::new();
35
36 let content_opt: Option<String> = repos_path.and_then(|p| match std::fs::read_to_string(p) {
37 Ok(c) => Some(c),
38 Err(e) => {
39 repos_conf_error = Some(format!("Could not read repos.conf ({}): {e}", p.display()));
40 None
41 }
42 });
43
44 if let Some(content) = content_opt {
45 match load_resolve_repos_from_str(&content) {
46 Ok((repo_rows, _map)) => {
47 for r in repo_rows {
48 let name = r.name.as_deref().unwrap_or("").trim().to_string();
49 let rf = r.results_filter.as_deref().unwrap_or("").trim().to_string();
50 let presence = scan.presence_of(&name);
51 let (pacman_status, source_hint) = map_presence(presence);
52 let key_trust = r
53 .key_id
54 .as_deref()
55 .map(str::trim)
56 .filter(|s| !s.is_empty())
57 .map_or(RepositoryKeyTrust::NotApplicable, |kid| {
58 classify_key(kid, key_index.as_ref())
59 });
60 rows.push(RepositoryModalRow {
61 pacman_section_name: name,
62 results_filter_display: rf,
63 pacman_status,
64 source_hint,
65 key_trust,
66 });
67 }
68 }
69 Err(e) => {
70 repos_conf_error = Some(e);
71 }
72 }
73 }
74
75 (rows, repos_conf_error, pacman_warnings)
76}
77
78#[must_use]
89pub fn build_repositories_modal_fields_default()
90-> (Vec<RepositoryModalRow>, Option<String>, Vec<String>) {
91 build_repositories_modal_fields(
92 resolve_repos_config_path().as_deref(),
93 Path::new("/etc/pacman.conf"),
94 )
95}
96
97fn map_presence(presence: PacmanRepoPresence) -> (RepositoryPacmanStatus, Option<String>) {
108 match presence {
109 PacmanRepoPresence::Absent => (RepositoryPacmanStatus::Absent, None),
110 PacmanRepoPresence::Active { source } => (
111 RepositoryPacmanStatus::Active,
112 source.as_deref().and_then(short_source_hint),
113 ),
114 PacmanRepoPresence::Commented { source } => (
115 RepositoryPacmanStatus::Commented,
116 source.as_deref().and_then(short_source_hint),
117 ),
118 }
119}
120
121fn short_source_hint(p: &std::path::Path) -> Option<String> {
129 p.file_name()
130 .map(|s| s.to_string_lossy().into_owned())
131 .filter(|s| !s.is_empty())
132}
133
134struct TrustedKeyIndex {
146 full_fingerprints: HashSet<String>,
148 long_key_ids: HashSet<String>,
150 short_key_ids: HashSet<String>,
152}
153
154impl TrustedKeyIndex {
155 fn from_pacman_list_keys_stdout(stdout: &str) -> Self {
169 let mut full_fingerprints = HashSet::new();
170 let mut long_key_ids = HashSet::new();
171 let mut short_key_ids = HashSet::new();
172 for line in stdout.lines() {
173 for run in hex_digit_runs(line) {
174 if run.len() == 40 {
175 let fp = run.to_uppercase();
176 long_key_ids.insert(fp[24..40].to_string());
177 short_key_ids.insert(fp[32..40].to_string());
178 full_fingerprints.insert(fp);
179 }
180 }
181 }
182 Self {
183 full_fingerprints,
184 long_key_ids,
185 short_key_ids,
186 }
187 }
188}
189
190fn hex_digit_runs(line: &str) -> Vec<&str> {
201 let mut runs = Vec::new();
202 let mut start: Option<usize> = None;
203 for (i, c) in line.char_indices() {
204 if c.is_ascii_hexdigit() {
205 if start.is_none() {
206 start = Some(i);
207 }
208 } else if let Some(st) = start.take() {
209 runs.push(&line[st..i]);
210 }
211 }
212 if let Some(st) = start {
213 runs.push(&line[st..]);
214 }
215 runs
216}
217
218fn pacman_trusted_key_index() -> Option<TrustedKeyIndex> {
230 if which::which("pacman-key").is_err() {
231 return None;
232 }
233 let out = Command::new("pacman-key")
234 .arg("--list-keys")
235 .output()
236 .ok()?;
237 if !out.status.success() {
238 return None;
239 }
240 Some(TrustedKeyIndex::from_pacman_list_keys_stdout(
241 &String::from_utf8_lossy(&out.stdout),
242 ))
243}
244
245fn classify_key(key_id: &str, index: Option<&TrustedKeyIndex>) -> RepositoryKeyTrust {
261 let needle: String = key_id
262 .chars()
263 .filter(char::is_ascii_hexdigit)
264 .collect::<String>()
265 .to_uppercase();
266 if needle.len() < 8 {
267 return RepositoryKeyTrust::Unknown;
268 }
269 let Some(idx) = index else {
270 return RepositoryKeyTrust::Unknown;
271 };
272 let trusted = match needle.len() {
273 40 => idx.full_fingerprints.contains(&needle),
274 16 => idx.long_key_ids.contains(&needle),
275 8 => idx.short_key_ids.contains(&needle),
276 _ => return RepositoryKeyTrust::Unknown,
277 };
278 if trusted {
279 RepositoryKeyTrust::Trusted
280 } else {
281 RepositoryKeyTrust::NotTrusted
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use tempfile::tempdir;
289
290 #[test]
291 fn merge_row_with_active_pacman_section() {
292 let tmp = tempdir().expect("td");
293 let repo_file = tmp.path().join("repos.conf");
294 std::fs::write(
295 &repo_file,
296 r#"
297[[repo]]
298name = "myrepo"
299results_filter = "mine"
300key_id = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
301"#,
302 )
303 .expect("write repos");
304 let pac = tmp.path().join("pacman.conf");
305 std::fs::write(&pac, "[myrepo]\nServer = https://x.test\n").expect("write pacman");
306 let (rows, err, _) =
307 build_repositories_modal_fields(Some(repo_file.as_path()), pac.as_path());
308 assert!(err.is_none(), "{err:?}");
309 assert_eq!(rows.len(), 1);
310 assert_eq!(rows[0].pacman_section_name, "myrepo");
311 assert_eq!(rows[0].results_filter_display, "mine");
312 assert_eq!(rows[0].pacman_status, RepositoryPacmanStatus::Active);
313 }
314
315 #[test]
316 fn classify_key_rejects_short_id_not_matching_any_listed_fingerprint_suffix() {
317 let stdout = concat!(
320 "1111111111111111111111111111111111111111\n",
321 "2222222222222222222222222222222222222222\n",
322 );
323 let index = TrustedKeyIndex::from_pacman_list_keys_stdout(stdout);
324 assert_eq!(
325 classify_key("5678ABCD", Some(&index)),
326 RepositoryKeyTrust::NotTrusted
327 );
328 }
329
330 #[test]
331 fn classify_key_rejects_short_id_that_exists_only_across_merged_hex_runs() {
332 let stdout = concat!(
335 "pub\n 0000000000000000000000000000000012345678\n",
336 "uid a <a@test>\n",
337 "\n",
338 "pub\n ABCDEF0123456789ABCDEF0123456789012345678\n",
339 );
340 let index = TrustedKeyIndex::from_pacman_list_keys_stdout(stdout);
341 assert_eq!(
342 classify_key("5678ABCD", Some(&index)),
343 RepositoryKeyTrust::NotTrusted
344 );
345 }
346
347 #[test]
348 fn classify_key_trusted_when_short_id_matches_listed_fingerprint_suffix() {
349 let stdout = "0000000000000000000000000000000012345678\n";
350 let index = TrustedKeyIndex::from_pacman_list_keys_stdout(stdout);
351 assert_eq!(
352 classify_key("12345678", Some(&index)),
353 RepositoryKeyTrust::Trusted
354 );
355 }
356
357 #[test]
358 fn classify_key_unknown_when_normalized_id_shorter_than_eight_hex() {
359 let stdout = "0000000000000000000000000000000012345678\n";
360 let index = TrustedKeyIndex::from_pacman_list_keys_stdout(stdout);
361 assert_eq!(
362 classify_key("1234567", Some(&index)),
363 RepositoryKeyTrust::Unknown
364 );
365 }
366
367 #[test]
368 fn classify_key_unknown_when_keyring_index_unavailable() {
369 assert_eq!(classify_key("12345678", None), RepositoryKeyTrust::Unknown);
370 }
371
372 #[test]
373 fn classify_accepts_full_long_and_short_key_forms() {
374 let fp = "0123456789ABCDEF0123456789ABCDEF01234567";
375 let listing = format!("pub rsa4096 2020-01-01 [SC]\n {fp}\n");
376 let idx = TrustedKeyIndex::from_pacman_list_keys_stdout(&listing);
377 assert_eq!(classify_key(fp, Some(&idx)), RepositoryKeyTrust::Trusted);
378 assert_eq!(
379 classify_key("89ABCDEF01234567", Some(&idx)),
380 RepositoryKeyTrust::Trusted
381 );
382 assert_eq!(
383 classify_key("01234567", Some(&idx)),
384 RepositoryKeyTrust::Trusted
385 );
386 }
387
388 #[test]
389 fn classify_rejects_short_id_that_is_only_inside_fingerprint() {
390 let fp = "0123456789ABCDEF0123456789ABCDEF01234567";
391 let listing = format!("pub\n {fp}\n");
392 let idx = TrustedKeyIndex::from_pacman_list_keys_stdout(&listing);
393 assert_eq!(
394 classify_key("89ABCDEF", Some(&idx)),
395 RepositoryKeyTrust::NotTrusted
396 );
397 }
398
399 #[test]
400 fn classify_rejects_sixteen_hex_that_is_not_long_key_id_suffix() {
401 let fp = "0123456789ABCDEF0123456789ABCDEF01234567";
402 let listing = format!("pub\n {fp}\n");
403 let idx = TrustedKeyIndex::from_pacman_list_keys_stdout(&listing);
404 assert_eq!(
405 classify_key("456789ABCDEF0123", Some(&idx)),
406 RepositoryKeyTrust::NotTrusted
407 );
408 }
409
410 #[test]
411 fn classify_unknown_for_nonstandard_hex_lengths() {
412 let fp = "0123456789ABCDEF0123456789ABCDEF01234567";
413 let listing = format!("pub\n {fp}\n");
414 let idx = TrustedKeyIndex::from_pacman_list_keys_stdout(&listing);
415 assert_eq!(
416 classify_key("0123456789ABC", Some(&idx)),
417 RepositoryKeyTrust::Unknown
418 );
419 }
420
421 #[test]
422 fn hex_digit_runs_splits_non_hex_separators() {
423 assert_eq!(
424 hex_digit_runs(" ABCD0123EFABCD0123EFABCD0123EFABCD0123"),
425 vec!["ABCD0123EFABCD0123EFABCD0123EFABCD0123"]
426 );
427 }
428
429 #[test]
430 fn repos_parse_error_surfaces() {
431 let tmp = tempdir().expect("td");
432 let repo_file = tmp.path().join("repos.conf");
433 std::fs::write(
434 &repo_file,
435 r#"
436[[repo]]
437preset = "unsupported"
438"#,
439 )
440 .expect("bad");
441 let pac = tmp.path().join("pacman.conf");
442 std::fs::write(&pac, "\n").expect("write");
443 let (rows, err, _) =
444 build_repositories_modal_fields(Some(repo_file.as_path()), pac.as_path());
445 assert!(err.is_some());
446 assert!(rows.is_empty());
447 }
448}