Skip to main content

provenant/license_detection/embedded/
index.rs

1use super::schema::{EmbeddedArtifactMetadata, EmbeddedLoaderSnapshot, SCHEMA_VERSION};
2use crate::license_detection::index::LicenseIndex;
3use crate::license_detection::index::build_index_from_loaded;
4
5#[derive(Debug, Clone)]
6#[allow(dead_code)]
7pub struct LoadedEmbeddedLicenseIndex {
8    pub index: LicenseIndex,
9    pub metadata: EmbeddedArtifactMetadata,
10}
11
12#[derive(Debug, Clone)]
13pub struct SerializationError(pub String);
14
15impl std::fmt::Display for SerializationError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        write!(f, "License loader artifact error: {}", self.0)
18    }
19}
20
21impl std::error::Error for SerializationError {}
22
23pub fn load_loader_snapshot_from_bytes(
24    bytes: &[u8],
25) -> Result<EmbeddedLoaderSnapshot, SerializationError> {
26    if bytes.is_empty() {
27        return Err(SerializationError(
28            "Embedded license index artifact is empty".to_string(),
29        ));
30    }
31
32    let decompressed = zstd::decode_all(bytes).map_err(|e| {
33        SerializationError(format!("Failed to decompress embedded artifact: {}", e))
34    })?;
35
36    let snapshot: EmbeddedLoaderSnapshot = rmp_serde::from_slice(&decompressed).map_err(|e| {
37        SerializationError(format!("Failed to deserialize embedded artifact: {}", e))
38    })?;
39
40    if snapshot.schema_version != SCHEMA_VERSION {
41        return Err(SerializationError(format!(
42            "Embedded artifact schema version mismatch: expected {}, got {}",
43            SCHEMA_VERSION, snapshot.schema_version
44        )));
45    }
46
47    Ok(snapshot)
48}
49
50#[allow(dead_code)]
51pub fn load_embedded_license_index_from_bytes(
52    bytes: &[u8],
53) -> Result<LoadedEmbeddedLicenseIndex, SerializationError> {
54    let snapshot = load_loader_snapshot_from_bytes(bytes)?;
55    let index = build_index_from_loaded(snapshot.rules, snapshot.licenses, false);
56
57    Ok(LoadedEmbeddedLicenseIndex {
58        index,
59        metadata: snapshot.metadata,
60    })
61}
62
63pub fn load_embedded_artifact_metadata_from_bytes(
64    bytes: &[u8],
65) -> Result<EmbeddedArtifactMetadata, SerializationError> {
66    Ok(load_loader_snapshot_from_bytes(bytes)?.metadata)
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::license_detection::models::{LoadedLicense, LoadedRule};
73
74    fn create_test_metadata() -> EmbeddedArtifactMetadata {
75        EmbeddedArtifactMetadata {
76            spdx_license_list_version: "3.27".to_string(),
77        }
78    }
79
80    fn serialize_loader_snapshot_to_bytes(
81        rules: Vec<LoadedRule>,
82        licenses: Vec<LoadedLicense>,
83    ) -> Result<Vec<u8>, SerializationError> {
84        let snapshot = EmbeddedLoaderSnapshot {
85            schema_version: SCHEMA_VERSION,
86            metadata: create_test_metadata(),
87            rules,
88            licenses,
89        };
90
91        let msgpack = rmp_serde::to_vec(&snapshot).map_err(|e| {
92            SerializationError(format!("Failed to serialize embedded artifact: {}", e))
93        })?;
94
95        zstd::encode_all(&msgpack[..], 0)
96            .map_err(|e| SerializationError(format!("Failed to compress embedded artifact: {}", e)))
97    }
98
99    fn create_test_loaded_rule() -> LoadedRule {
100        LoadedRule {
101            identifier: "test.RULE".to_string(),
102            license_expression: "mit".to_string(),
103            text: "MIT License text".to_string(),
104            rule_kind: crate::license_detection::models::RuleKind::Text,
105            is_false_positive: false,
106            is_required_phrase: false,
107            skip_for_required_phrase_generation: false,
108            relevance: Some(100),
109            minimum_coverage: None,
110            has_stored_minimum_coverage: false,
111            is_continuous: false,
112            referenced_filenames: None,
113            ignorable_urls: None,
114            ignorable_emails: None,
115            ignorable_copyrights: None,
116            ignorable_holders: None,
117            ignorable_authors: None,
118            language: None,
119            notes: None,
120            is_deprecated: false,
121            replaced_by: vec![],
122        }
123    }
124
125    fn create_test_loaded_license() -> LoadedLicense {
126        LoadedLicense {
127            key: "mit".to_string(),
128            short_name: Some("MIT".to_string()),
129            name: "MIT License".to_string(),
130            language: Some("en".to_string()),
131            spdx_license_key: Some("MIT".to_string()),
132            other_spdx_license_keys: vec![],
133            category: Some("Permissive".to_string()),
134            owner: None,
135            homepage_url: None,
136            text: "MIT License text".to_string(),
137            reference_urls: vec![],
138            osi_license_key: None,
139            text_urls: vec![],
140            osi_url: None,
141            faq_url: None,
142            other_urls: vec![],
143            notes: None,
144            is_deprecated: false,
145            is_exception: false,
146            is_unknown: false,
147            is_generic: false,
148            replaced_by: vec![],
149            minimum_coverage: None,
150            standard_notice: None,
151            ignorable_copyrights: None,
152            ignorable_holders: None,
153            ignorable_authors: None,
154            ignorable_urls: None,
155            ignorable_emails: None,
156        }
157    }
158
159    #[test]
160    fn test_load_license_index_from_bytes_roundtrip() {
161        let bytes = serialize_loader_snapshot_to_bytes(
162            vec![create_test_loaded_rule()],
163            vec![create_test_loaded_license()],
164        )
165        .expect("Should serialize");
166
167        let index = load_embedded_license_index_from_bytes(&bytes)
168            .expect("Should deserialize")
169            .index;
170
171        assert_eq!(index.licenses_by_key.len(), 1);
172        assert!(
173            index
174                .rules_by_rid
175                .iter()
176                .any(|rule| rule.identifier == "test.RULE"),
177            "runtime index should retain the serialized rule"
178        );
179        assert!(
180            index
181                .rules_by_rid
182                .iter()
183                .any(|rule| rule.identifier == "mit.LICENSE"),
184            "runtime index should synthesize a license-derived rule"
185        );
186    }
187
188    #[test]
189    fn test_load_embedded_artifact_metadata_from_bytes_roundtrip() {
190        let bytes = serialize_loader_snapshot_to_bytes(
191            vec![create_test_loaded_rule()],
192            vec![create_test_loaded_license()],
193        )
194        .expect("Should serialize");
195
196        let metadata = load_embedded_artifact_metadata_from_bytes(&bytes)
197            .expect("Should deserialize metadata");
198
199        assert_eq!(metadata.spdx_license_list_version, "3.27");
200    }
201
202    #[test]
203    fn test_load_license_index_from_bytes_rejects_empty() {
204        let error = load_embedded_license_index_from_bytes(&[]).unwrap_err();
205        assert!(error.to_string().contains("artifact is empty"));
206    }
207}