1use std::cmp::Ordering;
4use std::collections::{BTreeMap, BTreeSet};
5
6use serde::Serialize;
7
8use crate::rpmvercmp::compare_evr;
9
10#[derive(Debug, PartialEq, Eq, Serialize)]
12pub struct VersionChange {
13 pub name: String,
14 pub source_version: String,
15 pub target_version: String,
16}
17
18#[derive(Debug, Serialize)]
20pub struct CompareResult {
21 pub added: Vec<String>,
22 pub removed: Vec<String>,
23 pub upgraded: Vec<VersionChange>,
24 pub downgraded: Vec<VersionChange>,
25 pub unchanged: Vec<String>,
26}
27
28pub fn split_entry(entry: &str) -> (&str, Option<&str>, Option<&str>) {
32 if let Some((name, version)) = entry.split_once(" >= ") {
34 return (name, Some(">="), Some(version));
35 }
36 if let Some((name, version)) = entry.split_once(" = ") {
37 return (name, Some("="), Some(version));
38 }
39 (entry, None, None)
40}
41
42fn split_solib(entry: &str) -> Option<(String, &str)> {
46 let open = entry.find('(')?;
47 if !entry[..open].contains(".so.") {
50 return None;
51 }
52 let close = entry[open..].find(')')? + open;
53 let content = &entry[open + 1..close];
54 if content.is_empty() {
55 return None;
56 }
57 let name = format!("{}(){}", &entry[..open], &entry[close + 1..]);
58 Some((name, content))
59}
60
61pub fn filter_self_deps(entries: Vec<String>, subpkg_names: &BTreeSet<String>) -> Vec<String> {
65 entries
66 .into_iter()
67 .filter(|entry| {
68 let (name, _, _) = split_entry(entry);
69 !subpkg_names.contains(name)
70 })
71 .collect()
72}
73
74pub fn diff(source: Vec<String>, target: Vec<String>) -> CompareResult {
77 let source_set: BTreeSet<String> = source.into_iter().collect();
78 let target_set: BTreeSet<String> = target.into_iter().collect();
79
80 let raw_added: BTreeSet<&String> = target_set.difference(&source_set).collect();
81 let raw_removed: BTreeSet<&String> = source_set.difference(&target_set).collect();
82 let unchanged: Vec<String> = source_set.intersection(&target_set).cloned().collect();
83
84 let mut removed_by_name: BTreeMap<&str, (&str, &str)> = BTreeMap::new();
86 for r in &raw_removed {
87 let (name, op, version) = split_entry(r);
88 if let (Some(op), Some(v)) = (op, version) {
89 removed_by_name.insert(name, (op, v));
90 }
91 }
92
93 let mut added = Vec::new();
94 let mut changed: Vec<VersionChange> = Vec::new();
95 let mut changed_names: BTreeSet<&str> = BTreeSet::new();
96
97 for a in &raw_added {
98 let (name, op, version) = split_entry(a);
99 match (op, version, removed_by_name.get(name)) {
100 (Some(target_op), Some(target_v), Some(&(source_op, source_v)))
101 if target_op == source_op =>
102 {
103 let prefix = if target_op == "=" { "" } else { ">= " };
104 changed.push(VersionChange {
105 name: name.to_string(),
106 source_version: format!("{prefix}{source_v}"),
107 target_version: format!("{prefix}{target_v}"),
108 });
109 changed_names.insert(name);
110 }
111 _ => added.push(a.to_string()),
112 }
113 }
114
115 let remaining_removed: Vec<String> = raw_removed
116 .iter()
117 .filter(|r| {
118 let (name, _, _) = split_entry(r);
119 !changed_names.contains(name)
120 })
121 .map(|r| r.to_string())
122 .collect();
123
124 let mut solib_removed_by_pattern: BTreeMap<String, (String, &str)> = BTreeMap::new();
126 for r in &remaining_removed {
127 if let Some((pattern, version)) = split_solib(r) {
128 solib_removed_by_pattern.insert(pattern, (r.clone(), version));
129 }
130 }
131
132 let mut solib_changed_originals: BTreeSet<String> = BTreeSet::new();
133 let mut final_added = Vec::new();
134 for a in &added {
135 if let Some((pattern, target_v)) = split_solib(a)
136 && let Some((original, source_v)) = solib_removed_by_pattern.get(&pattern)
137 {
138 changed.push(VersionChange {
139 name: pattern,
140 source_version: source_v.to_string(),
141 target_version: target_v.to_string(),
142 });
143 solib_changed_originals.insert(original.clone());
144 continue;
145 }
146 final_added.push(a.clone());
147 }
148
149 let removed: Vec<String> = remaining_removed
150 .into_iter()
151 .filter(|r| !solib_changed_originals.contains(r))
152 .collect();
153
154 changed.sort_by(|a, b| a.name.cmp(&b.name));
155
156 let mut upgraded = Vec::new();
159 let mut downgraded = Vec::new();
160 for c in changed {
161 let src = c
162 .source_version
163 .strip_prefix(">= ")
164 .unwrap_or(&c.source_version);
165 let tgt = c
166 .target_version
167 .strip_prefix(">= ")
168 .unwrap_or(&c.target_version);
169 match compare_evr(src, tgt) {
170 Ordering::Less => upgraded.push(c),
171 Ordering::Greater => downgraded.push(c),
172 Ordering::Equal => upgraded.push(c),
173 }
174 }
175
176 CompareResult {
177 added: final_added,
178 removed,
179 upgraded,
180 downgraded,
181 unchanged,
182 }
183}
184
185pub fn format_result(
190 result: &CompareResult,
191 label: &str,
192 source_branch: &str,
193 target_branch: &str,
194 show_unchanged: bool,
195) -> String {
196 use std::fmt::Write;
197 let mut out = String::new();
198
199 if result.added.is_empty()
200 && result.removed.is_empty()
201 && result.upgraded.is_empty()
202 && result.downgraded.is_empty()
203 {
204 writeln!(out, "No differences in {label}.").unwrap();
205 if show_unchanged && !result.unchanged.is_empty() {
206 writeln!(out).unwrap();
207 writeln!(out, "Unchanged:").unwrap();
208 for p in &result.unchanged {
209 writeln!(out, " {p}").unwrap();
210 }
211 }
212 return out;
213 }
214 let mut need_blank = false;
215 for (section_label, entries) in [
216 ("Upgraded", &result.upgraded),
217 ("Downgraded", &result.downgraded),
218 ] {
219 if entries.is_empty() {
220 continue;
221 }
222 if need_blank {
223 writeln!(out).unwrap();
224 }
225 let name_w = entries.iter().map(|u| u.name.len()).max().unwrap();
226 let src_w = entries
227 .iter()
228 .map(|u| u.source_version.len())
229 .max()
230 .unwrap()
231 .max(source_branch.len());
232 let tgt_w = entries
233 .iter()
234 .map(|u| u.target_version.len())
235 .max()
236 .unwrap()
237 .max(target_branch.len());
238
239 let sep = format!(
240 "+-{}-+-{}-+-{}-+",
241 "-".repeat(name_w),
242 "-".repeat(src_w),
243 "-".repeat(tgt_w)
244 );
245 writeln!(out, "{section_label} ({source_branch} -> {target_branch}):").unwrap();
246 writeln!(out, "{sep}").unwrap();
247 writeln!(
248 out,
249 "| {:<name_w$} | {:<src_w$} | {:<tgt_w$} |",
250 label, source_branch, target_branch
251 )
252 .unwrap();
253 writeln!(out, "{sep}").unwrap();
254 for u in entries {
255 writeln!(
256 out,
257 "| {:<name_w$} | {:<src_w$} | {:<tgt_w$} |",
258 u.name, u.source_version, u.target_version
259 )
260 .unwrap();
261 }
262 writeln!(out, "{sep}").unwrap();
263 need_blank = true;
264 }
265 if !result.removed.is_empty() {
266 if need_blank {
267 writeln!(out).unwrap();
268 }
269 writeln!(out, "Removed (in {source_branch} but not {target_branch}):").unwrap();
270 for p in &result.removed {
271 writeln!(out, " - {p}").unwrap();
272 }
273 need_blank = true;
274 }
275 if !result.added.is_empty() {
276 if need_blank {
277 writeln!(out).unwrap();
278 }
279 writeln!(out, "Added (in {target_branch} but not {source_branch}):").unwrap();
280 for p in &result.added {
281 writeln!(out, " + {p}").unwrap();
282 }
283 need_blank = true;
284 }
285 if show_unchanged && !result.unchanged.is_empty() {
286 if need_blank {
287 writeln!(out).unwrap();
288 }
289 writeln!(out, "Unchanged:").unwrap();
290 for p in &result.unchanged {
291 writeln!(out, " {p}").unwrap();
292 }
293 }
294 out
295}
296
297pub fn print_result(
302 result: &CompareResult,
303 label: &str,
304 source_branch: &str,
305 target_branch: &str,
306 show_unchanged: bool,
307) {
308 print!(
309 "{}",
310 format_result(result, label, source_branch, target_branch, show_unchanged)
311 );
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn filter_self_deps_removes_matching_names() {
320 let entries = vec![
321 "libbpf = 2:1.5.0-3.el9".to_string(),
322 "libbpf-devel = 2:1.5.0-3.el9".to_string(),
323 "glibc >= 2.28".to_string(),
324 "kernel-headers".to_string(),
325 "libbpf.so.1()(64bit)".to_string(),
326 ];
327 let self_names: BTreeSet<String> = ["libbpf", "libbpf-devel"]
328 .iter()
329 .map(|s| s.to_string())
330 .collect();
331 let filtered = filter_self_deps(entries, &self_names);
332 assert_eq!(
333 filtered,
334 vec!["glibc >= 2.28", "kernel-headers", "libbpf.so.1()(64bit)",]
335 );
336 }
337
338 #[test]
339 fn filter_self_deps_empty_names() {
340 let entries = vec!["foo".to_string(), "bar = 1.0".to_string()];
341 let self_names: BTreeSet<String> = BTreeSet::new();
342 let filtered = filter_self_deps(entries.clone(), &self_names);
343 assert_eq!(filtered, entries);
344 }
345
346 #[test]
347 fn split_entry_with_equals() {
348 let (name, op, version) = split_entry("libbpf(aarch-64) = 2:1.5.0-3.el9");
349 assert_eq!(name, "libbpf(aarch-64)");
350 assert_eq!(op, Some("="));
351 assert_eq!(version, Some("2:1.5.0-3.el9"));
352 }
353
354 #[test]
355 fn split_entry_with_gte() {
356 let (name, op, version) = split_entry("kernel-headers >= 5.14.0-647");
357 assert_eq!(name, "kernel-headers");
358 assert_eq!(op, Some(">="));
359 assert_eq!(version, Some("5.14.0-647"));
360 }
361
362 #[test]
363 fn split_entry_without_version() {
364 let (name, op, version) = split_entry("/usr/sbin/halt");
365 assert_eq!(name, "/usr/sbin/halt");
366 assert_eq!(op, None);
367 assert_eq!(version, None);
368 }
369
370 #[test]
371 fn diff_empty() {
372 let result = diff(vec![], vec![]);
373 assert!(result.added.is_empty());
374 assert!(result.removed.is_empty());
375 assert!(result.upgraded.is_empty());
376 assert!(result.downgraded.is_empty());
377 }
378
379 #[test]
380 fn diff_added_only() {
381 let result = diff(vec![], vec!["foo".to_string()]);
382 assert_eq!(result.added, vec!["foo"]);
383 assert!(result.removed.is_empty());
384 assert!(result.upgraded.is_empty());
385 assert!(result.downgraded.is_empty());
386 }
387
388 #[test]
389 fn diff_removed_only() {
390 let result = diff(vec!["foo".to_string()], vec![]);
391 assert!(result.added.is_empty());
392 assert_eq!(result.removed, vec!["foo"]);
393 assert!(result.upgraded.is_empty());
394 assert!(result.downgraded.is_empty());
395 }
396
397 #[test]
398 fn diff_detects_upgrade() {
399 let source = vec!["libbpf = 1.0".to_string()];
400 let target = vec!["libbpf = 2.0".to_string()];
401 let result = diff(source, target);
402 assert!(result.added.is_empty());
403 assert!(result.removed.is_empty());
404 assert!(result.downgraded.is_empty());
405 assert_eq!(result.upgraded.len(), 1);
406 assert_eq!(result.upgraded[0].name, "libbpf");
407 assert_eq!(result.upgraded[0].source_version, "1.0");
408 assert_eq!(result.upgraded[0].target_version, "2.0");
409 }
410
411 #[test]
412 fn diff_detects_downgrade() {
413 let source = vec!["libbpf = 2.0".to_string()];
414 let target = vec!["libbpf = 1.0".to_string()];
415 let result = diff(source, target);
416 assert!(result.added.is_empty());
417 assert!(result.removed.is_empty());
418 assert!(result.upgraded.is_empty());
419 assert_eq!(result.downgraded.len(), 1);
420 assert_eq!(result.downgraded[0].name, "libbpf");
421 assert_eq!(result.downgraded[0].source_version, "2.0");
422 assert_eq!(result.downgraded[0].target_version, "1.0");
423 }
424
425 #[test]
426 fn diff_mixed_changes() {
427 let source = vec!["libfoo = 1.0".to_string(), "libold".to_string()];
428 let target = vec!["libfoo = 2.0".to_string(), "libnew".to_string()];
429 let result = diff(source, target);
430 assert_eq!(result.added, vec!["libnew"]);
431 assert_eq!(result.removed, vec!["libold"]);
432 assert_eq!(result.upgraded.len(), 1);
433 assert_eq!(result.upgraded[0].name, "libfoo");
434 assert!(result.downgraded.is_empty());
435 }
436
437 #[test]
438 fn diff_detects_gte_upgrade() {
439 let source = vec!["kernel-headers >= 5.14.0-647".to_string()];
440 let target = vec!["kernel-headers >= 5.16.0".to_string()];
441 let result = diff(source, target);
442 assert!(result.added.is_empty());
443 assert!(result.removed.is_empty());
444 assert!(result.downgraded.is_empty());
445 assert_eq!(result.upgraded.len(), 1);
446 assert_eq!(result.upgraded[0].name, "kernel-headers");
447 assert_eq!(result.upgraded[0].source_version, ">= 5.14.0-647");
448 assert_eq!(result.upgraded[0].target_version, ">= 5.16.0");
449 }
450
451 #[test]
452 fn diff_detects_gte_downgrade() {
453 let source = vec!["kernel-headers >= 5.16.0".to_string()];
454 let target = vec!["kernel-headers >= 5.14.0-647".to_string()];
455 let result = diff(source, target);
456 assert!(result.added.is_empty());
457 assert!(result.removed.is_empty());
458 assert!(result.upgraded.is_empty());
459 assert_eq!(result.downgraded.len(), 1);
460 assert_eq!(result.downgraded[0].name, "kernel-headers");
461 }
462
463 #[test]
464 fn split_solib_with_version() {
465 let (name, version) = split_solib("libc.so.6(GLIBC_2.28)(64bit)").unwrap();
466 assert_eq!(name, "libc.so.6()(64bit)");
467 assert_eq!(version, "GLIBC_2.28");
468 }
469
470 #[test]
471 fn split_solib_empty_parens() {
472 assert!(split_solib("libfoo.so()(64bit)").is_none());
473 }
474
475 #[test]
476 fn split_solib_no_parens() {
477 assert!(split_solib("/usr/sbin/halt").is_none());
478 }
479
480 #[test]
481 fn split_solib_non_solib_parens() {
482 assert!(split_solib("pkgconfig(dracut)").is_none());
483 }
484
485 #[test]
486 fn diff_detects_solib_upgrade() {
487 let source = vec!["libc.so.6(GLIBC_2.28)(64bit)".to_string()];
488 let target = vec!["libc.so.6(GLIBC_2.38)(64bit)".to_string()];
489 let result = diff(source, target);
490 assert!(result.added.is_empty());
491 assert!(result.removed.is_empty());
492 assert!(result.downgraded.is_empty());
493 assert_eq!(result.upgraded.len(), 1);
494 assert_eq!(result.upgraded[0].name, "libc.so.6()(64bit)");
495 assert_eq!(result.upgraded[0].source_version, "GLIBC_2.28");
496 assert_eq!(result.upgraded[0].target_version, "GLIBC_2.38");
497 }
498
499 #[test]
500 fn diff_detects_solib_downgrade() {
501 let source = vec!["libc.so.6(GLIBC_2.38)(64bit)".to_string()];
502 let target = vec!["libc.so.6(GLIBC_2.28)(64bit)".to_string()];
503 let result = diff(source, target);
504 assert!(result.added.is_empty());
505 assert!(result.removed.is_empty());
506 assert!(result.upgraded.is_empty());
507 assert_eq!(result.downgraded.len(), 1);
508 assert_eq!(result.downgraded[0].name, "libc.so.6()(64bit)");
509 }
510
511 #[test]
512 fn diff_non_solib_parens_not_matched() {
513 let source = vec!["pkgconfig(dracut)".to_string()];
514 let target = vec!["pkgconfig(systemd)".to_string()];
515 let result = diff(source, target);
516 assert_eq!(result.added, vec!["pkgconfig(systemd)"]);
517 assert_eq!(result.removed, vec!["pkgconfig(dracut)"]);
518 assert!(result.upgraded.is_empty());
519 assert!(result.downgraded.is_empty());
520 }
521
522 #[test]
523 fn diff_no_changes() {
524 let entries = vec!["foo = 1.0".to_string(), "bar".to_string()];
525 let result = diff(entries.clone(), entries);
526 assert!(result.added.is_empty());
527 assert!(result.removed.is_empty());
528 assert!(result.upgraded.is_empty());
529 assert!(result.downgraded.is_empty());
530 assert_eq!(result.unchanged, vec!["bar", "foo = 1.0"]);
531 }
532
533 #[test]
534 fn diff_tracks_unchanged() {
535 let source = vec![
536 "libfoo = 1.0".to_string(),
537 "libbar".to_string(),
538 "libold".to_string(),
539 ];
540 let target = vec![
541 "libfoo = 2.0".to_string(),
542 "libbar".to_string(),
543 "libnew".to_string(),
544 ];
545 let result = diff(source, target);
546 assert_eq!(result.unchanged, vec!["libbar"]);
547 }
548
549 #[test]
550 fn print_result_no_differences() {
551 let result = CompareResult {
552 added: vec![],
553 removed: vec![],
554 upgraded: vec![],
555 downgraded: vec![],
556 unchanged: vec![],
557 };
558 print_result(&result, "Provide", "f41", "rawhide", false);
560 }
561
562 #[test]
563 fn print_result_upgraded_only() {
564 let result = CompareResult {
565 added: vec![],
566 removed: vec![],
567 upgraded: vec![VersionChange {
568 name: "libbpf".to_string(),
569 source_version: "1.0".to_string(),
570 target_version: "2.0".to_string(),
571 }],
572 downgraded: vec![],
573 unchanged: vec![],
574 };
575 print_result(&result, "Require", "c9s", "f44", false);
576 }
577
578 #[test]
579 fn print_result_downgraded_only() {
580 let result = CompareResult {
581 added: vec![],
582 removed: vec![],
583 upgraded: vec![],
584 downgraded: vec![VersionChange {
585 name: "libbpf".to_string(),
586 source_version: "2.0".to_string(),
587 target_version: "1.0".to_string(),
588 }],
589 unchanged: vec![],
590 };
591 print_result(&result, "Require", "f44", "c9s", false);
592 }
593
594 #[test]
595 fn print_result_added_and_removed() {
596 let result = CompareResult {
597 added: vec!["libnew".to_string()],
598 removed: vec!["libold".to_string()],
599 upgraded: vec![],
600 downgraded: vec![],
601 unchanged: vec![],
602 };
603 print_result(&result, "Provide", "f41", "rawhide", false);
604 }
605
606 #[test]
607 fn print_result_all_sections() {
608 let result = CompareResult {
609 added: vec!["libnew".to_string()],
610 removed: vec!["libold".to_string()],
611 upgraded: vec![VersionChange {
612 name: "libfoo".to_string(),
613 source_version: "1.0".to_string(),
614 target_version: "2.0".to_string(),
615 }],
616 downgraded: vec![VersionChange {
617 name: "libbar".to_string(),
618 source_version: "3.0".to_string(),
619 target_version: "1.0".to_string(),
620 }],
621 unchanged: vec![],
622 };
623 print_result(&result, "Require", "f41", "c9s", false);
625 }
626
627 #[test]
628 fn print_result_show_unchanged_with_differences() {
629 let result = CompareResult {
630 added: vec!["libnew".to_string()],
631 removed: vec![],
632 upgraded: vec![],
633 downgraded: vec![],
634 unchanged: vec!["libcommon".to_string()],
635 };
636 print_result(&result, "Provide", "f41", "rawhide", true);
637 }
638
639 #[test]
640 fn print_result_show_unchanged_no_differences() {
641 let result = CompareResult {
642 added: vec![],
643 removed: vec![],
644 upgraded: vec![],
645 downgraded: vec![],
646 unchanged: vec!["libcommon".to_string()],
647 };
648 print_result(&result, "Provide", "f41", "rawhide", true);
650 }
651
652 #[test]
653 fn print_result_all_sections_with_unchanged() {
654 let result = CompareResult {
655 added: vec!["libnew".to_string()],
656 removed: vec!["libold".to_string()],
657 upgraded: vec![VersionChange {
658 name: "libfoo".to_string(),
659 source_version: "1.0".to_string(),
660 target_version: "2.0".to_string(),
661 }],
662 downgraded: vec![VersionChange {
663 name: "libbar".to_string(),
664 source_version: "3.0".to_string(),
665 target_version: "1.0".to_string(),
666 }],
667 unchanged: vec!["libcommon".to_string()],
668 };
669 print_result(&result, "Require", "f41", "c9s", true);
671 }
672
673 #[test]
674 fn print_result_show_unchanged_false_hides_unchanged() {
675 let result = CompareResult {
676 added: vec![],
677 removed: vec![],
678 upgraded: vec![],
679 downgraded: vec![],
680 unchanged: vec!["libcommon".to_string()],
681 };
682 print_result(&result, "Provide", "f41", "rawhide", false);
684 }
685}