Skip to main content

sbom_diff/renderer/
text.rs

1use super::{
2    kind_suffix, write_full, write_summary, FieldChangeFormatter, FullFormatter, RenderOptions,
3    Renderer, SectionKind, SummaryFormatter, SummaryRenderer,
4};
5use crate::{Diff, EcosystemCounts, EdgeDiff};
6use sbom_model::Component;
7use std::collections::BTreeMap;
8use std::io::Write;
9
10/// plain text renderer for terminal output.
11pub struct TextRenderer;
12
13impl FieldChangeFormatter for TextRenderer {
14    fn field_change<W: Write>(
15        &self,
16        w: &mut W,
17        name: &str,
18        old: &str,
19        new: &str,
20    ) -> std::io::Result<()> {
21        writeln!(w, "  {}: {} -> {}", name, old, new)
22    }
23
24    fn hash_header<W: Write>(&self, w: &mut W, downgrade: bool) -> std::io::Result<()> {
25        if downgrade {
26            writeln!(w, "  Hashes (algorithm downgrade):")
27        } else {
28            writeln!(w, "  Hashes:")
29        }
30    }
31
32    fn hash_removed<W: Write>(&self, w: &mut W, algo: &str, digest: &str) -> std::io::Result<()> {
33        writeln!(w, "    - {}: {}", algo, digest)
34    }
35
36    fn hash_changed<W: Write>(
37        &self,
38        w: &mut W,
39        algo: &str,
40        old: &str,
41        new: &str,
42    ) -> std::io::Result<()> {
43        writeln!(w, "    ~ {}: {} -> {}", algo, old, new)
44    }
45
46    fn hash_added<W: Write>(&self, w: &mut W, algo: &str, digest: &str) -> std::io::Result<()> {
47        writeln!(w, "    + {}: {}", algo, digest)
48    }
49
50    fn component_header<W: Write>(&self, w: &mut W, id: &str) -> std::io::Result<()> {
51        writeln!(w, "{}", id)
52    }
53}
54
55impl FullFormatter for TextRenderer {
56    fn full_warnings<W: Write>(&self, w: &mut W, opts: &RenderOptions) -> std::io::Result<()> {
57        writeln!(w, "[!] Warnings")?;
58        writeln!(w, "------------")?;
59        for warning in &opts.old_warnings {
60            writeln!(w, "[old] {}", warning)?;
61        }
62        for warning in &opts.new_warnings {
63            writeln!(w, "[new] {}", warning)?;
64        }
65        writeln!(w)
66    }
67
68    fn full_count_header<W: Write>(&self, w: &mut W, diff: &Diff) -> std::io::Result<()> {
69        writeln!(w, "Diff Summary")?;
70        writeln!(w, "============")?;
71        self.write_counts(w, diff)?;
72        writeln!(w)
73    }
74
75    fn full_ecosystem_breakdown<W: Write>(
76        &self,
77        w: &mut W,
78        breakdown: &BTreeMap<String, EcosystemCounts>,
79    ) -> std::io::Result<()> {
80        writeln!(w, "By Ecosystem")?;
81        writeln!(w, "------------")?;
82        for (eco, counts) in breakdown {
83            writeln!(
84                w,
85                "{}: {} added, {} removed, {} changed",
86                eco, counts.added, counts.removed, counts.changed
87            )?;
88        }
89        writeln!(w)
90    }
91
92    fn full_ecosystem_header<W: Write>(&self, w: &mut W, ecosystem: &str) -> std::io::Result<()> {
93        writeln!(w, "[{}]", ecosystem)?;
94        writeln!(w)
95    }
96
97    fn section_open<W: Write>(
98        &self,
99        w: &mut W,
100        kind: SectionKind,
101        _count: usize,
102    ) -> std::io::Result<()> {
103        match kind {
104            SectionKind::Added => {
105                writeln!(w, "[+] Added")?;
106                writeln!(w, "---------")
107            }
108            SectionKind::Removed => {
109                writeln!(w, "[-] Removed")?;
110                writeln!(w, "-----------")
111            }
112            SectionKind::Changed => {
113                writeln!(w, "[~] Changed")?;
114                writeln!(w, "-----------")
115            }
116        }
117    }
118
119    fn section_close<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
120        writeln!(w)
121    }
122
123    fn component_list<W: Write>(&self, w: &mut W, components: &[Component]) -> std::io::Result<()> {
124        for c in components {
125            writeln!(w, "{}", c.purl.as_deref().unwrap_or(c.id.as_str()))?;
126        }
127        Ok(())
128    }
129
130    fn edge_open<W: Write>(&self, w: &mut W, _count: usize) -> std::io::Result<()> {
131        writeln!(w, "[~] Edge Changes")?;
132        writeln!(w, "----------------")
133    }
134
135    fn edge_entry<W: Write>(&self, w: &mut W, diff: &Diff, edge: &EdgeDiff) -> std::io::Result<()> {
136        writeln!(w, "{}", diff.display_name(&edge.parent))?;
137        for (removed, kind) in &edge.removed {
138            writeln!(w, "  - {}{}", diff.display_name(removed), kind_suffix(kind))?;
139        }
140        for (added, kind) in &edge.added {
141            writeln!(w, "  + {}{}", diff.display_name(added), kind_suffix(kind))?;
142        }
143        for (changed, (old_kind, new_kind)) in &edge.kind_changed {
144            writeln!(
145                w,
146                "  ~ {} ({} -> {})",
147                diff.display_name(changed),
148                old_kind,
149                new_kind
150            )?;
151        }
152        Ok(())
153    }
154
155    fn edge_close<W: Write>(&self, _w: &mut W) -> std::io::Result<()> {
156        Ok(())
157    }
158
159    fn metadata_open<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
160        writeln!(w, "[~] Metadata Changes")?;
161        writeln!(w, "--------------------")
162    }
163
164    fn metadata_close<W: Write>(&self, _w: &mut W) -> std::io::Result<()> {
165        Ok(())
166    }
167}
168
169impl Renderer for TextRenderer {
170    fn render<W: Write>(
171        &self,
172        diff: &Diff,
173        opts: &RenderOptions,
174        writer: &mut W,
175    ) -> anyhow::Result<()> {
176        write_full(self, diff, opts, writer)?;
177        Ok(())
178    }
179}
180
181impl SummaryFormatter for TextRenderer {
182    fn write_warnings<W: Write>(&self, w: &mut W, opts: &RenderOptions) -> std::io::Result<()> {
183        writeln!(w, "Warnings:     {}", opts.warning_count())?;
184        for warning in &opts.old_warnings {
185            writeln!(w, "  [old] {}", warning)?;
186        }
187        for warning in &opts.new_warnings {
188            writeln!(w, "  [new] {}", warning)?;
189        }
190        writeln!(w)
191    }
192
193    fn write_counts<W: Write>(&self, w: &mut W, diff: &Diff) -> std::io::Result<()> {
194        writeln!(w, "Old total:        {} components", diff.old_total)?;
195        writeln!(w, "New total:        {} components", diff.new_total)?;
196        writeln!(w, "Unchanged:        {}", diff.unchanged)?;
197        writeln!(w, "Added:            {}", diff.added.len())?;
198        writeln!(w, "Removed:          {}", diff.removed.len())?;
199        writeln!(w, "Changed:          {}", diff.changed.len())?;
200        writeln!(w, "Edge changes:     {}", diff.edge_diffs.len())?;
201        writeln!(
202            w,
203            "Metadata changed: {}",
204            if diff.metadata_changed.is_some() {
205                "yes"
206            } else {
207                "no"
208            }
209        )
210    }
211
212    fn write_ecosystem_breakdown<W: Write>(
213        &self,
214        w: &mut W,
215        breakdown: &BTreeMap<String, EcosystemCounts>,
216    ) -> std::io::Result<()> {
217        writeln!(w)?;
218        writeln!(w, "By ecosystem:")?;
219        for (eco, counts) in breakdown {
220            writeln!(
221                w,
222                "  {}: {} added, {} removed, {} changed",
223                eco, counts.added, counts.removed, counts.changed
224            )?;
225        }
226        Ok(())
227    }
228}
229
230impl SummaryRenderer for TextRenderer {
231    fn render_summary<W: Write>(
232        &self,
233        diff: &Diff,
234        opts: &RenderOptions,
235        writer: &mut W,
236    ) -> anyhow::Result<()> {
237        write_summary(self, diff, opts, writer)?;
238        Ok(())
239    }
240}