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 PackageManager,
303 PersistentID,
304 Other,
305}
306
307#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)]
308#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
309pub enum PrimaryPackagePurpose {
310 Application,
311 Framework,
312 Library,
313 Container,
314 OperatingSystem,
315 Device,
316 Firmware,
317 Source,
318 Archive,
319 File,
320 Install,
321 Other,
322}
323
324#[cfg(test)]
325mod test {
326 use std::fs::read_to_string;
327
328 use crate::models::{Algorithm, SPDX};
329
330 use super::*;
331
332 #[test]
333 fn all_packages_are_deserialized() {
334 let spdx: SPDX = serde_json::from_str(
335 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
336 )
337 .unwrap();
338 assert_eq!(spdx.package_information.len(), 4);
339 }
340 #[test]
341 fn package_name() {
342 let spdx: SPDX = serde_json::from_str(
343 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
344 )
345 .unwrap();
346 assert_eq!(
347 spdx.package_information[0].package_name,
348 "glibc".to_string()
349 );
350 }
351 #[test]
352 fn package_spdx_identifier() {
353 let spdx: SPDX = serde_json::from_str(
354 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
355 )
356 .unwrap();
357 assert_eq!(
358 spdx.package_information[0].package_spdx_identifier,
359 "SPDXRef-Package".to_string()
360 );
361 }
362 #[test]
363 fn package_version() {
364 let spdx: SPDX = serde_json::from_str(
365 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
366 )
367 .unwrap();
368 assert_eq!(
369 spdx.package_information[0].package_version,
370 Some("2.11.1".to_string())
371 );
372 }
373 #[test]
374 fn package_file_name() {
375 let spdx: SPDX = serde_json::from_str(
376 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
377 )
378 .unwrap();
379 assert_eq!(
380 spdx.package_information[0].package_file_name,
381 Some("glibc-2.11.1.tar.gz".to_string())
382 );
383 }
384 #[test]
385 fn package_supplier() {
386 let spdx: SPDX = serde_json::from_str(
387 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
388 )
389 .unwrap();
390 assert_eq!(
391 spdx.package_information[0].package_supplier,
392 Some("Person: Jane Doe (jane.doe@example.com)".to_string())
393 );
394 }
395 #[test]
396 fn package_originator() {
397 let spdx: SPDX = serde_json::from_str(
398 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
399 )
400 .unwrap();
401 assert_eq!(
402 spdx.package_information[0].package_originator,
403 Some("Organization: ExampleCodeInspect (contact@example.com)".to_string())
404 );
405 }
406 #[test]
407 fn package_download_location() {
408 let spdx: SPDX = serde_json::from_str(
409 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
410 )
411 .unwrap();
412 assert_eq!(
413 spdx.package_information[0].package_download_location,
414 "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz".to_string()
415 );
416 }
417 #[test]
418 fn files_analyzed() {
419 let spdx: SPDX = serde_json::from_str(
420 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
421 )
422 .unwrap();
423 assert_eq!(spdx.package_information[0].files_analyzed, Some(true));
424 }
425 #[test]
426 fn package_verification_code() {
427 let spdx: SPDX = serde_json::from_str(
428 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
429 )
430 .unwrap();
431 assert_eq!(
432 spdx.package_information[0].package_verification_code,
433 Some(PackageVerificationCode {
434 value: "d6a770ba38583ed4bb4525bd96e50461655d2758".to_string(),
435 excludes: vec!["./package.spdx".to_string()]
436 })
437 );
438 }
439 #[test]
440 fn package_chekcsum() {
441 let spdx: SPDX = serde_json::from_str(
442 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
443 )
444 .unwrap();
445 assert!(spdx.package_information[0]
446 .package_checksum
447 .contains(&Checksum::new(
448 Algorithm::SHA1,
449 "85ed0817af83a24ad8da68c2b5094de69833983c"
450 )));
451 assert!(spdx.package_information[0]
452 .package_checksum
453 .contains(&Checksum::new(
454 Algorithm::MD5,
455 "624c1abb3664f4b35547e7c73864ad24"
456 )));
457 assert!(spdx.package_information[0]
458 .package_checksum
459 .contains(&Checksum::new(
460 Algorithm::SHA256,
461 "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
462 )));
463 }
464 #[test]
465 fn package_home_page() {
466 let spdx: SPDX = serde_json::from_str(
467 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
468 )
469 .unwrap();
470 assert_eq!(
471 spdx.package_information[0].package_home_page,
472 Some("http://ftp.gnu.org/gnu/glibc".to_string())
473 );
474 }
475 #[test]
476 fn source_information() {
477 let spdx: SPDX = serde_json::from_str(
478 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
479 )
480 .unwrap();
481 assert_eq!(
482 spdx.package_information[0].source_information,
483 Some("uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.".to_string())
484 );
485 }
486 #[test]
487 fn concluded_license() {
488 let spdx: SPDX = serde_json::from_str(
489 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
490 )
491 .unwrap();
492 assert_eq!(
493 spdx.package_information[0]
494 .concluded_license
495 .as_ref()
496 .unwrap()
497 .clone(),
498 SpdxExpression::parse("(LGPL-2.0-only OR LicenseRef-3)").unwrap()
499 );
500 }
501 #[test]
502 fn all_licenses_information_from_files() {
503 let spdx: SPDX = serde_json::from_str(
504 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
505 )
506 .unwrap();
507 assert!(spdx.package_information[0]
508 .all_licenses_information_from_files
509 .contains(&"GPL-2.0-only".to_string()));
510 assert!(spdx.package_information[0]
511 .all_licenses_information_from_files
512 .contains(&"LicenseRef-2".to_string()));
513 assert!(spdx.package_information[0]
514 .all_licenses_information_from_files
515 .contains(&"LicenseRef-1".to_string()));
516 }
517 #[test]
518 fn declared_license() {
519 let spdx: SPDX = serde_json::from_str(
520 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
521 )
522 .unwrap();
523 assert_eq!(
524 spdx.package_information[0]
525 .declared_license
526 .as_ref()
527 .unwrap()
528 .clone(),
529 SpdxExpression::parse("(LGPL-2.0-only AND LicenseRef-3)").unwrap()
530 );
531 }
532 #[test]
533 fn comments_on_license() {
534 let spdx: SPDX = serde_json::from_str(
535 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
536 )
537 .unwrap();
538 assert_eq!(
539 spdx.package_information[0].comments_on_license,
540 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())
541 );
542 }
543 #[test]
544 fn copyright_text() {
545 let spdx: SPDX = serde_json::from_str(
546 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
547 )
548 .unwrap();
549 assert_eq!(
550 spdx.package_information[0]
551 .copyright_text
552 .as_ref()
553 .unwrap()
554 .clone(),
555 "Copyright 2008-2010 John Smith".to_string()
556 );
557 }
558 #[test]
559 fn package_summary_description() {
560 let spdx: SPDX = serde_json::from_str(
561 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
562 )
563 .unwrap();
564 assert_eq!(
565 spdx.package_information[0].package_summary_description,
566 Some("GNU C library.".to_string())
567 );
568 }
569 #[test]
570 fn package_detailed_description() {
571 let spdx: SPDX = serde_json::from_str(
572 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
573 )
574 .unwrap();
575 assert_eq!(
576 spdx.package_information[0].package_detailed_description,
577 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())
578 );
579 }
580 #[test]
581 fn package_comment() {
582 let spdx: SPDX = serde_json::from_str(
583 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
584 )
585 .unwrap();
586 assert_eq!(
587 spdx.package_information[1].package_comment,
588 Some("This package was converted from a DOAP Project by the same name".to_string())
589 );
590 }
591 #[test]
592 fn external_reference() {
593 let spdx: SPDX = serde_json::from_str(
594 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
595 )
596 .unwrap();
597 assert!(
598 spdx.package_information[0].external_reference.contains(&ExternalPackageReference {
599 reference_comment: Some("This is the external ref for Acme".to_string()),
600 reference_category: ExternalPackageReferenceCategory::Other,
601 reference_locator: "acmecorp/acmenator/4.1.3-alpha".to_string(),
602 reference_type: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge".to_string()
603 })
604 );
605 assert!(spdx.package_information[0].external_reference.contains(
606 &ExternalPackageReference {
607 reference_comment: None,
608 reference_category: ExternalPackageReferenceCategory::Security,
609 reference_locator:
610 "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*".to_string(),
611 reference_type: "http://spdx.org/rdf/references/cpe23Type".to_string()
612 }
613 ));
614 }
615 #[test]
616 fn package_attribution_text() {
617 let spdx: SPDX = serde_json::from_str(
618 &read_to_string("tests/data/SPDXJSONExample-v2.2.spdx.json").unwrap(),
619 )
620 .unwrap();
621 assert!(
622 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())
623 );
624 }
625}
626