sbom_tools/parsers/
mod.rs1mod cyclonedx;
30mod detection;
31mod spdx;
32pub mod streaming;
33mod traits;
34
35pub use cyclonedx::CycloneDxParser;
36pub use detection::{DetectionResult, FormatDetector, MIN_CONFIDENCE_THRESHOLD, ParserKind};
37pub use spdx::SpdxParser;
38pub use streaming::{ParseEvent, ParseProgress, StreamingConfig, StreamingParser};
39pub use traits::{FormatConfidence, FormatDetection, ParseError, SbomParser};
40
41use crate::model::NormalizedSbom;
42use std::path::Path;
43
44#[derive(Debug, Clone)]
46pub struct DetectedFormat {
47 pub format_name: String,
49 pub confidence: f32,
51 pub variant: Option<String>,
53 pub version: Option<String>,
55 pub warnings: Vec<String>,
57}
58
59#[must_use]
63pub fn detect_format(content: &str) -> Option<DetectedFormat> {
64 let detector = FormatDetector::new();
65 let result = detector.detect_from_content(content);
66
67 if result.can_parse() {
68 Some(DetectedFormat {
69 format_name: result
70 .parser
71 .map(|p| p.name().to_string())
72 .unwrap_or_default(),
73 confidence: result.confidence.value(),
74 variant: result.variant,
75 version: result.version,
76 warnings: result.warnings,
77 })
78 } else {
79 None
80 }
81}
82
83const MAX_SBOM_FILE_SIZE: u64 = 512 * 1024 * 1024;
85
86pub fn parse_sbom(path: &Path) -> Result<NormalizedSbom, ParseError> {
92 let metadata = std::fs::metadata(path).map_err(|e| ParseError::IoError(e.to_string()))?;
93 if metadata.len() > MAX_SBOM_FILE_SIZE {
94 return Err(ParseError::IoError(format!(
95 "SBOM file is {} MB, exceeding the {} MB limit. Use the streaming parser for large files.",
96 metadata.len() / (1024 * 1024),
97 MAX_SBOM_FILE_SIZE / (1024 * 1024),
98 )));
99 }
100 let content = std::fs::read_to_string(path).map_err(|e| ParseError::IoError(e.to_string()))?;
101 parse_sbom_str(&content)
102}
103
104pub fn parse_sbom_str(content: &str) -> Result<NormalizedSbom, ParseError> {
108 let detector = FormatDetector::new();
109 detector.parse_str(content)
110}
111
112#[deprecated(
116 since = "0.2.0",
117 note = "Use detect_format() or CycloneDxParser::detect() instead"
118)]
119#[must_use]
120pub fn is_cyclonedx(content: &str) -> bool {
121 CycloneDxParser::new().can_parse(content)
122}
123
124#[deprecated(
126 since = "0.2.0",
127 note = "Use detect_format() or SpdxParser::detect() instead"
128)]
129#[must_use]
130pub fn is_spdx(content: &str) -> bool {
131 SpdxParser::new().can_parse(content)
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_detect_cyclonedx_json() {
140 let content = r#"{"bomFormat": "CycloneDX", "specVersion": "1.5"}"#;
141 let detected = detect_format(content).expect("Should detect format");
142 assert_eq!(detected.format_name, "CycloneDX");
143 assert!(detected.confidence >= 0.75);
144 assert_eq!(detected.variant, Some("JSON".to_string()));
145 assert_eq!(detected.version, Some("1.5".to_string()));
146 }
147
148 #[test]
149 fn test_detect_spdx_json() {
150 let content = r#"{"spdxVersion": "SPDX-2.3", "SPDXID": "SPDXRef-DOCUMENT"}"#;
151 let detected = detect_format(content).expect("Should detect format");
152 assert_eq!(detected.format_name, "SPDX");
153 assert!(detected.confidence >= 0.75);
154 assert_eq!(detected.variant, Some("JSON".to_string()));
155 assert_eq!(detected.version, Some("2.3".to_string()));
156 }
157
158 #[test]
159 fn test_detect_spdx_tag_value() {
160 let content = "SPDXVersion: SPDX-2.3\nDataLicense: CC0-1.0\nSPDXID: SPDXRef-DOCUMENT";
161 let detected = detect_format(content).expect("Should detect format");
162 assert_eq!(detected.format_name, "SPDX");
163 assert!(detected.confidence >= 0.75);
164 assert_eq!(detected.variant, Some("tag-value".to_string()));
165 assert_eq!(detected.version, Some("2.3".to_string()));
166 }
167
168 #[test]
169 fn test_detect_unknown_format() {
170 let content = r#"{"some": "random", "json": "content"}"#;
171 let detected = detect_format(content);
172 assert!(detected.is_none());
173 }
174
175 #[test]
176 fn test_confidence_based_selection() {
177 let cdx_content = r#"{"bomFormat": "CycloneDX", "specVersion": "1.6", "components": []}"#;
179 let cdx_parser = CycloneDxParser::new();
180 let spdx_parser = SpdxParser::new();
181
182 let cdx_conf = cdx_parser.confidence(cdx_content);
183 let spdx_conf = spdx_parser.confidence(cdx_content);
184
185 assert!(cdx_conf.value() > spdx_conf.value());
186 }
187}