1use 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))]
33pub 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 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 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 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 if license_detections.is_empty() {
254 for pkg in &package_data {
255 license_detections.extend(pkg.license_detections.clone());
256 }
257 }
258
259 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#[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 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#[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#[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#[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#[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#[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#[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 pub package_uid: PackageUid,
939 pub datafile_paths: Vec<String>,
941 pub datasource_ids: Vec<DatasourceId>,
943}
944
945impl Package {
946 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 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#[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 pub dependency_uid: DependencyUid,
1403 pub for_package_uid: Option<PackageUid>,
1405 pub datafile_path: String,
1407 pub datasource_id: DatasourceId,
1409 pub namespace: Option<String>,
1411}
1412
1413impl TopLevelDependency {
1414 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}