Skip to main content

provenant/license_detection/embedded/
index.rs

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