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