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<()> {
13 for c in components {
14 writeln!(writer, "{}", c.purl.as_deref().unwrap_or(c.id.as_str()))?;
15 }
16 Ok(())
17}
18
19pub 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}