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            relevance: Some(100),
76            minimum_coverage: None,
77            has_stored_minimum_coverage: false,
78            is_continuous: false,
79            referenced_filenames: None,
80            ignorable_urls: None,
81            ignorable_emails: None,
82            ignorable_copyrights: None,
83            ignorable_holders: None,
84            ignorable_authors: None,
85            language: None,
86            notes: None,
87            is_deprecated: false,
88        }
89    }
90
91    fn create_test_loaded_license() -> LoadedLicense {
92        LoadedLicense {
93            key: "mit".to_string(),
94            short_name: Some("MIT".to_string()),
95            name: "MIT License".to_string(),
96            language: Some("en".to_string()),
97            spdx_license_key: Some("MIT".to_string()),
98            other_spdx_license_keys: vec![],
99            category: Some("Permissive".to_string()),
100            owner: None,
101            homepage_url: None,
102            text: "MIT License text".to_string(),
103            reference_urls: vec![],
104            osi_license_key: None,
105            text_urls: vec![],
106            osi_url: None,
107            faq_url: None,
108            other_urls: vec![],
109            notes: None,
110            is_deprecated: false,
111            is_exception: false,
112            is_unknown: false,
113            is_generic: false,
114            replaced_by: vec![],
115            minimum_coverage: None,
116            standard_notice: None,
117            ignorable_copyrights: None,
118            ignorable_holders: None,
119            ignorable_authors: None,
120            ignorable_urls: None,
121            ignorable_emails: None,
122        }
123    }
124
125    #[test]
126    fn test_load_license_index_from_bytes_roundtrip() {
127        let bytes = serialize_loader_snapshot_to_bytes(
128            vec![create_test_loaded_rule()],
129            vec![create_test_loaded_license()],
130        )
131        .expect("Should serialize");
132
133        let index = load_license_index_from_bytes(&bytes).expect("Should deserialize");
134
135        assert_eq!(index.licenses_by_key.len(), 1);
136        assert!(
137            index
138                .rules_by_rid
139                .iter()
140                .any(|rule| rule.identifier == "test.RULE"),
141            "runtime index should retain the serialized rule"
142        );
143        assert!(
144            index
145                .rules_by_rid
146                .iter()
147                .any(|rule| rule.identifier == "mit.LICENSE"),
148            "runtime index should synthesize a license-derived rule"
149        );
150    }
151
152    #[test]
153    fn test_load_license_index_from_bytes_rejects_empty() {
154        let error = load_license_index_from_bytes(&[]).unwrap_err();
155        assert!(error.to_string().contains("artifact is empty"));
156    }
157}