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