1use super::{ReportConfig, ReportError, ReportFormat, ReportType, WriterReporter};
33use crate::diff::DiffResult;
34use crate::model::NormalizedSbom;
35use chrono::Utc;
36use serde::Serialize;
37use std::io::Write;
38
39pub struct StreamingJsonWriter<'w, W: Write> {
48 writer: &'w mut W,
49 pretty: bool,
50 indent_level: usize,
51 flush_interval: usize,
52 items_written: usize,
53}
54
55impl<'w, W: Write> StreamingJsonWriter<'w, W> {
56 pub fn new(writer: &'w mut W, pretty: bool) -> Self {
58 Self {
59 writer,
60 pretty,
61 indent_level: 0,
62 flush_interval: 100, items_written: 0,
64 }
65 }
66
67 pub fn with_flush_interval(mut self, interval: usize) -> Self {
69 self.flush_interval = interval.max(1);
70 self
71 }
72
73 fn write_object_start(&mut self) -> Result<(), ReportError> {
75 self.write_raw("{")?;
76 self.indent_level += 1;
77 Ok(())
78 }
79
80 fn write_object_end(&mut self) -> Result<(), ReportError> {
82 self.indent_level = self.indent_level.saturating_sub(1);
83 self.write_newline()?;
84 self.write_indent()?;
85 self.write_raw("}")?;
86 Ok(())
87 }
88
89 fn write_array_start(&mut self) -> Result<(), ReportError> {
91 self.write_raw("[")?;
92 self.indent_level += 1;
93 Ok(())
94 }
95
96 fn write_array_end(&mut self) -> Result<(), ReportError> {
98 self.indent_level = self.indent_level.saturating_sub(1);
99 self.write_newline()?;
100 self.write_indent()?;
101 self.write_raw("]")?;
102 Ok(())
103 }
104
105 fn write_key_value<V: Serialize>(
107 &mut self,
108 key: &str,
109 value: &V,
110 trailing_comma: bool,
111 ) -> Result<(), ReportError> {
112 self.write_newline()?;
113 self.write_indent()?;
114 self.write_raw(&format!("\"{}\":", key))?;
115 if self.pretty {
116 self.write_raw(" ")?;
117 }
118
119 let json = if self.pretty {
120 serde_json::to_string_pretty(value)
121 } else {
122 serde_json::to_string(value)
123 }
124 .map_err(|e| ReportError::SerializationError(e.to_string()))?;
125
126 if self.pretty && json.contains('\n') {
128 let indented = self.indent_multiline(&json);
129 self.write_raw(&indented)?;
130 } else {
131 self.write_raw(&json)?;
132 }
133
134 if trailing_comma {
135 self.write_raw(",")?;
136 }
137
138 Ok(())
139 }
140
141 fn write_key_array_start(&mut self, key: &str) -> Result<(), ReportError> {
143 self.write_newline()?;
144 self.write_indent()?;
145 self.write_raw(&format!("\"{}\":", key))?;
146 if self.pretty {
147 self.write_raw(" ")?;
148 }
149 self.write_array_start()?;
150 Ok(())
151 }
152
153 fn write_array_item<V: Serialize>(
155 &mut self,
156 value: &V,
157 trailing_comma: bool,
158 ) -> Result<(), ReportError> {
159 self.write_newline()?;
160 self.write_indent()?;
161
162 let json = if self.pretty {
163 serde_json::to_string_pretty(value)
164 } else {
165 serde_json::to_string(value)
166 }
167 .map_err(|e| ReportError::SerializationError(e.to_string()))?;
168
169 if self.pretty && json.contains('\n') {
170 let indented = self.indent_multiline(&json);
171 self.write_raw(&indented)?;
172 } else {
173 self.write_raw(&json)?;
174 }
175
176 if trailing_comma {
177 self.write_raw(",")?;
178 }
179
180 self.items_written += 1;
181 if self.items_written.is_multiple_of(self.flush_interval) {
182 self.writer.flush()?;
183 }
184
185 Ok(())
186 }
187
188 fn write_raw(&mut self, s: &str) -> Result<(), ReportError> {
190 self.writer.write_all(s.as_bytes())?;
191 Ok(())
192 }
193
194 fn write_newline(&mut self) -> Result<(), ReportError> {
196 if self.pretty {
197 self.write_raw("\n")?;
198 }
199 Ok(())
200 }
201
202 fn write_indent(&mut self) -> Result<(), ReportError> {
204 if self.pretty {
205 let indent = " ".repeat(self.indent_level);
206 self.write_raw(&indent)?;
207 }
208 Ok(())
209 }
210
211 fn indent_multiline(&self, json: &str) -> String {
213 let base_indent = " ".repeat(self.indent_level);
214 let lines: Vec<&str> = json.lines().collect();
215 if lines.len() <= 1 {
216 return json.to_string();
217 }
218
219 let mut result = String::new();
220 result.push_str(lines[0]);
221 for line in &lines[1..] {
222 result.push('\n');
223 result.push_str(&base_indent);
224 result.push_str(line);
225 }
226 result
227 }
228
229 pub fn write_diff_report(
231 mut self,
232 result: &DiffResult,
233 old_sbom: &NormalizedSbom,
234 new_sbom: &NormalizedSbom,
235 config: &ReportConfig,
236 ) -> Result<(), ReportError> {
237 self.write_object_start()?;
238
239 let metadata = StreamingMetadata {
241 tool: ToolInfo {
242 name: "sbom-tools".to_string(),
243 version: env!("CARGO_PKG_VERSION").to_string(),
244 },
245 generated_at: Utc::now().to_rfc3339(),
246 old_sbom: SbomInfo {
247 format: old_sbom.document.format.to_string(),
248 file_path: config.metadata.old_sbom_path.clone(),
249 component_count: old_sbom.component_count(),
250 },
251 new_sbom: Some(SbomInfo {
252 format: new_sbom.document.format.to_string(),
253 file_path: config.metadata.new_sbom_path.clone(),
254 component_count: new_sbom.component_count(),
255 }),
256 };
257 self.write_key_value("metadata", &metadata, true)?;
258
259 let summary = StreamingSummary {
261 total_changes: result.summary.total_changes,
262 components_added: result.summary.components_added,
263 components_removed: result.summary.components_removed,
264 components_modified: result.summary.components_modified,
265 vulnerabilities_introduced: result.summary.vulnerabilities_introduced,
266 vulnerabilities_resolved: result.summary.vulnerabilities_resolved,
267 semantic_score: result.semantic_score,
268 };
269 self.write_key_value("summary", &summary, true)?;
270
271 if config.includes(ReportType::Components) {
273 self.write_key_array_start("components_added")?;
274 let added_len = result.components.added.len();
275 for (i, comp) in result.components.added.iter().enumerate() {
276 self.write_array_item(comp, i + 1 < added_len)?;
277 }
278 self.write_array_end()?;
279 self.write_raw(",")?;
280
281 self.write_key_array_start("components_removed")?;
282 let removed_len = result.components.removed.len();
283 for (i, comp) in result.components.removed.iter().enumerate() {
284 self.write_array_item(comp, i + 1 < removed_len)?;
285 }
286 self.write_array_end()?;
287 self.write_raw(",")?;
288
289 self.write_key_array_start("components_modified")?;
290 let modified_len = result.components.modified.len();
291 for (i, comp) in result.components.modified.iter().enumerate() {
292 self.write_array_item(comp, i + 1 < modified_len)?;
293 }
294 self.write_array_end()?;
295
296 let has_more = config.includes(ReportType::Vulnerabilities)
298 || config.includes(ReportType::Dependencies)
299 || config.includes(ReportType::Licenses);
300 if has_more {
301 self.write_raw(",")?;
302 }
303 }
304
305 if config.includes(ReportType::Vulnerabilities) {
307 self.write_key_array_start("vulnerabilities_introduced")?;
308 let introduced_len = result.vulnerabilities.introduced.len();
309 for (i, vuln) in result.vulnerabilities.introduced.iter().enumerate() {
310 self.write_array_item(vuln, i + 1 < introduced_len)?;
311 }
312 self.write_array_end()?;
313 self.write_raw(",")?;
314
315 self.write_key_array_start("vulnerabilities_resolved")?;
316 let resolved_len = result.vulnerabilities.resolved.len();
317 for (i, vuln) in result.vulnerabilities.resolved.iter().enumerate() {
318 self.write_array_item(vuln, i + 1 < resolved_len)?;
319 }
320 self.write_array_end()?;
321
322 let has_more =
323 config.includes(ReportType::Dependencies) || config.includes(ReportType::Licenses);
324 if has_more {
325 self.write_raw(",")?;
326 }
327 }
328
329 if config.includes(ReportType::Dependencies) {
331 self.write_key_array_start("dependencies_added")?;
332 let added_len = result.dependencies.added.len();
333 for (i, dep) in result.dependencies.added.iter().enumerate() {
334 self.write_array_item(dep, i + 1 < added_len)?;
335 }
336 self.write_array_end()?;
337 self.write_raw(",")?;
338
339 self.write_key_array_start("dependencies_removed")?;
340 let removed_len = result.dependencies.removed.len();
341 for (i, dep) in result.dependencies.removed.iter().enumerate() {
342 self.write_array_item(dep, i + 1 < removed_len)?;
343 }
344 self.write_array_end()?;
345
346 if config.includes(ReportType::Licenses) {
347 self.write_raw(",")?;
348 }
349 }
350
351 if config.includes(ReportType::Licenses) {
353 self.write_key_array_start("licenses_new")?;
354 let new_len = result.licenses.new_licenses.len();
355 for (i, lic) in result.licenses.new_licenses.iter().enumerate() {
356 self.write_array_item(lic, i + 1 < new_len)?;
357 }
358 self.write_array_end()?;
359 self.write_raw(",")?;
360
361 self.write_key_array_start("licenses_removed")?;
362 let removed_len = result.licenses.removed_licenses.len();
363 for (i, lic) in result.licenses.removed_licenses.iter().enumerate() {
364 self.write_array_item(lic, i + 1 < removed_len)?;
365 }
366 self.write_array_end()?;
367 }
368
369 self.write_object_end()?;
370 self.write_newline()?;
371 self.writer.flush()?;
372
373 Ok(())
374 }
375}
376
377pub struct NdjsonWriter<'w, W: Write> {
387 writer: &'w mut W,
388 flush_interval: usize,
389 items_written: usize,
390}
391
392impl<'w, W: Write> NdjsonWriter<'w, W> {
393 pub fn new(writer: &'w mut W) -> Self {
395 Self {
396 writer,
397 flush_interval: 100,
398 items_written: 0,
399 }
400 }
401
402 pub fn with_flush_interval(mut self, interval: usize) -> Self {
404 self.flush_interval = interval.max(1);
405 self
406 }
407
408 pub fn write_item<T: Serialize>(&mut self, item: &T) -> Result<(), ReportError> {
410 let json = serde_json::to_string(item)
411 .map_err(|e| ReportError::SerializationError(e.to_string()))?;
412 self.writer.write_all(json.as_bytes())?;
413 self.writer.write_all(b"\n")?;
414
415 self.items_written += 1;
416 if self.items_written.is_multiple_of(self.flush_interval) {
417 self.writer.flush()?;
418 }
419
420 Ok(())
421 }
422
423 pub fn write_tagged<T: Serialize>(&mut self, tag: &str, item: &T) -> Result<(), ReportError> {
425 #[derive(Serialize)]
426 struct Tagged<'a, T> {
427 #[serde(rename = "type")]
428 type_: &'a str,
429 data: &'a T,
430 }
431
432 let tagged = Tagged {
433 type_: tag,
434 data: item,
435 };
436 self.write_item(&tagged)
437 }
438
439 pub fn write_diff_components(&mut self, result: &DiffResult) -> Result<(), ReportError> {
441 for comp in &result.components.added {
442 self.write_tagged("component_added", comp)?;
443 }
444 for comp in &result.components.removed {
445 self.write_tagged("component_removed", comp)?;
446 }
447 for comp in &result.components.modified {
448 self.write_tagged("component_modified", comp)?;
449 }
450 self.writer.flush()?;
451 Ok(())
452 }
453
454 pub fn write_diff_vulnerabilities(&mut self, result: &DiffResult) -> Result<(), ReportError> {
456 for vuln in &result.vulnerabilities.introduced {
457 self.write_tagged("vulnerability_introduced", vuln)?;
458 }
459 for vuln in &result.vulnerabilities.resolved {
460 self.write_tagged("vulnerability_resolved", vuln)?;
461 }
462 for vuln in &result.vulnerabilities.persistent {
463 self.write_tagged("vulnerability_persistent", vuln)?;
464 }
465 self.writer.flush()?;
466 Ok(())
467 }
468
469 pub fn write_diff_report(
473 &mut self,
474 result: &DiffResult,
475 old_sbom: &NormalizedSbom,
476 new_sbom: &NormalizedSbom,
477 config: &ReportConfig,
478 ) -> Result<(), ReportError> {
479 let metadata = NdjsonMetadata {
481 type_: "metadata",
482 tool: "sbom-tools",
483 version: env!("CARGO_PKG_VERSION"),
484 generated_at: Utc::now().to_rfc3339(),
485 old_sbom_format: old_sbom.document.format.to_string(),
486 new_sbom_format: new_sbom.document.format.to_string(),
487 old_component_count: old_sbom.component_count(),
488 new_component_count: new_sbom.component_count(),
489 };
490 self.write_item(&metadata)?;
491
492 let summary = NdjsonSummary {
494 type_: "summary",
495 total_changes: result.summary.total_changes,
496 components_added: result.summary.components_added,
497 components_removed: result.summary.components_removed,
498 components_modified: result.summary.components_modified,
499 vulnerabilities_introduced: result.summary.vulnerabilities_introduced,
500 vulnerabilities_resolved: result.summary.vulnerabilities_resolved,
501 semantic_score: result.semantic_score,
502 };
503 self.write_item(&summary)?;
504
505 if config.includes(ReportType::Components) {
507 self.write_diff_components(result)?;
508 }
509
510 if config.includes(ReportType::Vulnerabilities) {
512 self.write_diff_vulnerabilities(result)?;
513 }
514
515 if config.includes(ReportType::Dependencies) {
517 for dep in &result.dependencies.added {
518 self.write_tagged("dependency_added", dep)?;
519 }
520 for dep in &result.dependencies.removed {
521 self.write_tagged("dependency_removed", dep)?;
522 }
523 }
524
525 if config.includes(ReportType::Licenses) {
527 for lic in &result.licenses.new_licenses {
528 self.write_tagged("license_new", lic)?;
529 }
530 for lic in &result.licenses.removed_licenses {
531 self.write_tagged("license_removed", lic)?;
532 }
533 }
534
535 self.writer.flush()?;
536 Ok(())
537 }
538
539 pub fn items_written(&self) -> usize {
541 self.items_written
542 }
543}
544
545#[derive(Default)]
554pub struct StreamingJsonReporter {
555 pretty: bool,
556}
557
558impl StreamingJsonReporter {
559 pub fn new() -> Self {
561 Self { pretty: true }
562 }
563
564 pub fn compact() -> Self {
566 Self { pretty: false }
567 }
568}
569
570impl WriterReporter for StreamingJsonReporter {
571 fn write_diff_to<W: Write>(
572 &self,
573 result: &DiffResult,
574 old_sbom: &NormalizedSbom,
575 new_sbom: &NormalizedSbom,
576 config: &ReportConfig,
577 writer: &mut W,
578 ) -> Result<(), ReportError> {
579 let streaming = StreamingJsonWriter::new(writer, self.pretty);
580 streaming.write_diff_report(result, old_sbom, new_sbom, config)
581 }
582
583 fn write_view_to<W: Write>(
584 &self,
585 sbom: &NormalizedSbom,
586 config: &ReportConfig,
587 writer: &mut W,
588 ) -> Result<(), ReportError> {
589 use super::JsonReporter;
592 use super::ReportGenerator;
593
594 let reporter = JsonReporter::new().pretty(self.pretty);
595 let report = reporter.generate_view_report(sbom, config)?;
596 writer.write_all(report.as_bytes())?;
597 Ok(())
598 }
599
600 fn format(&self) -> ReportFormat {
601 ReportFormat::Json
602 }
603}
604
605#[derive(Default)]
607pub struct NdjsonReporter;
608
609impl NdjsonReporter {
610 pub fn new() -> Self {
612 Self
613 }
614}
615
616impl WriterReporter for NdjsonReporter {
617 fn write_diff_to<W: Write>(
618 &self,
619 result: &DiffResult,
620 old_sbom: &NormalizedSbom,
621 new_sbom: &NormalizedSbom,
622 config: &ReportConfig,
623 writer: &mut W,
624 ) -> Result<(), ReportError> {
625 let mut ndjson = NdjsonWriter::new(writer);
626 ndjson.write_diff_report(result, old_sbom, new_sbom, config)
627 }
628
629 fn write_view_to<W: Write>(
630 &self,
631 sbom: &NormalizedSbom,
632 _config: &ReportConfig,
633 writer: &mut W,
634 ) -> Result<(), ReportError> {
635 let mut ndjson = NdjsonWriter::new(writer);
636
637 #[derive(Serialize)]
639 struct ViewMetadata<'a> {
640 #[serde(rename = "type")]
641 type_: &'a str,
642 format: String,
643 component_count: usize,
644 }
645
646 let metadata = ViewMetadata {
647 type_: "metadata",
648 format: sbom.document.format.to_string(),
649 component_count: sbom.component_count(),
650 };
651 ndjson.write_item(&metadata)?;
652
653 for (_, comp) in &sbom.components {
655 #[derive(Serialize)]
656 struct ComponentLine<'a> {
657 #[serde(rename = "type")]
658 type_: &'a str,
659 name: &'a str,
660 version: Option<&'a str>,
661 ecosystem: Option<String>,
662 }
663
664 let line = ComponentLine {
665 type_: "component",
666 name: &comp.name,
667 version: comp.version.as_deref(),
668 ecosystem: comp.ecosystem.as_ref().map(|e| e.to_string()),
669 };
670 ndjson.write_item(&line)?;
671 }
672
673 Ok(())
674 }
675
676 fn format(&self) -> ReportFormat {
677 ReportFormat::Json }
679}
680
681#[derive(Serialize)]
686struct ToolInfo {
687 name: String,
688 version: String,
689}
690
691#[derive(Serialize)]
692struct SbomInfo {
693 format: String,
694 #[serde(skip_serializing_if = "Option::is_none")]
695 file_path: Option<String>,
696 component_count: usize,
697}
698
699#[derive(Serialize)]
700struct StreamingMetadata {
701 tool: ToolInfo,
702 generated_at: String,
703 old_sbom: SbomInfo,
704 #[serde(skip_serializing_if = "Option::is_none")]
705 new_sbom: Option<SbomInfo>,
706}
707
708#[derive(Serialize)]
709struct StreamingSummary {
710 total_changes: usize,
711 components_added: usize,
712 components_removed: usize,
713 components_modified: usize,
714 vulnerabilities_introduced: usize,
715 vulnerabilities_resolved: usize,
716 semantic_score: f64,
717}
718
719#[derive(Serialize)]
720struct NdjsonMetadata<'a> {
721 #[serde(rename = "type")]
722 type_: &'a str,
723 tool: &'a str,
724 version: &'a str,
725 generated_at: String,
726 old_sbom_format: String,
727 new_sbom_format: String,
728 old_component_count: usize,
729 new_component_count: usize,
730}
731
732#[derive(Serialize)]
733struct NdjsonSummary<'a> {
734 #[serde(rename = "type")]
735 type_: &'a str,
736 total_changes: usize,
737 components_added: usize,
738 components_removed: usize,
739 components_modified: usize,
740 vulnerabilities_introduced: usize,
741 vulnerabilities_resolved: usize,
742 semantic_score: f64,
743}
744
745#[cfg(test)]
750mod tests {
751 use super::*;
752
753 #[test]
754 fn test_ndjson_writer_item() {
755 let mut buffer = Vec::new();
756 let mut writer = NdjsonWriter::new(&mut buffer);
757
758 #[derive(Serialize)]
759 struct TestItem {
760 name: String,
761 value: i32,
762 }
763
764 let item = TestItem {
765 name: "test".to_string(),
766 value: 42,
767 };
768 writer.write_item(&item).unwrap();
769
770 let output = String::from_utf8(buffer).unwrap();
771 assert!(output.contains("\"name\":\"test\""));
772 assert!(output.contains("\"value\":42"));
773 assert!(output.ends_with('\n'));
774 }
775
776 #[test]
777 fn test_ndjson_writer_tagged() {
778 let mut buffer = Vec::new();
779 let mut writer = NdjsonWriter::new(&mut buffer);
780
781 writer.write_tagged("test_type", &42).unwrap();
782
783 let output = String::from_utf8(buffer).unwrap();
784 assert!(output.contains("\"type\":\"test_type\""));
785 assert!(output.contains("\"data\":42"));
786 }
787
788 #[test]
789 fn test_streaming_json_reporter_implements_writer_reporter() {
790 let reporter = StreamingJsonReporter::new();
791 assert_eq!(WriterReporter::format(&reporter), ReportFormat::Json);
793 }
794
795 #[test]
796 fn test_ndjson_reporter_implements_writer_reporter() {
797 let reporter = NdjsonReporter::new();
798 assert_eq!(WriterReporter::format(&reporter), ReportFormat::Json);
799 }
800
801 #[test]
802 fn test_ndjson_writer_items_counted() {
803 let mut buffer = Vec::new();
804 let mut writer = NdjsonWriter::new(&mut buffer);
805
806 writer.write_item(&1).unwrap();
807 writer.write_item(&2).unwrap();
808 writer.write_item(&3).unwrap();
809
810 assert_eq!(writer.items_written(), 3);
811 }
812}