Skip to main content

provenant/license_detection/embedded/
index.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use super::schema::{EmbeddedArtifactMetadata, EmbeddedLoaderSnapshot, SCHEMA_VERSION};
5use crate::license_detection::index::LicenseIndex;
6use crate::license_detection::index::build_index_from_loaded;
7
8// Loader handle: fields are consumed by maintainer/index-build paths, not every routine-scan path.
9#[derive(Debug, Clone)]
10#[allow(dead_code)]
11pub struct LoadedEmbeddedLicenseIndex {
12    pub index: LicenseIndex,
13    pub metadata: EmbeddedArtifactMetadata,
14}
15
16#[derive(Debug, Clone, thiserror::Error)]
17#[error("License loader artifact error: {0}")]
18pub struct SerializationError(pub String);
19
20pub fn load_loader_snapshot_from_bytes(
21    bytes: &[u8],
22) -> Result<EmbeddedLoaderSnapshot, SerializationError> {
23    if bytes.is_empty() {
24        return Err(SerializationError(
25            "Embedded license index artifact is empty".to_string(),
26        ));
27    }
28
29    let decompressed = zstd::decode_all(bytes).map_err(|e| {
30        SerializationError(format!("Failed to decompress embedded artifact: {}", e))
31    })?;
32
33    let snapshot: EmbeddedLoaderSnapshot = postcard::from_bytes(&decompressed).map_err(|e| {
34        SerializationError(format!("Failed to deserialize embedded artifact: {}", e))
35    })?;
36
37    if snapshot.schema_version != SCHEMA_VERSION {
38        return Err(SerializationError(format!(
39            "Embedded artifact schema version mismatch: expected {}, got {}",
40            SCHEMA_VERSION, snapshot.schema_version
41        )));
42    }
43
44    Ok(snapshot)
45}
46
47// Used by maintainer/index-build entry points; not every binary links this path.
48#[allow(dead_code)]
49pub fn load_embedded_license_index_from_bytes(
50    bytes: &[u8],
51) -> Result<LoadedEmbeddedLicenseIndex, SerializationError> {
52    let snapshot = load_loader_snapshot_from_bytes(bytes)?;
53    let index = build_index_from_loaded(snapshot.rules, snapshot.licenses, false);
54
55    Ok(LoadedEmbeddedLicenseIndex {
56        index,
57        metadata: snapshot.metadata,
58    })
59}
60
61pub fn load_embedded_artifact_metadata_from_bytes(
62    bytes: &[u8],
63) -> Result<EmbeddedArtifactMetadata, SerializationError> {
64    Ok(load_loader_snapshot_from_bytes(bytes)?.metadata)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::license_detection::models::{LoadedLicense, LoadedRule};
71
72    fn create_test_metadata() -> EmbeddedArtifactMetadata {
73        EmbeddedArtifactMetadata {
74            spdx_license_list_version: "3.27".to_string(),
75            license_index_provenance: crate::models::LicenseIndexProvenance {
76                source: "embedded-artifact".to_string(),
77                dataset_fingerprint: "test".to_string(),
78                ignored_rules: vec![],
79                ignored_licenses: vec![],
80                ignored_rules_due_to_licenses: vec![],
81                added_rules: vec![],
82                replaced_rules: vec![],
83                added_licenses: vec![],
84                replaced_licenses: vec![],
85            },
86        }
87    }
88
89    fn serialize_loader_snapshot_to_bytes(
90        rules: Vec<LoadedRule>,
91        licenses: Vec<LoadedLicense>,
92    ) -> Result<Vec<u8>, SerializationError> {
93        let snapshot = EmbeddedLoaderSnapshot {
94            schema_version: SCHEMA_VERSION,
95            metadata: create_test_metadata(),
96            rules,
97            licenses,
98        };
99
100        let postcard_bytes = postcard::to_allocvec(&snapshot).map_err(|e| {
101            SerializationError(format!("Failed to serialize embedded artifact: {}", e))
102        })?;
103
104        zstd::encode_all(&postcard_bytes[..], 0)
105            .map_err(|e| SerializationError(format!("Failed to compress embedded artifact: {}", e)))
106    }
107
108    fn create_test_loaded_rule() -> LoadedRule {
109        LoadedRule {
110            identifier: "test.RULE".to_string(),
111            license_expression: "mit".to_string(),
112            text: "MIT License text".to_string(),
113            rule_kind: crate::license_detection::models::RuleKind::Text,
114            is_false_positive: false,
115            is_required_phrase: false,
116            skip_for_required_phrase_generation: false,
117            relevance: Some(100),
118            minimum_coverage: None,
119            has_stored_minimum_coverage: false,
120            is_continuous: false,
121            referenced_filenames: None,
122            ignorable_urls: None,
123            ignorable_emails: None,
124            ignorable_copyrights: None,
125            ignorable_holders: None,
126            ignorable_authors: None,
127            language: None,
128            notes: None,
129            is_deprecated: false,
130            replaced_by: vec![],
131        }
132    }
133
134    fn create_test_loaded_license() -> LoadedLicense {
135        LoadedLicense {
136            key: "mit".to_string(),
137            short_name: Some("MIT".to_string()),
138            name: "MIT License".to_string(),
139            language: Some("en".to_string()),
140            spdx_license_key: Some("MIT".to_string()),
141            other_spdx_license_keys: vec![],
142            category: Some("Permissive".to_string()),
143            owner: None,
144            homepage_url: None,
145            text: "MIT License text".to_string(),
146            reference_urls: vec![],
147            osi_license_key: None,
148            text_urls: vec![],
149            osi_url: None,
150            faq_url: None,
151            other_urls: vec![],
152            notes: None,
153            is_deprecated: false,
154            is_exception: false,
155            is_unknown: false,
156            is_generic: false,
157            replaced_by: vec![],
158            minimum_coverage: None,
159            standard_notice: None,
160            ignorable_copyrights: None,
161            ignorable_holders: None,
162            ignorable_authors: None,
163            ignorable_urls: None,
164            ignorable_emails: None,
165        }
166    }
167
168    #[test]
169    fn test_load_license_index_from_bytes_roundtrip() {
170        let bytes = serialize_loader_snapshot_to_bytes(
171            vec![create_test_loaded_rule()],
172            vec![create_test_loaded_license()],
173        )
174        .expect("Should serialize");
175
176        let index = load_embedded_license_index_from_bytes(&bytes)
177            .expect("Should deserialize")
178            .index;
179
180        assert_eq!(index.licenses_by_key.len(), 1);
181        assert!(
182            index
183                .rules_by_rid
184                .iter()
185                .any(|rule| rule.identifier == "test.RULE"),
186            "runtime index should retain the serialized rule"
187        );
188        assert!(
189            index
190                .rules_by_rid
191                .iter()
192                .any(|rule| rule.identifier == "mit.LICENSE"),
193            "runtime index should synthesize a license-derived rule"
194        );
195    }
196
197    #[test]
198    fn test_load_embedded_artifact_metadata_from_bytes_roundtrip() {
199        let bytes = serialize_loader_snapshot_to_bytes(
200            vec![create_test_loaded_rule()],
201            vec![create_test_loaded_license()],
202        )
203        .expect("Should serialize");
204
205        let metadata = load_embedded_artifact_metadata_from_bytes(&bytes)
206            .expect("Should deserialize metadata");
207
208        assert_eq!(metadata.spdx_license_list_version, "3.27");
209        assert_eq!(
210            metadata.license_index_provenance.source,
211            "embedded-artifact"
212        );
213    }
214
215    #[test]
216    fn test_load_license_index_from_bytes_rejects_empty() {
217        let error = load_embedded_license_index_from_bytes(&[]).unwrap_err();
218        assert!(error.to_string().contains("artifact is empty"));
219    }
220}