provenant/license_detection/embedded/
index.rs1use 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}