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