Skip to main content

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}