provenant/license_detection/embedded/
index.rs1use std::io::Read;
5
6use serde::Deserialize;
7
8use super::schema::{EmbeddedArtifactMetadata, EmbeddedLoaderSnapshot, SCHEMA_VERSION};
9use crate::license_detection::index::LicenseIndex;
10use crate::license_detection::index::build_index_from_loaded;
11
12#[derive(Debug, Clone, Deserialize)]
22struct EmbeddedArtifactMetadataPrefix {
23 schema_version: u32,
24 metadata: EmbeddedArtifactMetadata,
25}
26
27const METADATA_PREFIX_MAX_BYTES: usize = 64 * 1024;
32
33const METADATA_PREFIX_CHUNK_BYTES: usize = 4 * 1024;
35
36#[derive(Debug, Clone)]
38#[allow(dead_code)]
39pub struct LoadedEmbeddedLicenseIndex {
40 pub index: LicenseIndex,
41 pub metadata: EmbeddedArtifactMetadata,
42}
43
44#[derive(Debug, Clone, thiserror::Error)]
45#[error("License loader artifact error: {0}")]
46pub struct SerializationError(pub String);
47
48pub fn load_loader_snapshot_from_bytes(
49 bytes: &[u8],
50) -> Result<EmbeddedLoaderSnapshot, SerializationError> {
51 if bytes.is_empty() {
52 return Err(SerializationError(
53 "Embedded license index artifact is empty".to_string(),
54 ));
55 }
56
57 let decompressed = zstd::decode_all(bytes).map_err(|e| {
58 SerializationError(format!("Failed to decompress embedded artifact: {}", e))
59 })?;
60
61 let snapshot: EmbeddedLoaderSnapshot = postcard::from_bytes(&decompressed).map_err(|e| {
62 SerializationError(format!("Failed to deserialize embedded artifact: {}", e))
63 })?;
64
65 if snapshot.schema_version != SCHEMA_VERSION {
66 return Err(SerializationError(format!(
67 "Embedded artifact schema version mismatch: expected {}, got {}",
68 SCHEMA_VERSION, snapshot.schema_version
69 )));
70 }
71
72 Ok(snapshot)
73}
74
75#[allow(dead_code)]
77pub fn load_embedded_license_index_from_bytes(
78 bytes: &[u8],
79) -> Result<LoadedEmbeddedLicenseIndex, SerializationError> {
80 let snapshot = load_loader_snapshot_from_bytes(bytes)?;
81 let index = build_index_from_loaded(snapshot.rules, snapshot.licenses, false);
82
83 Ok(LoadedEmbeddedLicenseIndex {
84 index,
85 metadata: snapshot.metadata,
86 })
87}
88
89pub fn load_embedded_artifact_metadata_from_bytes(
98 bytes: &[u8],
99) -> Result<EmbeddedArtifactMetadata, SerializationError> {
100 if bytes.is_empty() {
101 return Err(SerializationError(
102 "Embedded license index artifact is empty".to_string(),
103 ));
104 }
105
106 let mut decoder =
107 zstd::stream::read::Decoder::new(std::io::Cursor::new(bytes)).map_err(|e| {
108 SerializationError(format!("Failed to decompress embedded artifact: {}", e))
109 })?;
110
111 let mut prefix: Vec<u8> = Vec::with_capacity(METADATA_PREFIX_CHUNK_BYTES);
116 loop {
117 match postcard::take_from_bytes::<EmbeddedArtifactMetadataPrefix>(&prefix) {
118 Ok((decoded, _rest)) => {
119 if decoded.schema_version != SCHEMA_VERSION {
120 return Err(SerializationError(format!(
121 "Embedded artifact schema version mismatch: expected {}, got {}",
122 SCHEMA_VERSION, decoded.schema_version
123 )));
124 }
125 return Ok(decoded.metadata);
126 }
127 Err(postcard::Error::DeserializeUnexpectedEnd) => {}
128 Err(e) => {
129 return Err(SerializationError(format!(
130 "Failed to deserialize embedded artifact metadata: {}",
131 e
132 )));
133 }
134 }
135
136 if prefix.len() >= METADATA_PREFIX_MAX_BYTES {
137 return Err(SerializationError(format!(
138 "Embedded artifact metadata exceeds {} byte prefix budget",
139 METADATA_PREFIX_MAX_BYTES
140 )));
141 }
142
143 let mut chunk = [0u8; METADATA_PREFIX_CHUNK_BYTES];
144 let read = decoder.read(&mut chunk).map_err(|e| {
145 SerializationError(format!("Failed to decompress embedded artifact: {}", e))
146 })?;
147 if read == 0 {
148 return Err(SerializationError(
150 "Failed to deserialize embedded artifact metadata: artifact ended before metadata"
151 .to_string(),
152 ));
153 }
154 prefix.extend_from_slice(&chunk[..read]);
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::license_detection::models::{LoadedLicense, LoadedRule};
162
163 fn create_test_metadata() -> EmbeddedArtifactMetadata {
164 EmbeddedArtifactMetadata {
165 spdx_license_list_version: "3.27".to_string(),
166 license_index_provenance: crate::models::LicenseIndexProvenance {
167 source: "embedded-artifact".to_string(),
168 dataset_fingerprint: "test".to_string(),
169 ignored_rules: vec![],
170 ignored_licenses: vec![],
171 ignored_rules_due_to_licenses: vec![],
172 added_rules: vec![],
173 replaced_rules: vec![],
174 added_licenses: vec![],
175 replaced_licenses: vec![],
176 },
177 }
178 }
179
180 fn serialize_loader_snapshot_to_bytes(
181 rules: Vec<LoadedRule>,
182 licenses: Vec<LoadedLicense>,
183 ) -> Result<Vec<u8>, SerializationError> {
184 let snapshot = EmbeddedLoaderSnapshot {
185 schema_version: SCHEMA_VERSION,
186 metadata: create_test_metadata(),
187 rules,
188 licenses,
189 };
190
191 let postcard_bytes = postcard::to_allocvec(&snapshot).map_err(|e| {
192 SerializationError(format!("Failed to serialize embedded artifact: {}", e))
193 })?;
194
195 zstd::encode_all(&postcard_bytes[..], 0)
196 .map_err(|e| SerializationError(format!("Failed to compress embedded artifact: {}", e)))
197 }
198
199 fn create_test_loaded_rule() -> LoadedRule {
200 LoadedRule {
201 identifier: "test.RULE".to_string(),
202 license_expression: "mit".to_string(),
203 text: "MIT License text".to_string(),
204 rule_kind: crate::license_detection::models::RuleKind::Text,
205 is_false_positive: false,
206 is_required_phrase: false,
207 skip_for_required_phrase_generation: false,
208 relevance: Some(100),
209 minimum_coverage: None,
210 has_stored_minimum_coverage: false,
211 is_continuous: false,
212 referenced_filenames: None,
213 ignorable_urls: None,
214 ignorable_emails: None,
215 ignorable_copyrights: None,
216 ignorable_holders: None,
217 ignorable_authors: None,
218 language: None,
219 notes: None,
220 is_deprecated: false,
221 replaced_by: vec![],
222 }
223 }
224
225 fn create_test_loaded_license() -> LoadedLicense {
226 LoadedLicense {
227 key: "mit".to_string(),
228 short_name: Some("MIT".to_string()),
229 name: "MIT License".to_string(),
230 language: Some("en".to_string()),
231 spdx_license_key: Some("MIT".to_string()),
232 other_spdx_license_keys: vec![],
233 category: Some("Permissive".to_string()),
234 owner: None,
235 homepage_url: None,
236 text: "MIT License text".to_string(),
237 reference_urls: vec![],
238 osi_license_key: None,
239 text_urls: vec![],
240 osi_url: None,
241 faq_url: None,
242 other_urls: vec![],
243 notes: None,
244 is_deprecated: false,
245 is_exception: false,
246 is_unknown: false,
247 is_generic: false,
248 replaced_by: vec![],
249 minimum_coverage: None,
250 standard_notice: None,
251 ignorable_copyrights: None,
252 ignorable_holders: None,
253 ignorable_authors: None,
254 ignorable_urls: None,
255 ignorable_emails: None,
256 }
257 }
258
259 #[test]
260 fn test_load_license_index_from_bytes_roundtrip() {
261 let bytes = serialize_loader_snapshot_to_bytes(
262 vec![create_test_loaded_rule()],
263 vec![create_test_loaded_license()],
264 )
265 .expect("Should serialize");
266
267 let index = load_embedded_license_index_from_bytes(&bytes)
268 .expect("Should deserialize")
269 .index;
270
271 assert_eq!(index.licenses_by_key.len(), 1);
272 assert!(
273 index
274 .rules_by_rid
275 .iter()
276 .any(|rule| rule.identifier == "test.RULE"),
277 "runtime index should retain the serialized rule"
278 );
279 assert!(
280 index
281 .rules_by_rid
282 .iter()
283 .any(|rule| rule.identifier == "mit.LICENSE"),
284 "runtime index should synthesize a license-derived rule"
285 );
286 }
287
288 #[test]
289 fn test_load_embedded_artifact_metadata_from_bytes_roundtrip() {
290 let bytes = serialize_loader_snapshot_to_bytes(
291 vec![create_test_loaded_rule()],
292 vec![create_test_loaded_license()],
293 )
294 .expect("Should serialize");
295
296 let metadata = load_embedded_artifact_metadata_from_bytes(&bytes)
297 .expect("Should deserialize metadata");
298
299 assert_eq!(metadata.spdx_license_list_version, "3.27");
300 assert_eq!(
301 metadata.license_index_provenance.source,
302 "embedded-artifact"
303 );
304 }
305
306 #[test]
307 fn test_load_license_index_from_bytes_rejects_empty() {
308 let error = load_embedded_license_index_from_bytes(&[]).unwrap_err();
309 assert!(error.to_string().contains("artifact is empty"));
310 }
311
312 #[test]
313 fn test_metadata_prefix_matches_full_decode() {
314 let rules: Vec<LoadedRule> = (0..256).map(|_| create_test_loaded_rule()).collect();
317 let licenses: Vec<LoadedLicense> = (0..256).map(|_| create_test_loaded_license()).collect();
318 let bytes = serialize_loader_snapshot_to_bytes(rules, licenses).expect("Should serialize");
319
320 let prefix_metadata =
321 load_embedded_artifact_metadata_from_bytes(&bytes).expect("prefix read should succeed");
322 let full_metadata = load_loader_snapshot_from_bytes(&bytes)
323 .expect("full decode should succeed")
324 .metadata;
325
326 assert_eq!(prefix_metadata, full_metadata);
327 }
328
329 #[test]
330 fn test_metadata_prefix_rejects_empty() {
331 let error = load_embedded_artifact_metadata_from_bytes(&[]).unwrap_err();
332 assert!(error.to_string().contains("artifact is empty"));
333 }
334
335 #[test]
336 fn test_metadata_prefix_rejects_schema_version_mismatch() {
337 let snapshot = EmbeddedLoaderSnapshot {
338 schema_version: SCHEMA_VERSION + 1,
339 metadata: create_test_metadata(),
340 rules: vec![create_test_loaded_rule()],
341 licenses: vec![create_test_loaded_license()],
342 };
343 let postcard_bytes = postcard::to_allocvec(&snapshot).expect("Should serialize");
344 let bytes = zstd::encode_all(&postcard_bytes[..], 0).expect("Should compress");
345
346 let error = load_embedded_artifact_metadata_from_bytes(&bytes).unwrap_err();
347 assert!(
348 error.to_string().contains("schema version mismatch"),
349 "expected schema mismatch, got: {error}"
350 );
351 }
352
353 #[test]
354 fn test_metadata_prefix_rejects_garbage() {
355 let bytes =
356 zstd::encode_all(&b"not a valid postcard snapshot"[..], 0).expect("Should compress");
357 let error = load_embedded_artifact_metadata_from_bytes(&bytes).unwrap_err();
358 assert!(
359 error.to_string().contains("deserialize"),
360 "expected deserialize failure, got: {error}"
361 );
362 }
363}