Skip to main content

provenant/models/
file_info.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use derive_builder::Builder;
5use packageurl::PackageUrl;
6use serde::{Deserialize, Serialize};
7use sha1::{Digest, Sha1};
8use std::collections::HashMap;
9use std::fmt;
10use std::str::FromStr;
11
12use super::DatasourceId;
13use super::DependencyUid;
14use super::DiagnosticSeverity;
15use super::GitSha1;
16use super::LineNumber;
17use super::MatchScore;
18use super::Md5Digest;
19use super::PackageType;
20use super::PackageUid;
21use super::ScanDiagnostic;
22use super::Sha1Digest;
23use super::Sha256Digest;
24use super::Sha512Digest;
25
26use crate::license_detection::MatcherKind;
27use crate::license_detection::tokenize::tokenize_without_stopwords;
28use crate::models::output::Tallies;
29use crate::utils::spdx::combine_license_expressions;
30
31#[derive(Debug, Builder, Serialize, Deserialize, Clone)]
32#[builder(build_fn(skip))]
33/// File-level scan result containing metadata and detected findings.
34pub struct FileInfo {
35    pub name: String,
36    pub base_name: String,
37    pub extension: String,
38    pub path: String,
39    pub file_type: FileType,
40    #[builder(default)]
41    #[serde(default)]
42    pub mime_type: Option<String>,
43    #[builder(default)]
44    #[serde(default)]
45    pub file_type_label: Option<String>,
46    pub size: u64,
47    #[builder(default)]
48    #[serde(default)]
49    pub date: Option<String>,
50    #[builder(default)]
51    #[serde(default)]
52    pub sha1: Option<Sha1Digest>,
53    #[builder(default)]
54    #[serde(default)]
55    pub md5: Option<Md5Digest>,
56    #[builder(default)]
57    #[serde(default)]
58    pub sha256: Option<Sha256Digest>,
59    #[builder(default)]
60    #[serde(default)]
61    pub sha1_git: Option<GitSha1>,
62    #[builder(default)]
63    #[serde(default)]
64    pub programming_language: Option<String>,
65    #[builder(default)]
66    #[serde(default)]
67    pub package_data: Vec<PackageData>,
68    #[builder(default)]
69    #[serde(default)]
70    pub detected_license_expression: Option<String>,
71    #[builder(default)]
72    #[serde(default)]
73    pub license_detections: Vec<LicenseDetection>,
74    #[builder(default)]
75    #[serde(default)]
76    pub license_clues: Vec<Match>,
77    #[builder(default)]
78    #[serde(default)]
79    pub percentage_of_license_text: Option<f64>,
80    #[builder(default)]
81    #[serde(default)]
82    pub copyrights: Vec<Copyright>,
83    #[builder(default)]
84    #[serde(default)]
85    pub holders: Vec<Holder>,
86    #[builder(default)]
87    #[serde(default)]
88    pub authors: Vec<Author>,
89    #[builder(default)]
90    #[serde(default)]
91    pub emails: Vec<OutputEmail>,
92    #[builder(default)]
93    #[serde(default)]
94    pub urls: Vec<OutputURL>,
95    #[builder(default)]
96    #[serde(default)]
97    pub for_packages: Vec<PackageUid>,
98    #[builder(default)]
99    #[serde(default)]
100    pub scan_diagnostics: Vec<ScanDiagnostic>,
101    #[builder(default)]
102    #[serde(default)]
103    pub license_policy: Option<Vec<LicensePolicyEntry>>,
104    #[builder(default)]
105    #[serde(default)]
106    pub is_generated: Option<bool>,
107    #[builder(default)]
108    #[serde(default)]
109    pub is_binary: Option<bool>,
110    #[builder(default)]
111    #[serde(default)]
112    pub is_text: Option<bool>,
113    #[builder(default)]
114    #[serde(default)]
115    pub is_archive: Option<bool>,
116    #[builder(default)]
117    #[serde(default)]
118    pub is_media: Option<bool>,
119    #[builder(default)]
120    #[serde(default)]
121    pub is_source: Option<bool>,
122    #[builder(default)]
123    #[serde(default)]
124    pub is_script: Option<bool>,
125    #[builder(default)]
126    #[serde(default)]
127    pub files_count: Option<usize>,
128    #[builder(default)]
129    #[serde(default)]
130    pub dirs_count: Option<usize>,
131    #[builder(default)]
132    #[serde(default)]
133    pub size_count: Option<u64>,
134    #[builder(default)]
135    #[serde(default)]
136    pub source_count: Option<usize>,
137    #[builder(default)]
138    #[serde(default)]
139    pub is_legal: bool,
140    #[builder(default)]
141    #[serde(default)]
142    pub is_manifest: bool,
143    #[builder(default)]
144    #[serde(default)]
145    pub is_readme: bool,
146    #[builder(default)]
147    #[serde(default)]
148    pub is_top_level: bool,
149    #[builder(default)]
150    #[serde(default)]
151    pub is_key_file: bool,
152    #[builder(default)]
153    #[serde(default)]
154    pub is_referenced: bool,
155    #[builder(default)]
156    #[serde(default)]
157    pub is_community: bool,
158    #[builder(default)]
159    #[serde(default)]
160    pub facets: Vec<String>,
161    #[builder(default)]
162    #[serde(default)]
163    pub tallies: Option<Tallies>,
164}
165
166impl FileInfoBuilder {
167    /// Build a [`FileInfo`] from the current builder state.
168    pub fn build(&self) -> Result<FileInfo, String> {
169        let mut file_info = FileInfo::new(
170            self.name.clone().ok_or("Missing field: name")?,
171            self.base_name.clone().ok_or("Missing field: base_name")?,
172            self.extension.clone().ok_or("Missing field: extension")?,
173            self.path.clone().ok_or("Missing field: path")?,
174            self.file_type.clone().ok_or("Missing field: file_type")?,
175            self.mime_type.clone().flatten(),
176            self.file_type_label.clone().flatten(),
177            self.size.ok_or("Missing field: size")?,
178            self.date.clone().flatten(),
179            self.sha1.flatten(),
180            self.md5.flatten(),
181            self.sha256.flatten(),
182            self.programming_language.clone().flatten(),
183            self.package_data.clone().unwrap_or_default(),
184            self.detected_license_expression.clone().flatten(),
185            self.license_detections.clone().unwrap_or_default(),
186            self.license_clues.clone().unwrap_or_default(),
187            self.copyrights.clone().unwrap_or_default(),
188            self.holders.clone().unwrap_or_default(),
189            self.authors.clone().unwrap_or_default(),
190            self.emails.clone().unwrap_or_default(),
191            self.urls.clone().unwrap_or_default(),
192            self.for_packages.clone().unwrap_or_default(),
193            self.scan_diagnostics.clone().unwrap_or_default(),
194        );
195        file_info.license_policy = self.license_policy.clone().flatten();
196        file_info.sha1_git = self.sha1_git.flatten();
197        file_info.is_binary = self.is_binary.flatten();
198        file_info.is_text = self.is_text.flatten();
199        file_info.is_archive = self.is_archive.flatten();
200        file_info.is_media = self.is_media.flatten();
201        file_info.is_script = self.is_script.flatten();
202        file_info.files_count = self.files_count.flatten();
203        file_info.dirs_count = self.dirs_count.flatten();
204        file_info.size_count = self.size_count.flatten();
205        file_info.is_referenced = self.is_referenced.unwrap_or_default();
206        Ok(file_info)
207    }
208}
209
210impl FileInfo {
211    #[allow(clippy::too_many_arguments)]
212    /// Construct a [`FileInfo`] from fully resolved scanner fields.
213    pub fn new(
214        name: String,
215        base_name: String,
216        extension: String,
217        path: String,
218        file_type: FileType,
219        mime_type: Option<String>,
220        file_type_label: Option<String>,
221        size: u64,
222        date: Option<String>,
223        sha1: Option<Sha1Digest>,
224        md5: Option<Md5Digest>,
225        sha256: Option<Sha256Digest>,
226        programming_language: Option<String>,
227        package_data: Vec<PackageData>,
228        mut detected_license_expression: Option<String>,
229        mut license_detections: Vec<LicenseDetection>,
230        license_clues: Vec<Match>,
231        copyrights: Vec<Copyright>,
232        holders: Vec<Holder>,
233        authors: Vec<Author>,
234        emails: Vec<OutputEmail>,
235        urls: Vec<OutputURL>,
236        for_packages: Vec<PackageUid>,
237        scan_diagnostics: Vec<ScanDiagnostic>,
238    ) -> Self {
239        let mut package_data = package_data;
240        for package in &mut package_data {
241            enrich_package_data_license_provenance(package, &path);
242        }
243
244        // Combine license expressions from package data if detected_license_expression is None
245        detected_license_expression = detected_license_expression.or_else(|| {
246            let expressions = package_data
247                .iter()
248                .filter_map(|pkg| pkg.get_license_expression());
249            combine_license_expressions(expressions)
250        });
251
252        // Combine license detections from package data if none are provided
253        if license_detections.is_empty() {
254            for pkg in &package_data {
255                license_detections.extend(pkg.license_detections.clone());
256            }
257        }
258
259        // Combine license expressions from license detections if detected_license_expression is still None
260        if detected_license_expression.is_none() && !license_detections.is_empty() {
261            let expressions = license_detections
262                .iter()
263                .map(|detection| detection.license_expression.clone());
264            let expressions: Vec<String> = expressions.collect();
265            detected_license_expression = crate::utils::spdx::select_primary_license_expression(
266                expressions.clone(),
267            )
268            .or_else(|| {
269                crate::utils::spdx::combine_license_expressions_preserving_structure(expressions)
270            });
271        }
272
273        let mut file_info = FileInfo {
274            name,
275            base_name,
276            extension,
277            path,
278            file_type,
279            mime_type,
280            file_type_label,
281            size,
282            date,
283            sha1,
284            md5,
285            sha256,
286            sha1_git: None,
287            programming_language,
288            package_data,
289            detected_license_expression,
290            license_detections,
291            license_clues,
292            percentage_of_license_text: None,
293            copyrights,
294            holders,
295            authors,
296            emails,
297            urls,
298            for_packages,
299            scan_diagnostics,
300            license_policy: None,
301            is_generated: None,
302            is_binary: None,
303            is_text: None,
304            is_archive: None,
305            is_media: None,
306            is_source: None,
307            is_script: None,
308            files_count: None,
309            dirs_count: None,
310            size_count: None,
311            source_count: None,
312            is_legal: false,
313            is_manifest: false,
314            is_readme: false,
315            is_top_level: false,
316            is_key_file: false,
317            is_referenced: false,
318            is_community: false,
319            facets: vec![],
320            tallies: None,
321        };
322
323        file_info.backfill_license_provenance();
324        file_info
325    }
326
327    pub fn backfill_license_provenance(&mut self) {
328        for detection in &mut self.license_detections {
329            enrich_license_detection_provenance(detection, &self.path);
330        }
331
332        for package in &mut self.package_data {
333            enrich_package_data_license_provenance(package, &self.path);
334        }
335    }
336}
337
338impl FileInfo {
339    pub fn warning_diagnostics(&self) -> impl Iterator<Item = &ScanDiagnostic> {
340        self.scan_diagnostics
341            .iter()
342            .filter(|diagnostic| diagnostic.severity == DiagnosticSeverity::Warning)
343    }
344
345    pub fn error_diagnostics(&self) -> impl Iterator<Item = &ScanDiagnostic> {
346        self.scan_diagnostics.iter().filter(|diagnostic| {
347            diagnostic.severity == DiagnosticSeverity::Error
348                || diagnostic.severity == DiagnosticSeverity::Timeout
349        })
350    }
351}
352
353fn enrich_package_data_license_provenance(package_data: &mut PackageData, path: &str) {
354    for detection in &mut package_data.license_detections {
355        enrich_license_detection_provenance(detection, path);
356    }
357    for detection in &mut package_data.other_license_detections {
358        enrich_license_detection_provenance(detection, path);
359    }
360}
361
362pub(crate) fn enrich_license_detection_provenance(detection: &mut LicenseDetection, path: &str) {
363    for detection_match in &mut detection.matches {
364        if detection_match.from_file.is_none() {
365            detection_match.from_file = Some(path.to_string());
366        }
367
368        if detection_match.rule_identifier.is_empty() {
369            detection_match.rule_identifier = detection_match.matcher.to_string();
370        }
371    }
372
373    if detection.identifier.is_empty() {
374        detection.identifier = compute_public_detection_identifier(detection);
375    }
376}
377
378fn compute_public_detection_identifier(detection: &LicenseDetection) -> String {
379    let expression = python_safe_name(&detection.license_expression);
380    let mut hasher = Sha1::new();
381    hasher.update(format_public_detection_content(detection).as_bytes());
382    let hex_str = hex::encode(hasher.finalize());
383    let uuid_hex = &hex_str[..32];
384    let content_uuid = uuid::Uuid::parse_str(uuid_hex)
385        .map(|uuid| uuid.to_string())
386        .unwrap_or_else(|_| uuid_hex.to_string());
387
388    format!("{}-{}", expression, content_uuid)
389}
390
391fn format_public_detection_content(detection: &LicenseDetection) -> String {
392    let mut result = String::from("(");
393
394    for (index, detection_match) in detection.matches.iter().enumerate() {
395        if index > 0 {
396            result.push_str(", ");
397        }
398        result.push_str(&format!(
399            "({}, {}, {})",
400            python_str_repr(if detection_match.rule_identifier.is_empty() {
401                detection_match.matcher.as_str()
402            } else {
403                detection_match.rule_identifier.as_str()
404            }),
405            detection_match.score.value() as f32,
406            python_token_tuple_repr(&tokenize_without_stopwords(
407                detection_match.matched_text.as_deref().unwrap_or_default(),
408            )),
409        ));
410    }
411
412    if detection.matches.len() == 1 {
413        result.push(',');
414    }
415    result.push(')');
416    result
417}
418
419fn python_safe_name(value: &str) -> String {
420    let mut result = String::new();
421    let mut prev_underscore = false;
422
423    for character in value.chars() {
424        if character.is_alphanumeric() {
425            result.push(character);
426            prev_underscore = false;
427        } else if !prev_underscore {
428            result.push('_');
429            prev_underscore = true;
430        }
431    }
432
433    let trimmed = result.trim_matches('_');
434    if trimmed.is_empty() {
435        String::new()
436    } else {
437        trimmed.to_string()
438    }
439}
440
441fn python_str_repr(value: &str) -> String {
442    if value.contains('\'') && !value.contains('"') {
443        format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
444    } else {
445        format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\\'"))
446    }
447}
448
449fn python_token_tuple_repr(tokens: &[String]) -> String {
450    if tokens.is_empty() {
451        return String::from("()");
452    }
453
454    let mut result = String::from("(");
455    for (index, token) in tokens.iter().enumerate() {
456        if index > 0 {
457            result.push_str(", ");
458        }
459        result.push_str(&python_str_repr(token));
460    }
461
462    if tokens.len() == 1 {
463        result.push(',');
464    }
465    result.push(')');
466    result
467}
468
469/// Package metadata extracted from manifest files.
470///
471/// Compatible with ScanCode Toolkit output format. Contains standardized package
472/// information including name, version, dependencies, licenses, and other metadata.
473/// This is the primary data structure returned by all parsers.
474#[derive(Serialize, Deserialize, Debug, Clone, Default)]
475pub struct PackageData {
476    pub package_type: Option<PackageType>,
477    pub namespace: Option<String>,
478    pub name: Option<String>,
479    pub version: Option<String>,
480    #[serde(default)]
481    pub qualifiers: Option<HashMap<String, String>>,
482    pub subpath: Option<String>,
483    pub primary_language: Option<String>,
484    pub description: Option<String>,
485    pub release_date: Option<String>,
486    #[serde(default)]
487    pub parties: Vec<Party>,
488    #[serde(default)]
489    pub keywords: Vec<String>,
490    pub homepage_url: Option<String>,
491    pub download_url: Option<String>,
492    pub size: Option<u64>,
493    pub sha1: Option<Sha1Digest>,
494    pub md5: Option<Md5Digest>,
495    pub sha256: Option<Sha256Digest>,
496    pub sha512: Option<Sha512Digest>,
497    pub bug_tracking_url: Option<String>,
498    pub code_view_url: Option<String>,
499    pub vcs_url: Option<String>,
500    pub copyright: Option<String>,
501    pub holder: Option<String>,
502    pub declared_license_expression: Option<String>,
503    pub declared_license_expression_spdx: Option<String>,
504    #[serde(default)]
505    pub license_detections: Vec<LicenseDetection>,
506    pub other_license_expression: Option<String>,
507    pub other_license_expression_spdx: Option<String>,
508    #[serde(default)]
509    pub other_license_detections: Vec<LicenseDetection>,
510    pub extracted_license_statement: Option<String>,
511    pub notice_text: Option<String>,
512    #[serde(default)]
513    pub source_packages: Vec<String>,
514    #[serde(default)]
515    pub file_references: Vec<FileReference>,
516    #[serde(default)]
517    pub is_private: bool,
518    #[serde(default)]
519    pub is_virtual: bool,
520    #[serde(default, with = "super::json_value_map")]
521    pub extra_data: Option<HashMap<String, serde_json::Value>>,
522    #[serde(default)]
523    pub dependencies: Vec<Dependency>,
524    pub repository_homepage_url: Option<String>,
525    pub repository_download_url: Option<String>,
526    pub api_data_url: Option<String>,
527    pub datasource_id: Option<DatasourceId>,
528    pub purl: Option<String>,
529}
530
531impl PackageData {
532    /// Extracts a single license expression from all license detections in this package.
533    /// Returns None if there are no license detections.
534    pub fn get_license_expression(&self) -> Option<String> {
535        if self.license_detections.is_empty() {
536            return None;
537        }
538
539        let expressions = self
540            .license_detections
541            .iter()
542            .map(|detection| detection.license_expression.clone());
543        combine_license_expressions(expressions)
544    }
545}
546
547/// License detection result containing matched license expressions.
548///
549/// Aggregates multiple license matches into a single SPDX license expression.
550#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
551pub struct LicenseDetection {
552    pub license_expression: String,
553    pub license_expression_spdx: String,
554    pub matches: Vec<Match>,
555    #[serde(default)]
556    pub detection_log: Vec<String>,
557    #[serde(default = "String::new")]
558    pub identifier: String,
559}
560
561/// Individual license text match with location and confidence score.
562///
563/// Represents a specific region of text that matched a known license pattern.
564#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
565pub struct Match {
566    pub license_expression: String,
567    pub license_expression_spdx: String,
568    pub from_file: Option<String>,
569    pub start_line: LineNumber,
570    pub end_line: LineNumber,
571    #[serde(default)]
572    pub matcher: MatcherKind,
573    pub score: MatchScore,
574    pub matched_length: Option<usize>,
575    pub match_coverage: Option<f64>,
576    pub rule_relevance: Option<u8>,
577    #[serde(default = "String::new")]
578    pub rule_identifier: String,
579    pub rule_url: Option<String>,
580    pub matched_text: Option<String>,
581    pub matched_text_diagnostics: Option<String>,
582    #[serde(default)]
583    pub referenced_filenames: Option<Vec<String>>,
584}
585
586#[derive(Serialize, Deserialize, Debug, Clone)]
587pub struct Copyright {
588    pub copyright: String,
589    #[serde(default)]
590    pub normalized_copyright: Option<String>,
591    pub start_line: LineNumber,
592    pub end_line: LineNumber,
593}
594
595impl Copyright {
596    pub fn normalized_text(&self) -> &str {
597        self.normalized_copyright
598            .as_deref()
599            .unwrap_or(self.copyright.as_str())
600    }
601}
602
603#[derive(Serialize, Deserialize, Debug, Clone)]
604pub struct Holder {
605    pub holder: String,
606    pub start_line: LineNumber,
607    pub end_line: LineNumber,
608}
609
610#[derive(Serialize, Deserialize, Debug, Clone)]
611pub struct Author {
612    pub author: String,
613    pub start_line: LineNumber,
614    pub end_line: LineNumber,
615}
616
617/// Package dependency information with version constraints.
618///
619/// Represents a declared dependency with scope (e.g., runtime, dev, optional)
620/// and optional resolved package details.
621#[derive(Serialize, Deserialize, Debug, Clone)]
622pub struct Dependency {
623    pub purl: Option<String>,
624    pub extracted_requirement: Option<String>,
625    pub scope: Option<String>,
626    pub is_runtime: Option<bool>,
627    pub is_optional: Option<bool>,
628    pub is_pinned: Option<bool>,
629    pub is_direct: Option<bool>,
630    pub resolved_package: Option<Box<ResolvedPackage>>,
631    #[serde(default, with = "super::json_value_map")]
632    pub extra_data: Option<HashMap<String, serde_json::Value>>,
633}
634
635#[derive(Serialize, Deserialize, Debug, Clone)]
636pub struct ResolvedPackage {
637    pub package_type: PackageType,
638    pub namespace: String,
639    pub name: String,
640    pub version: String,
641    #[serde(default)]
642    pub qualifiers: Option<HashMap<String, String>>,
643    pub subpath: Option<String>,
644    pub primary_language: Option<String>,
645    pub description: Option<String>,
646    pub release_date: Option<String>,
647    #[serde(default)]
648    pub parties: Vec<Party>,
649    #[serde(default)]
650    pub keywords: Vec<String>,
651    pub homepage_url: Option<String>,
652    pub download_url: Option<String>,
653    pub size: Option<u64>,
654    pub sha1: Option<Sha1Digest>,
655    pub md5: Option<Md5Digest>,
656    pub sha256: Option<Sha256Digest>,
657    pub sha512: Option<Sha512Digest>,
658    pub bug_tracking_url: Option<String>,
659    pub code_view_url: Option<String>,
660    pub vcs_url: Option<String>,
661    pub copyright: Option<String>,
662    pub holder: Option<String>,
663    pub declared_license_expression: Option<String>,
664    pub declared_license_expression_spdx: Option<String>,
665    #[serde(default)]
666    pub license_detections: Vec<LicenseDetection>,
667    pub other_license_expression: Option<String>,
668    pub other_license_expression_spdx: Option<String>,
669    #[serde(default)]
670    pub other_license_detections: Vec<LicenseDetection>,
671    pub extracted_license_statement: Option<String>,
672    pub notice_text: Option<String>,
673    #[serde(default)]
674    pub source_packages: Vec<String>,
675    #[serde(default)]
676    pub file_references: Vec<FileReference>,
677    #[serde(default)]
678    pub is_private: bool,
679    #[serde(default)]
680    pub is_virtual: bool,
681    #[serde(default, with = "super::json_value_map")]
682    pub extra_data: Option<HashMap<String, serde_json::Value>>,
683    #[serde(default)]
684    pub dependencies: Vec<Dependency>,
685    pub repository_homepage_url: Option<String>,
686    pub repository_download_url: Option<String>,
687    pub api_data_url: Option<String>,
688    pub datasource_id: Option<DatasourceId>,
689    pub purl: Option<String>,
690}
691
692impl ResolvedPackage {
693    pub fn new(
694        package_type: PackageType,
695        namespace: String,
696        name: String,
697        version: String,
698    ) -> Self {
699        Self {
700            package_type,
701            namespace,
702            name,
703            version,
704            qualifiers: None,
705            subpath: None,
706            primary_language: None,
707            description: None,
708            release_date: None,
709            parties: vec![],
710            keywords: vec![],
711            homepage_url: None,
712            download_url: None,
713            size: None,
714            sha1: None,
715            md5: None,
716            sha256: None,
717            sha512: None,
718            bug_tracking_url: None,
719            code_view_url: None,
720            vcs_url: None,
721            copyright: None,
722            holder: None,
723            declared_license_expression: None,
724            declared_license_expression_spdx: None,
725            license_detections: vec![],
726            other_license_expression: None,
727            other_license_expression_spdx: None,
728            other_license_detections: vec![],
729            extracted_license_statement: None,
730            notice_text: None,
731            source_packages: vec![],
732            file_references: vec![],
733            is_private: false,
734            is_virtual: false,
735            extra_data: None,
736            dependencies: vec![],
737            repository_homepage_url: None,
738            repository_download_url: None,
739            api_data_url: None,
740            datasource_id: None,
741            purl: None,
742        }
743    }
744
745    pub fn from_package_data(package_data: &PackageData, fallback_type: PackageType) -> Self {
746        Self {
747            package_type: package_data.package_type.unwrap_or(fallback_type),
748            namespace: package_data.namespace.clone().unwrap_or_default(),
749            name: package_data.name.clone().unwrap_or_default(),
750            version: package_data.version.clone().unwrap_or_default(),
751            qualifiers: package_data.qualifiers.clone(),
752            subpath: package_data.subpath.clone(),
753            primary_language: package_data.primary_language.clone(),
754            description: package_data.description.clone(),
755            release_date: package_data.release_date.clone(),
756            parties: package_data.parties.clone(),
757            keywords: package_data.keywords.clone(),
758            homepage_url: package_data.homepage_url.clone(),
759            download_url: package_data.download_url.clone(),
760            size: package_data.size,
761            sha1: package_data.sha1,
762            md5: package_data.md5,
763            sha256: package_data.sha256,
764            sha512: package_data.sha512,
765            bug_tracking_url: package_data.bug_tracking_url.clone(),
766            code_view_url: package_data.code_view_url.clone(),
767            vcs_url: package_data.vcs_url.clone(),
768            copyright: package_data.copyright.clone(),
769            holder: package_data.holder.clone(),
770            declared_license_expression: package_data.declared_license_expression.clone(),
771            declared_license_expression_spdx: package_data.declared_license_expression_spdx.clone(),
772            license_detections: package_data.license_detections.clone(),
773            other_license_expression: package_data.other_license_expression.clone(),
774            other_license_expression_spdx: package_data.other_license_expression_spdx.clone(),
775            other_license_detections: package_data.other_license_detections.clone(),
776            extracted_license_statement: package_data.extracted_license_statement.clone(),
777            notice_text: package_data.notice_text.clone(),
778            source_packages: package_data.source_packages.clone(),
779            file_references: package_data.file_references.clone(),
780            is_private: package_data.is_private,
781            is_virtual: package_data.is_virtual,
782            extra_data: package_data.extra_data.clone(),
783            dependencies: package_data.dependencies.clone(),
784            repository_homepage_url: package_data.repository_homepage_url.clone(),
785            repository_download_url: package_data.repository_download_url.clone(),
786            api_data_url: package_data.api_data_url.clone(),
787            datasource_id: package_data.datasource_id,
788            purl: package_data.purl.clone(),
789        }
790    }
791}
792
793#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
794pub enum PartyType {
795    Person,
796    Organization,
797}
798
799impl fmt::Display for PartyType {
800    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
801        match self {
802            PartyType::Person => write!(f, "person"),
803            PartyType::Organization => write!(f, "organization"),
804        }
805    }
806}
807
808impl FromStr for PartyType {
809    type Err = String;
810    fn from_str(s: &str) -> Result<Self, Self::Err> {
811        match s {
812            "person" => Ok(PartyType::Person),
813            "organization" => Ok(PartyType::Organization),
814            other => Err(format!("unknown party type: {other}")),
815        }
816    }
817}
818
819/// Author, maintainer, or contributor information.
820///
821/// Represents a person or organization associated with a package.
822#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
823pub struct Party {
824    pub r#type: Option<PartyType>,
825    pub role: Option<String>,
826    pub name: Option<String>,
827    pub email: Option<String>,
828    pub url: Option<String>,
829    pub organization: Option<String>,
830    pub organization_url: Option<String>,
831    pub timezone: Option<String>,
832}
833
834impl Party {
835    pub(crate) fn person(role: &str, name: Option<String>, email: Option<String>) -> Self {
836        Self {
837            r#type: Some(PartyType::Person),
838            role: Some(role.to_string()),
839            name,
840            email,
841            url: None,
842            organization: None,
843            organization_url: None,
844            timezone: None,
845        }
846    }
847}
848
849/// Reference to a file within a package archive with checksums.
850///
851/// Used in SBOM generation to track files within distribution archives.
852#[derive(Serialize, Deserialize, Debug, Clone)]
853pub struct FileReference {
854    pub path: String,
855    pub size: Option<u64>,
856    pub sha1: Option<Sha1Digest>,
857    pub md5: Option<Md5Digest>,
858    pub sha256: Option<Sha256Digest>,
859    pub sha512: Option<Sha512Digest>,
860    #[serde(with = "super::json_value_map")]
861    pub extra_data: Option<std::collections::HashMap<String, serde_json::Value>>,
862}
863
864impl FileReference {
865    pub(crate) fn from_path(path: String) -> Self {
866        Self {
867            path,
868            size: None,
869            sha1: None,
870            md5: None,
871            sha256: None,
872            sha512: None,
873            extra_data: None,
874        }
875    }
876}
877
878/// Top-level assembled package, created by merging one or more `PackageData`
879/// objects from related manifest/lockfiles (e.g., package.json + package-lock.json).
880///
881/// Compatible with ScanCode Toolkit output format. The key differences from
882/// `PackageData` are:
883/// - `package_uid`: unique identifier (PURL with UUID qualifier)
884/// - `datafile_paths`: list of all contributing files
885/// - `datasource_ids`: list of all contributing parsers
886/// - Excludes `dependencies` and `file_references` (hoisted to top-level)
887#[derive(Serialize, Deserialize, Debug, Clone)]
888pub struct Package {
889    pub package_type: Option<PackageType>,
890    pub namespace: Option<String>,
891    pub name: Option<String>,
892    pub version: Option<String>,
893    #[serde(default)]
894    pub qualifiers: Option<HashMap<String, String>>,
895    pub subpath: Option<String>,
896    pub primary_language: Option<String>,
897    pub description: Option<String>,
898    pub release_date: Option<String>,
899    #[serde(default)]
900    pub parties: Vec<Party>,
901    #[serde(default)]
902    pub keywords: Vec<String>,
903    pub homepage_url: Option<String>,
904    pub download_url: Option<String>,
905    pub size: Option<u64>,
906    pub sha1: Option<Sha1Digest>,
907    pub md5: Option<Md5Digest>,
908    pub sha256: Option<Sha256Digest>,
909    pub sha512: Option<Sha512Digest>,
910    pub bug_tracking_url: Option<String>,
911    pub code_view_url: Option<String>,
912    pub vcs_url: Option<String>,
913    pub copyright: Option<String>,
914    pub holder: Option<String>,
915    pub declared_license_expression: Option<String>,
916    pub declared_license_expression_spdx: Option<String>,
917    #[serde(default)]
918    pub license_detections: Vec<LicenseDetection>,
919    pub other_license_expression: Option<String>,
920    pub other_license_expression_spdx: Option<String>,
921    #[serde(default)]
922    pub other_license_detections: Vec<LicenseDetection>,
923    pub extracted_license_statement: Option<String>,
924    pub notice_text: Option<String>,
925    #[serde(default)]
926    pub source_packages: Vec<String>,
927    #[serde(default)]
928    pub is_private: bool,
929    #[serde(default)]
930    pub is_virtual: bool,
931    #[serde(default, with = "super::json_value_map")]
932    pub extra_data: Option<HashMap<String, serde_json::Value>>,
933    pub repository_homepage_url: Option<String>,
934    pub repository_download_url: Option<String>,
935    pub api_data_url: Option<String>,
936    pub purl: Option<String>,
937    /// Unique identifier for this package instance (PURL with UUID qualifier).
938    pub package_uid: PackageUid,
939    /// Paths to all datafiles that contributed to this package.
940    pub datafile_paths: Vec<String>,
941    /// Datasource identifiers for all parsers that contributed to this package.
942    pub datasource_ids: Vec<DatasourceId>,
943}
944
945impl Package {
946    /// Create a `Package` from a `PackageData` and its source file path.
947    ///
948    /// Generates a unique `package_uid` from the package PURL when available.
949    /// For packages without a PURL but with enough manifest identity to assemble,
950    /// falls back to an opaque UID derived from datasource/name/version.
951    pub fn from_package_data(package_data: &PackageData, datafile_path: String) -> Self {
952        let mut package_data = package_data.clone();
953        enrich_package_data_license_provenance(&mut package_data, &datafile_path);
954
955        let mut package = Package {
956            package_type: package_data.package_type,
957            namespace: package_data.namespace.clone(),
958            name: package_data.name.clone(),
959            version: package_data.version.clone(),
960            qualifiers: package_data.qualifiers.clone(),
961            subpath: package_data.subpath.clone(),
962            primary_language: package_data.primary_language.clone(),
963            description: package_data.description.clone(),
964            release_date: package_data.release_date.clone(),
965            parties: package_data.parties.clone(),
966            keywords: package_data.keywords.clone(),
967            homepage_url: package_data.homepage_url.clone(),
968            download_url: package_data.download_url.clone(),
969            size: package_data.size,
970            sha1: package_data.sha1,
971            md5: package_data.md5,
972            sha256: package_data.sha256,
973            sha512: package_data.sha512,
974            bug_tracking_url: package_data.bug_tracking_url.clone(),
975            code_view_url: package_data.code_view_url.clone(),
976            vcs_url: package_data.vcs_url.clone(),
977            copyright: package_data.copyright.clone(),
978            holder: package_data.holder.clone(),
979            declared_license_expression: package_data.declared_license_expression.clone(),
980            declared_license_expression_spdx: package_data.declared_license_expression_spdx.clone(),
981            license_detections: package_data.license_detections.clone(),
982            other_license_expression: package_data.other_license_expression.clone(),
983            other_license_expression_spdx: package_data.other_license_expression_spdx.clone(),
984            other_license_detections: package_data.other_license_detections.clone(),
985            extracted_license_statement: package_data.extracted_license_statement.clone(),
986            notice_text: package_data.notice_text.clone(),
987            source_packages: package_data.source_packages.clone(),
988            is_private: package_data.is_private,
989            is_virtual: package_data.is_virtual,
990            extra_data: package_data.extra_data.clone(),
991            repository_homepage_url: package_data.repository_homepage_url.clone(),
992            repository_download_url: package_data.repository_download_url.clone(),
993            api_data_url: package_data.api_data_url.clone(),
994            purl: package_data.purl.clone(),
995            package_uid: PackageUid::empty(),
996            datafile_paths: vec![datafile_path],
997            datasource_ids: if let Some(dsid) = package_data.datasource_id {
998                vec![dsid]
999            } else {
1000                vec![]
1001            },
1002        };
1003
1004        package.refresh_identity();
1005        if package.package_uid.is_empty() {
1006            package.package_uid = package.fallback_package_uid();
1007        }
1008
1009        package
1010    }
1011
1012    /// Update this package with data from another `PackageData`.
1013    ///
1014    /// Merges data from a related file (e.g., lockfile) into this package.
1015    /// Existing non-empty values are preserved; empty fields are filled from
1016    /// the new data. Lists (parties, license_detections) are merged.
1017    pub fn update(&mut self, package_data: &PackageData, datafile_path: String) {
1018        let mut package_data = package_data.clone();
1019        enrich_package_data_license_provenance(&mut package_data, &datafile_path);
1020
1021        if let Some(dsid) = package_data.datasource_id {
1022            self.datasource_ids.push(dsid);
1023        }
1024        self.datafile_paths.push(datafile_path);
1025
1026        macro_rules! fill_if_empty {
1027            ($field:ident) => {
1028                if self.$field.is_none() {
1029                    self.$field = package_data.$field;
1030                }
1031            };
1032        }
1033
1034        fill_if_empty!(package_type);
1035        fill_if_empty!(name);
1036        fill_if_empty!(namespace);
1037        fill_if_empty!(version);
1038        fill_if_empty!(qualifiers);
1039        fill_if_empty!(subpath);
1040        fill_if_empty!(primary_language);
1041        fill_if_empty!(description);
1042        fill_if_empty!(release_date);
1043        fill_if_empty!(homepage_url);
1044        fill_if_empty!(download_url);
1045        fill_if_empty!(size);
1046        fill_if_empty!(sha1);
1047        fill_if_empty!(md5);
1048        fill_if_empty!(sha256);
1049        fill_if_empty!(sha512);
1050        fill_if_empty!(bug_tracking_url);
1051        fill_if_empty!(code_view_url);
1052        fill_if_empty!(vcs_url);
1053        fill_if_empty!(copyright);
1054        fill_if_empty!(holder);
1055        fill_if_empty!(declared_license_expression);
1056        fill_if_empty!(declared_license_expression_spdx);
1057        fill_if_empty!(other_license_expression);
1058        fill_if_empty!(other_license_expression_spdx);
1059        fill_if_empty!(extracted_license_statement);
1060        fill_if_empty!(notice_text);
1061        match (&mut self.extra_data, &package_data.extra_data) {
1062            (None, Some(extra_data)) => {
1063                self.extra_data = Some(extra_data.clone());
1064            }
1065            (Some(existing), Some(incoming)) => {
1066                for (key, value) in incoming {
1067                    existing.entry(key.clone()).or_insert_with(|| value.clone());
1068                }
1069            }
1070            _ => {}
1071        }
1072        fill_if_empty!(repository_homepage_url);
1073        fill_if_empty!(repository_download_url);
1074        fill_if_empty!(api_data_url);
1075
1076        for party in &package_data.parties {
1077            if let Some(existing) = self.parties.iter_mut().find(|p| {
1078                p.role == party.role
1079                    && ((p.name.is_some() && p.name == party.name)
1080                        || (p.email.is_some() && p.email == party.email))
1081            }) {
1082                if existing.name.is_none() {
1083                    existing.name = party.name.clone();
1084                }
1085                if existing.email.is_none() {
1086                    existing.email = party.email.clone();
1087                }
1088            } else {
1089                self.parties.push(party.clone());
1090            }
1091        }
1092
1093        for keyword in &package_data.keywords {
1094            if !self.keywords.contains(keyword) {
1095                self.keywords.push(keyword.clone());
1096            }
1097        }
1098
1099        for detection in &package_data.license_detections {
1100            self.license_detections.push(detection.clone());
1101        }
1102
1103        for detection in &package_data.other_license_detections {
1104            self.other_license_detections.push(detection.clone());
1105        }
1106
1107        for source_pkg in &package_data.source_packages {
1108            if !self.source_packages.contains(source_pkg) {
1109                self.source_packages.push(source_pkg.clone());
1110            }
1111        }
1112
1113        self.refresh_identity();
1114    }
1115
1116    pub fn backfill_license_provenance(&mut self) {
1117        let Some(datafile_path) = self.datafile_paths.first().cloned() else {
1118            return;
1119        };
1120
1121        for detection in &mut self.license_detections {
1122            enrich_license_detection_provenance(detection, &datafile_path);
1123        }
1124        for detection in &mut self.other_license_detections {
1125            enrich_license_detection_provenance(detection, &datafile_path);
1126        }
1127    }
1128
1129    fn refresh_identity(&mut self) {
1130        let Some(next_purl) = self.build_current_purl() else {
1131            return;
1132        };
1133
1134        if self.purl.as_deref() != Some(next_purl.as_str()) || self.package_uid.is_empty() {
1135            self.package_uid = PackageUid::new(&next_purl);
1136        }
1137
1138        self.purl = Some(next_purl);
1139    }
1140
1141    fn fallback_package_uid(&self) -> PackageUid {
1142        let name = self
1143            .name
1144            .as_deref()
1145            .map(str::trim)
1146            .filter(|value| !value.is_empty())
1147            .unwrap_or("unknown");
1148        let version = self
1149            .version
1150            .as_deref()
1151            .map(str::trim)
1152            .filter(|value| !value.is_empty())
1153            .unwrap_or("unknown");
1154        let datasource = self
1155            .datasource_ids
1156            .first()
1157            .map(DatasourceId::as_str)
1158            .unwrap_or("unknown");
1159
1160        PackageUid::new_opaque(&format!("generated-package:{datasource}/{name}@{version}"))
1161    }
1162
1163    fn build_current_purl(&self) -> Option<String> {
1164        if let Some(existing_purl) = self.purl.as_deref() {
1165            let mut purl = PackageUrl::from_str(existing_purl).ok()?;
1166
1167            if let Some(version) = self
1168                .version
1169                .as_deref()
1170                .filter(|value| !value.trim().is_empty())
1171            {
1172                purl.with_version(version).ok()?;
1173            } else {
1174                purl.without_version();
1175            }
1176
1177            return Some(purl.to_string());
1178        }
1179
1180        if let (Some(package_type), Some(name)) = (
1181            self.package_type.as_ref(),
1182            self.name
1183                .as_deref()
1184                .filter(|value| !value.trim().is_empty()),
1185        ) {
1186            let purl_type = match package_type {
1187                PackageType::Deno => "generic",
1188                _ => package_type.as_str(),
1189            };
1190
1191            let mut purl = PackageUrl::new(purl_type, name).ok()?;
1192
1193            if let Some(namespace) = self
1194                .namespace
1195                .as_deref()
1196                .filter(|value| !value.trim().is_empty())
1197            {
1198                purl.with_namespace(namespace).ok()?;
1199            }
1200
1201            if let Some(version) = self
1202                .version
1203                .as_deref()
1204                .filter(|value| !value.trim().is_empty())
1205            {
1206                purl.with_version(version).ok()?;
1207            }
1208
1209            if let Some(qualifiers) = &self.qualifiers {
1210                for (key, value) in qualifiers {
1211                    purl.add_qualifier(key.as_str(), value.as_str()).ok()?;
1212                }
1213            }
1214
1215            if let Some(subpath) = self
1216                .subpath
1217                .as_deref()
1218                .filter(|value| !value.trim().is_empty())
1219            {
1220                purl.with_subpath(subpath).ok()?;
1221            }
1222
1223            return Some(purl.to_string());
1224        }
1225        None
1226    }
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231    use super::*;
1232
1233    #[test]
1234    fn file_info_new_backfills_package_detection_provenance() {
1235        let package_data = PackageData {
1236            package_type: Some(PackageType::Npm),
1237            license_detections: vec![LicenseDetection {
1238                license_expression: "mit".to_string(),
1239                license_expression_spdx: "MIT".to_string(),
1240                matches: vec![Match {
1241                    license_expression: "mit".to_string(),
1242                    license_expression_spdx: "MIT".to_string(),
1243                    from_file: None,
1244                    start_line: LineNumber::ONE,
1245                    end_line: LineNumber::ONE,
1246                    matcher: MatcherKind::Declared,
1247                    score: MatchScore::MAX,
1248                    matched_length: Some(1),
1249                    match_coverage: Some(100.0),
1250                    rule_relevance: Some(100),
1251                    rule_identifier: String::new(),
1252                    rule_url: None,
1253                    matched_text: Some("MIT".to_string()),
1254                    referenced_filenames: None,
1255                    matched_text_diagnostics: None,
1256                }],
1257                detection_log: vec![],
1258                identifier: String::new(),
1259            }],
1260            ..PackageData::default()
1261        };
1262
1263        let file_info = FileInfo::new(
1264            "package.json".to_string(),
1265            "package".to_string(),
1266            ".json".to_string(),
1267            "project/package.json".to_string(),
1268            FileType::File,
1269            None,
1270            None,
1271            1,
1272            None,
1273            None,
1274            None,
1275            None,
1276            None,
1277            vec![package_data],
1278            None,
1279            vec![],
1280            vec![],
1281            vec![],
1282            vec![],
1283            vec![],
1284            vec![],
1285            vec![],
1286            vec![],
1287            vec![],
1288        );
1289
1290        assert_eq!(file_info.license_detections.len(), 1);
1291        assert_eq!(
1292            file_info.license_detections[0].matches[0]
1293                .from_file
1294                .as_deref(),
1295            Some("project/package.json")
1296        );
1297        assert!(!file_info.license_detections[0].identifier.is_empty());
1298        assert_eq!(
1299            file_info.package_data[0].license_detections[0].matches[0]
1300                .from_file
1301                .as_deref(),
1302            Some("project/package.json")
1303        );
1304        assert_eq!(
1305            file_info.package_data[0].license_detections[0].matches[0].rule_identifier,
1306            "parser-declared-license"
1307        );
1308        assert!(
1309            !file_info.package_data[0].license_detections[0]
1310                .identifier
1311                .is_empty()
1312        );
1313    }
1314
1315    #[test]
1316    fn package_from_package_data_backfills_detection_provenance() {
1317        let package_data = PackageData {
1318            package_type: Some(PackageType::Npm),
1319            license_detections: vec![LicenseDetection {
1320                license_expression: "mit".to_string(),
1321                license_expression_spdx: "MIT".to_string(),
1322                matches: vec![Match {
1323                    license_expression: "mit".to_string(),
1324                    license_expression_spdx: "MIT".to_string(),
1325                    from_file: None,
1326                    start_line: LineNumber::ONE,
1327                    end_line: LineNumber::ONE,
1328                    matcher: MatcherKind::Declared,
1329                    score: MatchScore::MAX,
1330                    matched_length: Some(1),
1331                    match_coverage: Some(100.0),
1332                    rule_relevance: Some(100),
1333                    rule_identifier: String::new(),
1334                    rule_url: None,
1335                    matched_text: Some("MIT".to_string()),
1336                    referenced_filenames: None,
1337                    matched_text_diagnostics: None,
1338                }],
1339                detection_log: vec![],
1340                identifier: String::new(),
1341            }],
1342            ..PackageData::default()
1343        };
1344
1345        let package = Package::from_package_data(&package_data, "project/package.json".to_string());
1346
1347        assert_eq!(
1348            package.license_detections[0].matches[0]
1349                .from_file
1350                .as_deref(),
1351            Some("project/package.json")
1352        );
1353        assert_eq!(
1354            package.license_detections[0].matches[0].rule_identifier,
1355            "parser-declared-license"
1356        );
1357        assert!(!package.license_detections[0].identifier.is_empty());
1358    }
1359
1360    #[test]
1361    fn package_from_package_data_preserves_existing_purl_qualifiers() {
1362        let package_data = PackageData {
1363            package_type: Some(PackageType::Alpine),
1364            namespace: Some("alpine".to_string()),
1365            name: Some("busybox".to_string()),
1366            version: Some("1.35.0-r17".to_string()),
1367            purl: Some("pkg:alpine/busybox@1.35.0-r17?arch=x86_64".to_string()),
1368            ..PackageData::default()
1369        };
1370
1371        let package = Package::from_package_data(&package_data, "lib/apk/db/installed".to_string());
1372
1373        assert_eq!(
1374            package.purl.as_deref(),
1375            Some("pkg:alpine/busybox@1.35.0-r17?arch=x86_64")
1376        );
1377        assert!(
1378            package
1379                .package_uid
1380                .starts_with("pkg:alpine/busybox@1.35.0-r17?arch=x86_64&uuid=")
1381        );
1382    }
1383}
1384
1385/// Top-level dependency instance, created during package assembly.
1386///
1387/// Extends the file-level `Dependency` with traceability fields that link
1388/// each dependency to its owning package and source datafile.
1389#[derive(Serialize, Deserialize, Debug, Clone)]
1390pub struct TopLevelDependency {
1391    pub purl: Option<String>,
1392    pub extracted_requirement: Option<String>,
1393    pub scope: Option<String>,
1394    pub is_runtime: Option<bool>,
1395    pub is_optional: Option<bool>,
1396    pub is_pinned: Option<bool>,
1397    pub is_direct: Option<bool>,
1398    pub resolved_package: Option<Box<ResolvedPackage>>,
1399    #[serde(default, with = "super::json_value_map")]
1400    pub extra_data: Option<HashMap<String, serde_json::Value>>,
1401    /// Unique identifier for this dependency instance (PURL with UUID qualifier).
1402    pub dependency_uid: DependencyUid,
1403    /// The `package_uid` of the package this dependency belongs to.
1404    pub for_package_uid: Option<PackageUid>,
1405    /// Path to the datafile where this dependency was declared.
1406    pub datafile_path: String,
1407    /// Datasource identifier for the parser that extracted this dependency.
1408    pub datasource_id: DatasourceId,
1409    /// Namespace for the dependency (e.g., distribution name for RPM packages).
1410    pub namespace: Option<String>,
1411}
1412
1413impl TopLevelDependency {
1414    /// Create a `TopLevelDependency` from a file-level `Dependency`.
1415    pub fn from_dependency(
1416        dep: &Dependency,
1417        datafile_path: String,
1418        datasource_id: DatasourceId,
1419        for_package_uid: Option<PackageUid>,
1420    ) -> Self {
1421        let dependency_uid = dep
1422            .purl
1423            .as_ref()
1424            .map(|p| DependencyUid::new(p))
1425            .unwrap_or_else(DependencyUid::empty);
1426
1427        TopLevelDependency {
1428            purl: dep.purl.clone(),
1429            extracted_requirement: dep.extracted_requirement.clone(),
1430            scope: dep.scope.clone(),
1431            is_runtime: dep.is_runtime,
1432            is_optional: dep.is_optional,
1433            is_pinned: dep.is_pinned,
1434            is_direct: dep.is_direct,
1435            resolved_package: dep.resolved_package.clone(),
1436            extra_data: dep.extra_data.clone(),
1437            dependency_uid,
1438            for_package_uid,
1439            datafile_path,
1440            datasource_id,
1441            namespace: None,
1442        }
1443    }
1444}
1445
1446#[derive(Serialize, Deserialize, Debug, Clone)]
1447pub struct OutputEmail {
1448    pub email: String,
1449    pub start_line: LineNumber,
1450    pub end_line: LineNumber,
1451}
1452
1453#[derive(Serialize, Deserialize, Debug, Clone)]
1454pub struct OutputURL {
1455    pub url: String,
1456    pub start_line: LineNumber,
1457    pub end_line: LineNumber,
1458}
1459
1460#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1461pub struct LicensePolicyEntry {
1462    pub license_key: String,
1463    pub label: String,
1464    pub color_code: String,
1465    pub icon: String,
1466}
1467
1468#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1469pub enum FileType {
1470    File,
1471    Directory,
1472}