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
17pub 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}