sbom_tools/serialization/emit/mod.rs
1//! Cross-format SBOM emission.
2//!
3//! Serializes a [`NormalizedSbom`] back out to a concrete SBOM wire format.
4//! Unlike [`super::enricher`]/[`super::pruner`], which patch the *original* raw
5//! JSON in place, this module *synthesizes* a fresh document from the canonical
6//! model — enabling cross-format conversion (e.g. SPDX → CycloneDX).
7//!
8//! # Lossiness
9//!
10//! Normalization into [`NormalizedSbom`] is one-way lossy: format-specific
11//! fields that don't map onto the canonical model are dropped during parsing.
12//! To bound that loss, callers may first run [`preserve_source_json`], which
13//! captures each component's verbatim source JSON into
14//! [`ComponentExtensions::source_json`](crate::model::ComponentExtensions::source_json).
15//! The emitter then splices same-format preserved fields back in and synthesizes
16//! the remainder from the typed model. Every synthesis/drop decision is recorded
17//! in a [`FidelityReport`] so the loss is reported honestly rather than hidden.
18
19mod cyclonedx;
20mod fidelity;
21mod preserve;
22mod spdx;
23
24pub use cyclonedx::emit_cyclonedx;
25pub use fidelity::FidelityReport;
26pub use preserve::preserve_source_json;
27pub use spdx::emit_spdx;
28
29/// Target format for [`emit`].
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum EmitTarget {
33 /// CycloneDX 1.7 JSON.
34 CycloneDx,
35 /// SPDX 2.3 JSON.
36 Spdx,
37}
38
39impl EmitTarget {
40 /// Parse a `--to` target value (case-insensitive).
41 ///
42 /// Accepts `cyclonedx`/`cdx` and `spdx`. Returns `None` for unknown values.
43 #[must_use]
44 pub fn parse(s: &str) -> Option<Self> {
45 match s.to_ascii_lowercase().as_str() {
46 "cyclonedx" | "cdx" | "cyclone-dx" => Some(Self::CycloneDx),
47 "spdx" => Some(Self::Spdx),
48 _ => None,
49 }
50 }
51}
52
53/// Errors that can occur while emitting an SBOM.
54#[derive(Debug, thiserror::Error)]
55pub enum EmitError {
56 /// The requested target format has no emitter yet.
57 #[error("emitting to {0} is not yet implemented")]
58 Unsupported(&'static str),
59
60 /// Serializing the synthesized document to JSON failed.
61 #[error("failed to serialize emitted SBOM: {0}")]
62 Serialize(#[from] serde_json::Error),
63}
64
65/// Emit `sbom` to the requested `target`, returning the serialized document and
66/// a [`FidelityReport`] describing what was synthesized or dropped.
67///
68/// # Errors
69///
70/// Returns [`EmitError::Serialize`] if JSON serialization fails.
71pub fn emit(
72 sbom: &crate::model::NormalizedSbom,
73 target: EmitTarget,
74) -> Result<(String, FidelityReport), EmitError> {
75 match target {
76 EmitTarget::CycloneDx => emit_cyclonedx(sbom),
77 EmitTarget::Spdx => emit_spdx(sbom),
78 }
79}