pub mod diff;
pub mod spdx2;
pub mod spdx3;
use anyhow::{Context, Result};
use serde::Serialize;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize)]
pub struct SbomPackage {
pub _spdx_id: String,
pub name: String,
pub version: Option<String>,
pub purl: Option<String>,
}
pub fn parse_spdx_sbom(data: &[u8]) -> Result<Vec<SbomPackage>> {
let doc: serde_json::Value =
serde_json::from_slice(data).context("Failed to parse SBOM as JSON")?;
if let Some(spec_version) = doc.get("specVersion").and_then(|v| v.as_str()) {
if spec_version.starts_with("3.") {
info!("Detected SPDX 3.0 document (specVersion: {})", spec_version);
return spdx3::parse_spdx3(data);
}
}
if let Some(spdx_version) = doc.get("spdxVersion").and_then(|v| v.as_str()) {
if spdx_version.starts_with("SPDX-3") {
info!("Detected SPDX 3.0 document (spdxVersion: {})", spdx_version);
return spdx3::parse_spdx3(data);
}
}
if let Some(spdx_version) = doc.get("spdxVersion").and_then(|v| v.as_str()) {
if spdx_version.starts_with("SPDX-2") {
debug!("Detected SPDX 2.x document (spdxVersion: {})", spdx_version);
return spdx2::parse_spdx2(data);
}
}
warn!("Could not detect SPDX version from document, trying SPDX 2.x parser");
spdx2::parse_spdx2(data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_spdx23() {
let json = r#"{
"SPDXID": "SPDXRef-DOCUMENT",
"spdxVersion": "SPDX-2.3",
"packages": [{"SPDXID": "SPDXRef-1", "name": "test", "versionInfo": "1.0"}]
}"#;
let pkgs = parse_spdx_sbom(json.as_bytes()).unwrap();
assert_eq!(pkgs.len(), 1);
assert_eq!(pkgs[0].name, "test");
}
#[test]
fn test_detect_spdx30() {
let json = r#"{
"type": "SpdxDocument",
"spdxId": "SPDXRef-DOCUMENT",
"specVersion": "3.0.1",
"element": [{"type": "Package", "spdxId": "SPDXRef-1", "name": "test", "packageVersion": "1.0"}]
}"#;
let pkgs = parse_spdx_sbom(json.as_bytes()).unwrap();
assert_eq!(pkgs.len(), 1);
assert_eq!(pkgs[0].name, "test");
}
#[test]
fn test_detect_spdx30_via_spdxversion() {
let json = r#"{
"SPDXID": "SPDXRef-DOCUMENT",
"spdxVersion": "SPDX-3.0",
"element": [{"type": "Package", "spdxId": "SPDXRef-1", "name": "test", "packageVersion": "1.0"}]
}"#;
let pkgs = parse_spdx_sbom(json.as_bytes()).unwrap();
assert_eq!(pkgs.len(), 1);
}
#[test]
fn test_fallback_to_spdx2() {
let json = r#"{
"SPDXID": "SPDXRef-DOCUMENT",
"packages": [{"SPDXID": "SPDXRef-1", "name": "test", "versionInfo": "1.0"}]
}"#;
let pkgs = parse_spdx_sbom(json.as_bytes()).unwrap();
assert_eq!(pkgs.len(), 1);
}
}