spdx_rs/models/
package_information.rs

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}