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