Skip to main content

provenant/license_detection/embedded/
index.rs

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