Skip to main content

sbom_diff/renderer/
text.rs

1use super::{
2    format_vec_or_none, kind_suffix, write_changed, write_summary, FieldChangeFormatter,
3    RenderOptions, Renderer, SummaryFormatter, SummaryRenderer,
4};
5use crate::{Diff, EcosystemCounts, MetadataChange};
6use sbom_model::Component;
7use std::collections::BTreeMap;
8use std::io::Write;
9
10fn write_text_added<W: Write>(writer: &mut W, components: &[Component]) -> std::io::Result<()> {
11    for c in components {
12        writeln!(writer, "{}", c.purl.as_deref().unwrap_or(c.id.as_str()))?;
13    }
14    Ok(())
15}
16
17/// plain text renderer for terminal output.
18pub struct TextRenderer;
19
20impl FieldChangeFormatter for TextRenderer {
21    fn field_change<W: Write>(
22        &self,
23        w: &mut W,
24        name: &str,
25        old: &str,
26        new: &str,
27    ) -> std::io::Result<()> {
28        writeln!(w, "  {}: {} -> {}", name, old, new)
29    }
30
31    fn hash_header<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
32        writeln!(w, "  Hashes:")
33    }
34
35    fn hash_removed<W: Write>(&self, w: &mut W, algo: &str, digest: &str) -> std::io::Result<()> {
36        writeln!(w, "    - {}: {}", algo, digest)
37    }
38
39    fn hash_changed<W: Write>(
40        &self,
41        w: &mut W,
42        algo: &str,
43        old: &str,
44        new: &str,
45    ) -> std::io::Result<()> {
46        writeln!(w, "    ~ {}: {} -> {}", algo, old, new)
47    }
48
49    fn hash_added<W: Write>(&self, w: &mut W, algo: &str, digest: &str) -> std::io::Result<()> {
50        writeln!(w, "    + {}: {}", algo, digest)
51    }
52
53    fn component_header<W: Write>(&self, w: &mut W, id: &str) -> std::io::Result<()> {
54        writeln!(w, "{}", id)
55    }
56}
57
58impl Renderer for TextRenderer {
59    fn render<W: Write>(
60        &self,
61        diff: &Diff,
62        opts: &RenderOptions,
63        writer: &mut W,
64    ) -> anyhow::Result<()> {
65        if opts.has_warnings() {
66            writeln!(writer, "[!] Warnings")?;
67            writeln!(writer, "------------")?;
68            for w in &opts.old_warnings {
69                writeln!(writer, "[old] {}", w)?;
70            }
71            for w in &opts.new_warnings {
72                writeln!(writer, "[new] {}", w)?;
73            }
74            writeln!(writer)?;
75        }
76
77        writeln!(writer, "Diff Summary")?;
78        writeln!(writer, "============")?;
79        writeln!(writer, "Old total:        {} components", diff.old_total)?;
80        writeln!(writer, "New total:        {} components", diff.new_total)?;
81        writeln!(writer, "Unchanged:        {}", diff.unchanged)?;
82        writeln!(writer, "Added:            {}", diff.added.len())?;
83        writeln!(writer, "Removed:          {}", diff.removed.len())?;
84        writeln!(writer, "Changed:          {}", diff.changed.len())?;
85        writeln!(writer, "Edge changes:     {}", diff.edge_diffs.len())?;
86        writeln!(
87            writer,
88            "Metadata changed: {}",
89            if diff.metadata_changed.is_some() {
90                "yes"
91            } else {
92                "no"
93            }
94        )?;
95        writeln!(writer)?;
96
97        if opts.group_by_ecosystem {
98            let grouped = diff.group_by_ecosystem();
99            let breakdown = grouped.ecosystem_breakdown();
100
101            writeln!(writer, "By Ecosystem")?;
102            writeln!(writer, "------------")?;
103            for (eco, counts) in &breakdown {
104                writeln!(
105                    writer,
106                    "{}: {} added, {} removed, {} changed",
107                    eco, counts.added, counts.removed, counts.changed
108                )?;
109            }
110            writeln!(writer)?;
111
112            for (eco, eco_diff) in &grouped.by_ecosystem {
113                writeln!(writer, "[{}]", eco)?;
114                writeln!(writer)?;
115                if !eco_diff.added.is_empty() {
116                    writeln!(writer, "[+] Added")?;
117                    writeln!(writer, "---------")?;
118                    write_text_added(writer, &eco_diff.added)?;
119                    writeln!(writer)?;
120                }
121                if !eco_diff.removed.is_empty() {
122                    writeln!(writer, "[-] Removed")?;
123                    writeln!(writer, "-----------")?;
124                    write_text_added(writer, &eco_diff.removed)?;
125                    writeln!(writer)?;
126                }
127                if !eco_diff.changed.is_empty() {
128                    writeln!(writer, "[~] Changed")?;
129                    writeln!(writer, "-----------")?;
130                    write_changed(self, writer, &eco_diff.changed)?;
131                    writeln!(writer)?;
132                }
133            }
134        } else {
135            if !diff.added.is_empty() {
136                writeln!(writer, "[+] Added")?;
137                writeln!(writer, "---------")?;
138                write_text_added(writer, &diff.added)?;
139                writeln!(writer)?;
140            }
141
142            if !diff.removed.is_empty() {
143                writeln!(writer, "[-] Removed")?;
144                writeln!(writer, "-----------")?;
145                write_text_added(writer, &diff.removed)?;
146                writeln!(writer)?;
147            }
148
149            if !diff.changed.is_empty() {
150                writeln!(writer, "[~] Changed")?;
151                writeln!(writer, "-----------")?;
152                write_changed(self, writer, &diff.changed)?;
153                writeln!(writer)?;
154            }
155        }
156
157        if !diff.edge_diffs.is_empty() {
158            writeln!(writer, "[~] Edge Changes")?;
159            writeln!(writer, "----------------")?;
160            for edge in &diff.edge_diffs {
161                writeln!(writer, "{}", diff.display_name(&edge.parent))?;
162                for (removed, kind) in &edge.removed {
163                    writeln!(
164                        writer,
165                        "  - {}{}",
166                        diff.display_name(removed),
167                        kind_suffix(kind)
168                    )?;
169                }
170                for (added, kind) in &edge.added {
171                    writeln!(
172                        writer,
173                        "  + {}{}",
174                        diff.display_name(added),
175                        kind_suffix(kind)
176                    )?;
177                }
178                for (changed, (old_kind, new_kind)) in &edge.kind_changed {
179                    writeln!(
180                        writer,
181                        "  ~ {} ({} -> {})",
182                        diff.display_name(changed),
183                        old_kind,
184                        new_kind
185                    )?;
186                }
187            }
188        }
189
190        if let Some(mc) = &diff.metadata_changed {
191            writeln!(writer)?;
192            write_text_metadata(writer, mc)?;
193        }
194
195        Ok(())
196    }
197}
198
199fn write_text_metadata<W: Write>(writer: &mut W, mc: &MetadataChange) -> std::io::Result<()> {
200    writeln!(writer, "[~] Metadata Changes")?;
201    writeln!(writer, "--------------------")?;
202    if let Some((ref old, ref new)) = mc.timestamp {
203        writeln!(
204            writer,
205            "  Timestamp: {} -> {}",
206            old.as_deref().unwrap_or("<none>"),
207            new.as_deref().unwrap_or("<none>")
208        )?;
209    }
210    if let Some((ref old, ref new)) = mc.tools {
211        writeln!(
212            writer,
213            "  Tools: {} -> {}",
214            format_vec_or_none(old),
215            format_vec_or_none(new)
216        )?;
217    }
218    if let Some((ref old, ref new)) = mc.authors {
219        writeln!(
220            writer,
221            "  Authors: {} -> {}",
222            format_vec_or_none(old),
223            format_vec_or_none(new)
224        )?;
225    }
226    Ok(())
227}
228
229impl SummaryFormatter for TextRenderer {
230    fn write_warnings<W: Write>(&self, w: &mut W, opts: &RenderOptions) -> std::io::Result<()> {
231        writeln!(w, "Warnings:     {}", opts.warning_count())?;
232        for warning in &opts.old_warnings {
233            writeln!(w, "  [old] {}", warning)?;
234        }
235        for warning in &opts.new_warnings {
236            writeln!(w, "  [new] {}", warning)?;
237        }
238        writeln!(w)
239    }
240
241    fn write_counts<W: Write>(&self, w: &mut W, diff: &Diff) -> std::io::Result<()> {
242        writeln!(w, "Old total:        {} components", diff.old_total)?;
243        writeln!(w, "New total:        {} components", diff.new_total)?;
244        writeln!(w, "Unchanged:        {}", diff.unchanged)?;
245        writeln!(w, "Added:            {}", diff.added.len())?;
246        writeln!(w, "Removed:          {}", diff.removed.len())?;
247        writeln!(w, "Changed:          {}", diff.changed.len())?;
248        writeln!(w, "Edge changes:     {}", diff.edge_diffs.len())?;
249        writeln!(
250            w,
251            "Metadata changed: {}",
252            if diff.metadata_changed.is_some() {
253                "yes"
254            } else {
255                "no"
256            }
257        )
258    }
259
260    fn write_ecosystem_breakdown<W: Write>(
261        &self,
262        w: &mut W,
263        breakdown: &BTreeMap<String, EcosystemCounts>,
264    ) -> std::io::Result<()> {
265        writeln!(w)?;
266        writeln!(w, "By ecosystem:")?;
267        for (eco, counts) in breakdown {
268            writeln!(
269                w,
270                "  {}: {} added, {} removed, {} changed",
271                eco, counts.added, counts.removed, counts.changed
272            )?;
273        }
274        Ok(())
275    }
276}
277
278impl SummaryRenderer for TextRenderer {
279    fn render_summary<W: Write>(
280        &self,
281        diff: &Diff,
282        opts: &RenderOptions,
283        writer: &mut W,
284    ) -> anyhow::Result<()> {
285        write_summary(self, diff, opts, writer)?;
286        Ok(())
287    }
288}