1use super::{
2 format_option, format_set, format_vec_or_none, RenderOptions, Renderer, SummaryRenderer,
3};
4use crate::{Diff, FieldChange};
5use sbom_model::is_hash_algorithm_downgrade;
6use std::io::Write;
7
8fn csv_writer<W: Write>(writer: W) -> csv::Writer<W> {
11 csv::WriterBuilder::new()
12 .terminator(csv::Terminator::Any(b'\n'))
13 .from_writer(writer)
14}
15
16fn write_warning_rows<W: Write>(wtr: &mut csv::Writer<W>, opts: &RenderOptions) -> csv::Result<()> {
19 for w in &opts.old_warnings {
20 wtr.write_record(["warning", "old", "", "", w, ""])?;
21 }
22 for w in &opts.new_warnings {
23 wtr.write_record(["warning", "new", "", "", w, ""])?;
24 }
25 Ok(())
26}
27
28pub struct CsvRenderer;
35
36impl Renderer for CsvRenderer {
37 fn render<W: Write>(
38 &self,
39 diff: &Diff,
40 opts: &RenderOptions,
41 writer: &mut W,
42 ) -> anyhow::Result<()> {
43 let mut wtr = csv_writer(&mut *writer);
44
45 wtr.write_record([
46 "status",
47 "component",
48 "ecosystem",
49 "field",
50 "old_value",
51 "new_value",
52 ])?;
53
54 if opts.has_warnings() {
55 write_warning_rows(&mut wtr, opts)?;
56 }
57
58 for comp in &diff.added {
59 let display = comp.purl.as_deref().unwrap_or(comp.id.as_str());
60 let eco = comp.ecosystem.as_deref().unwrap_or("");
61 let ver = comp.version.as_deref().unwrap_or("");
62 wtr.write_record(["added", display, eco, "version", "", ver])?;
63 }
64
65 for comp in &diff.removed {
66 let display = comp.purl.as_deref().unwrap_or(comp.id.as_str());
67 let eco = comp.ecosystem.as_deref().unwrap_or("");
68 let ver = comp.version.as_deref().unwrap_or("");
69 wtr.write_record(["removed", display, eco, "version", ver, ""])?;
70 }
71
72 for change in &diff.changed {
73 let display = change.new.purl.as_deref().unwrap_or(change.id.as_str());
74 let eco = change.new.ecosystem.as_deref().unwrap_or("");
75 for fc in &change.changes {
76 let (field, old, new) = csv_field_change(fc, change.is_downgrade);
77 wtr.write_record(["changed", display, eco, field, &old, &new])?;
78 }
79 }
80
81 for edge in &diff.edge_diffs {
82 let parent = diff.display_name(&edge.parent);
83 for (child, kind) in &edge.added {
84 let child_name = diff.display_name(child);
85 wtr.write_record(["edge-added", parent, "", child_name, "", &kind.to_string()])?;
86 }
87 for (child, kind) in &edge.removed {
88 let child_name = diff.display_name(child);
89 wtr.write_record([
90 "edge-removed",
91 parent,
92 "",
93 child_name,
94 &kind.to_string(),
95 "",
96 ])?;
97 }
98 for (child, (old_kind, new_kind)) in &edge.kind_changed {
99 let child_name = diff.display_name(child);
100 wtr.write_record([
101 "edge-kind-changed",
102 parent,
103 "",
104 child_name,
105 &old_kind.to_string(),
106 &new_kind.to_string(),
107 ])?;
108 }
109 }
110
111 if let Some(mc) = &diff.metadata_changed {
112 if let Some((ref old, ref new)) = mc.timestamp {
113 wtr.write_record([
114 "metadata",
115 "",
116 "",
117 "timestamp",
118 old.as_deref().unwrap_or(""),
119 new.as_deref().unwrap_or(""),
120 ])?;
121 }
122 if let Some((ref old, ref new)) = mc.tools {
123 wtr.write_record([
124 "metadata",
125 "",
126 "",
127 "tools",
128 &format_vec_or_none(old),
129 &format_vec_or_none(new),
130 ])?;
131 }
132 if let Some((ref old, ref new)) = mc.authors {
133 wtr.write_record([
134 "metadata",
135 "",
136 "",
137 "authors",
138 &format_vec_or_none(old),
139 &format_vec_or_none(new),
140 ])?;
141 }
142 }
143
144 wtr.flush()?;
145 Ok(())
146 }
147}
148
149impl SummaryRenderer for CsvRenderer {
150 fn render_summary<W: Write>(
151 &self,
152 diff: &Diff,
153 opts: &RenderOptions,
154 writer: &mut W,
155 ) -> anyhow::Result<()> {
156 let meta_changed = if diff.metadata_changed.is_some() {
157 "1"
158 } else {
159 "0"
160 };
161
162 if opts.has_warnings() {
166 let mut wtr = csv_writer(&mut *writer);
167 wtr.write_record([
168 "status",
169 "component",
170 "ecosystem",
171 "field",
172 "old_value",
173 "new_value",
174 ])?;
175 write_warning_rows(&mut wtr, opts)?;
176 wtr.flush()?;
177 drop(wtr);
178 writeln!(writer)?;
179 }
180
181 let mut wtr = csv_writer(&mut *writer);
182 wtr.write_record(["metric", "count"])?;
183 wtr.write_record(["old_total", &diff.old_total.to_string()])?;
184 wtr.write_record(["new_total", &diff.new_total.to_string()])?;
185 wtr.write_record(["unchanged", &diff.unchanged.to_string()])?;
186 wtr.write_record(["added", &diff.added.len().to_string()])?;
187 wtr.write_record(["removed", &diff.removed.len().to_string()])?;
188 wtr.write_record(["changed", &diff.changed.len().to_string()])?;
189 wtr.write_record(["edge_changes", &diff.edge_diffs.len().to_string()])?;
190 wtr.write_record(["metadata_changed", meta_changed])?;
191
192 wtr.flush()?;
193 drop(wtr);
194
195 if opts.group_by_ecosystem {
196 let breakdown = diff.ecosystem_breakdown();
197 if !breakdown.is_empty() {
198 writeln!(writer)?;
199 let mut wtr = csv_writer(&mut *writer);
200 wtr.write_record(["ecosystem", "added", "removed", "changed"])?;
201 for (eco, counts) in &breakdown {
202 wtr.write_record([
203 eco.as_str(),
204 &counts.added.to_string(),
205 &counts.removed.to_string(),
206 &counts.changed.to_string(),
207 ])?;
208 }
209 wtr.flush()?;
210 }
211 }
212
213 Ok(())
214 }
215}
216
217fn csv_field_change(fc: &FieldChange, is_downgrade: bool) -> (&'static str, String, String) {
219 match fc {
220 FieldChange::Version(old, new) => (
221 if is_downgrade {
222 "version-downgrade"
223 } else {
224 "version"
225 },
226 format_option(old).to_string(),
227 format_option(new).to_string(),
228 ),
229 FieldChange::License(old, new) => ("license", format_set(old), format_set(new)),
230 FieldChange::Supplier(old, new) => (
231 "supplier",
232 format_option(old).to_string(),
233 format_option(new).to_string(),
234 ),
235 FieldChange::Purl(old, new) => (
236 "purl",
237 format_option(old).to_string(),
238 format_option(new).to_string(),
239 ),
240 FieldChange::Description(old, new) => (
241 "description",
242 format_option(old).to_string(),
243 format_option(new).to_string(),
244 ),
245 FieldChange::Hashes(old, new) => {
246 let old_str = old
247 .iter()
248 .map(|(k, v)| format!("{}={}", k, v))
249 .collect::<Vec<_>>()
250 .join("; ");
251 let new_str = new
252 .iter()
253 .map(|(k, v)| format!("{}={}", k, v))
254 .collect::<Vec<_>>()
255 .join("; ");
256 let field = if is_hash_algorithm_downgrade(old, new) {
257 "hashes-downgrade"
258 } else {
259 "hashes"
260 };
261 (field, old_str, new_str)
262 }
263 FieldChange::Ecosystem(old, new) => (
264 "ecosystem",
265 format_option(old).to_string(),
266 format_option(new).to_string(),
267 ),
268 }
269}