1// SPDX-FileCopyrightText: 2020-2021 HH Partners
2//
3// SPDX-License-Identifier: MIT
4
5use serde::{Deserialize, Serialize};
6use spdx_expression::SpdxExpression;
7
8use super::Annotation;
9
10use super::{Checksum, FileInformation};
11
12/// ## Package Information
13///
14/// SPDX's [Package Information](https://spdx.github.io/spdx-spec/3-package-information/).
15#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
16#[serde(rename_all = "camelCase", deny_unknown_fields)]
17pub struct PackageInformation {
18 /// <https://spdx.github.io/spdx-spec/3-package-information/#31-package-name>
19 #[serde(rename = "name")]
20 pub package_name: String,
21
22 /// <https://spdx.github.io/spdx-spec/3-package-information/#32-package-spdx-identifier>
23 #[serde(rename = "SPDXID")]
24 pub package_spdx_identifier: String,
25
26 /// <https://spdx.github.io/spdx-spec/3-package-information/#33-package-version>
27 #[serde(
28 rename = "versionInfo",
29 skip_serializing_if = "Option::is_none",
30 default
31 )]
32 pub package_version: Option<String>,
33
34 /// <https://spdx.github.io/spdx-spec/3-package-information/#34-package-file-name>
35 #[serde(skip_serializing_if = "Option::is_none", default)]
36 pub package_file_name: Option<String>,
37
38 /// <https://spdx.github.io/spdx-spec/3-package-information/#35-package-supplier>
39 #[serde(rename = "supplier", skip_serializing_if = "Option::is_none", default)]
40 pub package_supplier: Option<String>,
41
42 /// <https://spdx.github.io/spdx-spec/3-package-information/#36-package-originator>
43 #[serde(
44 rename = "originator",
45 skip_serializing_if = "Option::is_none",
46 default
47 )]
48 pub package_originator: Option<String>,
49
50 /// <https://spdx.github.io/spdx-spec/3-package-information/#37-package-download-location>
51 #[serde(rename = "downloadLocation")]
52 pub package_download_location: String,
53
54 /// <https://spdx.github.io/spdx-spec/3-package-information/#38-files-analyzed>
55 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub files_analyzed: Option<bool>,
57
58 /// <https://spdx.github.io/spdx-spec/3-package-information/#39-package-verification-code>
59 #[serde(skip_serializing_if = "Option::is_none", default)]
60 pub package_verification_code: Option<PackageVerificationCode>,
61
62 /// <https://spdx.github.io/spdx-spec/3-package-information/#310-package-checksum>
63 #[serde(rename = "checksums", skip_serializing_if = "Vec::is_empty", default)]
64 pub package_checksum: Vec<Checksum>,
65
66 /// <https://spdx.github.io/spdx-spec/3-package-information/#311-package-home-page>
67 #[serde(rename = "homepage", skip_serializing_if = "Option::is_none", default)]
68 pub package_home_page: Option<String>,
69
70 /// <https://spdx.github.io/spdx-spec/3-package-information/#312-source-information>
71 #[serde(
72 rename = "sourceInfo",
73 skip_serializing_if = "Option::is_none",
74 default
75 )]
76 pub source_information: Option<String>,
77
78 /// <https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license>
79 #[serde(
80 rename = "licenseConcluded",
81 skip_serializing_if = "Option::is_none",
82 default
83 )]
84 pub concluded_license: Option<SpdxExpression>,
85
86 /// <https://spdx.github.io/spdx-spec/3-package-information/#314-all-licenses-information-from-files>
87 #[serde(
88 rename = "licenseInfoFromFiles",
89 skip_serializing_if = "Vec::is_empty",
90 default
91 )]
92 pub all_licenses_information_from_files: Vec<String>,
93
94 /// <https://spdx.github.io/spdx-spec/3-package-information/#315-declared-license>
95 #[serde(
96 rename = "licenseDeclared",
97 skip_serializing_if = "Option::is_none",
98 default
99 )]
100 pub declared_license: Option<SpdxExpression>,
101
102 /// <https://spdx.github.io/spdx-spec/3-package-information/#316-comments-on-license>
103 #[serde(
104 rename = "licenseComments",
105 skip_serializing_if = "Option::is_none",
106 default
107 )]
108 pub comments_on_license: Option<String>,
109
110 /// <https://spdx.github.io/spdx-spec/3-package-information/#317-copyright-text>
111 #[serde(
112 rename = "copyrightText",
113 skip_serializing_if = "Option::is_none",
114 default
115 )]
116 pub copyright_text: Option<String>,
117
118 /// <https://spdx.github.io/spdx-spec/3-package-information/#318-package-summary-description>
119 #[serde(rename = "summary", skip_serializing_if = "Option::is_none", default)]
120 pub package_summary_description: Option<String>,
121
122 /// <https://spdx.github.io/spdx-spec/3-package-information/#319-package-detailed-description>
123 #[serde(
124 rename = "description",
125 skip_serializing_if = "Option::is_none",
126 default
127 )]
128 pub package_detailed_description: Option<String>,
129
130 /// <https://spdx.github.io/spdx-spec/3-package-information/#320-package-comment>
131 #[serde(rename = "comment", skip_serializing_if = "Option::is_none", default)]
132 pub package_comment: Option<String>,
133
134 /// <https://spdx.github.io/spdx-spec/3-package-information/#321-external-reference>
135 #[serde(
136 rename = "externalRefs",
137 skip_serializing_if = "Vec::is_empty",
138 default
139 )]
140 pub external_reference: Vec<ExternalPackageReference>,
141
142 /// <https://spdx.github.io/spdx-spec/3-package-information/#323-package-attribution-text>
143 #[serde(
144 rename = "attributionTexts",
145 skip_serializing_if = "Vec::is_empty",
146 default
147 )]
148 pub package_attribution_text: Vec<String>,
149
150 /// List of "files in the package". Not sure which relationship type this maps to.
151 /// Info: <https://github.com/spdx/spdx-spec/issues/487>
152 // Valid SPDX?
153 #[serde(rename = "hasFiles", skip_serializing_if = "Vec::is_empty", default)]
154 pub files: Vec<String>,
155
156 #[serde(skip_serializing_if = "Vec::is_empty", default)]
157 pub annotations: Vec<Annotation>,
158
159 #[serde(rename = "builtDate", skip_serializing_if = "Option::is_none", default)]
160 pub built_date: Option<String>,
161
162 #[serde(
163 rename = "releaseDate",
164 skip_serializing_if = "Option::is_none",
165 default
166 )]
167 pub release_date: Option<String>,
168
169 #[serde(
170 rename = "validUntilDate",
171 skip_serializing_if = "Option::is_none",
172 default
173 )]
174 pub valid_until_date: Option<String>,
175
176 #[serde(
177 rename = "primaryPackagePurpose",
178 skip_serializing_if = "Option::is_none",
179 default
180 )]
181 pub primary_package_purpose: Option<PrimaryPackagePurpose>,
182}
183
184impl Default for PackageInformation {
185 fn default() -> Self {
186 Self {
187 package_name: "NOASSERTION".to_string(),
188 package_spdx_identifier: "NOASSERTION".to_string(),
189 package_version: None,
190 package_file_name: None,
191 package_supplier: None,
192 package_originator: None,
193 package_download_location: "NOASSERTION".to_string(),
194 files_analyzed: None,
195 package_verification_code: None,
196 package_checksum: Vec::new(),
197 package_home_page: None,
198 source_information: None,
199 concluded_license: None,
200 all_licenses_information_from_files: Vec::new(),
201 declared_license: None,
202 comments_on_license: None,
203 copyright_text: None,
204 package_summary_description: None,
205 package_detailed_description: None,
206 package_comment: None,
207 external_reference: Vec::new(),
208 package_attribution_text: Vec::new(),
209 files: Vec::new(),
210 annotations: Vec::new(),
211 built_date: None,
212 release_date: None,
213 valid_until_date: None,
214 primary_package_purpose: None,
215 }
216 }
217}
218
219impl PackageInformation {
220 /// Create new package.
221 pub fn new(name: &str, id: &mut i32) -> Self {
222 *id += 1;
223 Self {
224 package_name: name.to_string(),
225 package_spdx_identifier: format!("SPDXRef-{id}"),
226 ..Self::default()
227 }
228 }
229
230 /// Find all files of the package.
231 pub fn find_files_for_package<'a>(
232 &'a self,
233 files: &'a [FileInformation],
234 ) -> Vec<&'a FileInformation> {
235 self.files
236 .iter()
237 .filter_map(|file| {
238 files
239 .iter()
240 .find(|file_information| &file_information.file_spdx_identifier == file)
241 })
242 .collect()
243 }
244}
245
246/// <https://spdx.github.io/spdx-spec/3-package-information/#39-package-verification-code>
247#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Clone)]
248pub struct PackageVerificationCode {
249 /// Value of the verification code.
250 #[serde(rename = "packageVerificationCodeValue")]
251 pub value: String,
252
253 /// Files that were excluded when calculating the verification code.
254 #[serde(
255 rename = "packageVerificationCodeExcludedFiles",
256 skip_serializing_if = "Vec::is_empty",
257 default
258 )]
259 pub excludes: Vec<String>,
260}
261
262impl PackageVerificationCode {
263 pub fn new(value: String, excludes: Vec<String>) -> Self {
264 Self { value, excludes }
265 }
266}
267
268/// <https://spdx.github.io/spdx-spec/3-package-information/#321-external-reference>
269#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Clone)]
270#[serde(rename_all = "camelCase")]
271pub struct ExternalPackageReference {
272 pub reference_category: ExternalPackageReferenceCategory,
273 pub reference_type: String,
274 pub reference_locator: String,
275 #[serde(rename = "comment")]
276 #[serde(skip_serializing_if = "Option::is_none")]
277 #[serde(default)]
278 pub reference_comment: Option<String>,
279}
280
281impl ExternalPackageReference {
282 pub const fn new(
283 reference_category: ExternalPackageReferenceCategory,
284 reference_type: String,
285 reference_locator: String,
286 reference_comment: Option<String>,
287 ) -> Self {
288 Self {
289 reference_category,
290 reference_type,
291 reference_locator,
292 reference_comment,
293 }
294 }
295}
296
297/// <https://spdx.github.io/spdx-spec/3-package-information/#321-external-reference>
298#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Clone)]
299#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
300pub enum ExternalPackageReferenceCategory {
301 Security,
302 #[serde(alias = "PACKAGE_MANAGER")]
303 PackageManager,
304 #[serde(alias = "PERSISTENT_ID")]
305 PersistentID,
306 Other,
307}
308
309#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)]
310#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
311pub enum PrimaryPackagePurpose {
312 Application,
313 Framework,
314 Library,
315 Container,
316 OperatingSystem,
317 Device,
318 Firmware,
319 Source,
320 Archive,
321 File,
322 Install,
323 Other,
324}
325
326#[cfg(test)]
327mod test {
328 use std::fs::read_to_string;
329
330 use crate::models::{Algorithm, SPDX};
331
332 use super::*;
333
334 #[test]
335 fn all_packages_are_deserialized() {
336 let spdx: SPDX = serde_json::from_str(
337 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
338 )
339 .unwrap();
340 assert_eq!(spdx.package_information.len(), 4);
341 }
342 #[test]
343 fn package_name() {
344 let spdx: SPDX = serde_json::from_str(
345 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
346 )
347 .unwrap();
348 assert_eq!(
349 spdx.package_information[0].package_name,
350 "glibc".to_string()
351 );
352 }
353 #[test]
354 fn package_spdx_identifier() {
355 let spdx: SPDX = serde_json::from_str(
356 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
357 )
358 .unwrap();
359 assert_eq!(
360 spdx.package_information[0].package_spdx_identifier,
361 "SPDXRef-Package".to_string()
362 );
363 }
364 #[test]
365 fn package_version() {
366 let spdx: SPDX = serde_json::from_str(
367 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
368 )
369 .unwrap();
370 assert_eq!(
371 spdx.package_information[0].package_version,
372 Some("2.11.1".to_string())
373 );
374 }
375 #[test]
376 fn package_file_name() {
377 let spdx: SPDX = serde_json::from_str(
378 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
379 )
380 .unwrap();
381 assert_eq!(
382 spdx.package_information[0].package_file_name,
383 Some("glibc-2.11.1.tar.gz".to_string())
384 );
385 }
386 #[test]
387 fn package_supplier() {
388 let spdx: SPDX = serde_json::from_str(
389 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
390 )
391 .unwrap();
392 assert_eq!(
393 spdx.package_information[0].package_supplier,
394 Some("Person: Jane Doe (jane.doe@example.com)".to_string())
395 );
396 }
397 #[test]
398 fn package_originator() {
399 let spdx: SPDX = serde_json::from_str(
400 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
401 )
402 .unwrap();
403 assert_eq!(
404 spdx.package_information[0].package_originator,
405 Some("Organization: ExampleCodeInspect (contact@example.com)".to_string())
406 );
407 }
408 #[test]
409 fn package_download_location() {
410 let spdx: SPDX = serde_json::from_str(
411 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
412 )
413 .unwrap();
414 assert_eq!(
415 spdx.package_information[0].package_download_location,
416 "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz".to_string()
417 );
418 }
419 #[test]
420 fn files_analyzed() {
421 let spdx: SPDX = serde_json::from_str(
422 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
423 )
424 .unwrap();
425 assert_eq!(spdx.package_information[0].files_analyzed, Some(true));
426 }
427 #[test]
428 fn package_verification_code() {
429 let spdx: SPDX = serde_json::from_str(
430 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
431 )
432 .unwrap();
433 assert_eq!(
434 spdx.package_information[0].package_verification_code,
435 Some(PackageVerificationCode {
436 value: "d6a770ba38583ed4bb4525bd96e50461655d2758".to_string(),
437 excludes: vec!["./package.spdx".to_string()]
438 })
439 );
440 }
441 #[test]
442 fn package_chekcsum() {
443 let spdx: SPDX = serde_json::from_str(
444 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
445 )
446 .unwrap();
447 assert!(spdx.package_information[0]
448 .package_checksum
449 .contains(&Checksum::new(
450 Algorithm::SHA1,
451 "85ed0817af83a24ad8da68c2b5094de69833983c"
452 )));
453 assert!(spdx.package_information[0]
454 .package_checksum
455 .contains(&Checksum::new(
456 Algorithm::MD5,
457 "624c1abb3664f4b35547e7c73864ad24"
458 )));
459 assert!(spdx.package_information[0]
460 .package_checksum
461 .contains(&Checksum::new(
462 Algorithm::SHA256,
463 "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
464 )));
465 }
466 #[test]
467 fn package_home_page() {
468 let spdx: SPDX = serde_json::from_str(
469 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
470 )
471 .unwrap();
472 assert_eq!(
473 spdx.package_information[0].package_home_page,
474 Some("http://ftp.gnu.org/gnu/glibc".to_string())
475 );
476 }
477 #[test]
478 fn source_information() {
479 let spdx: SPDX = serde_json::from_str(
480 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
481 )
482 .unwrap();
483 assert_eq!(
484 spdx.package_information[0].source_information,
485 Some("uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.".to_string())
486 );
487 }
488 #[test]
489 fn concluded_license() {
490 let spdx: SPDX = serde_json::from_str(
491 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
492 )
493 .unwrap();
494 assert_eq!(
495 spdx.package_information[0]
496 .concluded_license
497 .as_ref()
498 .unwrap()
499 .clone(),
500 SpdxExpression::parse("(LGPL-2.0-only OR LicenseRef-3)").unwrap()
501 );
502 }
503 #[test]
504 fn all_licenses_information_from_files() {
505 let spdx: SPDX = serde_json::from_str(
506 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
507 )
508 .unwrap();
509 assert!(spdx.package_information[0]
510 .all_licenses_information_from_files
511 .contains(&"GPL-2.0-only".to_string()));
512 assert!(spdx.package_information[0]
513 .all_licenses_information_from_files
514 .contains(&"LicenseRef-2".to_string()));
515 assert!(spdx.package_information[0]
516 .all_licenses_information_from_files
517 .contains(&"LicenseRef-1".to_string()));
518 }
519 #[test]
520 fn declared_license() {
521 let spdx: SPDX = serde_json::from_str(
522 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
523 )
524 .unwrap();
525 assert_eq!(
526 spdx.package_information[0]
527 .declared_license
528 .as_ref()
529 .unwrap()
530 .clone(),
531 SpdxExpression::parse("(LGPL-2.0-only AND LicenseRef-3)").unwrap()
532 );
533 }
534 #[test]
535 fn comments_on_license() {
536 let spdx: SPDX = serde_json::from_str(
537 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
538 )
539 .unwrap();
540 assert_eq!(
541 spdx.package_information[0].comments_on_license,
542 Some("The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.".to_string())
543 );
544 }
545 #[test]
546 fn copyright_text() {
547 let spdx: SPDX = serde_json::from_str(
548 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
549 )
550 .unwrap();
551 assert_eq!(
552 spdx.package_information[0]
553 .copyright_text
554 .as_ref()
555 .unwrap()
556 .clone(),
557 "Copyright 2008-2010 John Smith".to_string()
558 );
559 }
560 #[test]
561 fn package_summary_description() {
562 let spdx: SPDX = serde_json::from_str(
563 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
564 )
565 .unwrap();
566 assert_eq!(
567 spdx.package_information[0].package_summary_description,
568 Some("GNU C library.".to_string())
569 );
570 }
571 #[test]
572 fn package_detailed_description() {
573 let spdx: SPDX = serde_json::from_str(
574 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
575 )
576 .unwrap();
577 assert_eq!(
578 spdx.package_information[0].package_detailed_description,
579 Some("The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.".to_string())
580 );
581 }
582 #[test]
583 fn package_comment() {
584 let spdx: SPDX = serde_json::from_str(
585 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
586 )
587 .unwrap();
588 assert_eq!(
589 spdx.package_information[1].package_comment,
590 Some("This package was converted from a DOAP Project by the same name".to_string())
591 );
592 }
593 #[test]
594 fn external_reference() {
595 let spdx: SPDX = serde_json::from_str(
596 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
597 )
598 .unwrap();
599 assert!(
600 spdx.package_information[0].external_reference.contains(&ExternalPackageReference {
601 reference_comment: Some("This is the external ref for Acme".to_string()),
602 reference_category: ExternalPackageReferenceCategory::Other,
603 reference_locator: "acmecorp/acmenator/4.1.3-alpha".to_string(),
604 reference_type: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge".to_string()
605 })
606 );
607 assert!(spdx.package_information[0].external_reference.contains(
608 &ExternalPackageReference {
609 reference_comment: None,
610 reference_category: ExternalPackageReferenceCategory::Security,
611 reference_locator:
612 "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*".to_string(),
613 reference_type: "http://spdx.org/rdf/references/cpe23Type".to_string()
614 }
615 ));
616 }
617 #[test]
618 fn package_attribution_text() {
619 let spdx: SPDX = serde_json::from_str(
620 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
621 )
622 .unwrap();
623 assert!(
624 spdx.package_information[0].package_attribution_text.contains(&"The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.".to_string())
625 );
626 }
627}
628