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 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}