spdx_toolkit/
license_list.rs

1// SPDX-FileCopyrightText: 2020 HH Partners
2//
3// SPDX-License-Identifier: MIT
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::Error;
8
9#[derive(Serialize, Deserialize, Debug)]
10#[serde(rename_all = "camelCase", deny_unknown_fields)]
11pub struct LicenseList {
12    pub license_list_version: String,
13    #[serde(default)]
14    pub licenses: Vec<License>,
15    #[serde(default)]
16    pub exceptions: Vec<Exception>,
17    pub release_date: String,
18}
19
20impl LicenseList {
21    /// Get [`LicenseList`] from GitHub. Specify version as `Some("v3.14")` or use None for the
22    /// latest version.
23    ///
24    /// # Errors
25    ///
26    /// Returns [`SpdxError`] if there is a problem with retrieving the license list from GitHub
27    /// or if deserializing the data fails.
28    pub fn from_github(version: Option<&str>) -> Result<Self, Error> {
29        let version = version.unwrap_or("master");
30
31        let licenses_url = format!(
32            "https://raw.githubusercontent.com/spdx/license-list-data/{version}/json/licenses.json"
33        );
34        let body = reqwest::blocking::get(licenses_url)?.text()?;
35        let mut license_list: Self = serde_json::from_str(&body)?;
36
37        let exceptions_url =
38            format!("https://raw.githubusercontent.com/spdx/license-list-data/{version}/json/exceptions.json");
39        let body = reqwest::blocking::get(exceptions_url)?.text()?;
40        let exceptions_list: Self = serde_json::from_str(&body)?;
41        license_list.exceptions = exceptions_list.exceptions;
42        Ok(license_list)
43    }
44
45    pub fn includes_license(&self, spdx_id: &str) -> bool {
46        self.licenses
47            .iter()
48            .any(|license| license.license_id == spdx_id)
49    }
50
51    pub fn includes_exception(&self, exception_id: &str) -> bool {
52        self.exceptions
53            .iter()
54            .any(|exception| exception.license_exception_id == exception_id)
55    }
56
57    #[allow(clippy::doc_markdown)]
58    /// Return true if the input expression is a license on the SPDX license list, a LicenseRef
59    /// license, a `DocumentRef` license, `NONE` or `NOASSERTION`.
60    pub fn is_valid_license(&self, expression: &str) -> bool {
61        expression == "NOASSERTION"
62            || expression == "NONE"
63            || expression.starts_with("LicenseRef-")
64            || expression.starts_with("DocumentRef-")
65            || self.includes_license(&expression.replace('+', ""))
66            || self.includes_exception(expression)
67    }
68}
69
70#[derive(Serialize, Deserialize, Debug)]
71#[serde(rename_all = "camelCase", deny_unknown_fields)]
72pub struct License {
73    pub reference: String,
74    pub is_deprecated_license_id: bool,
75    pub details_url: String,
76    pub reference_number: i32,
77    pub name: String,
78    pub license_id: String,
79    pub see_also: Vec<String>,
80    pub is_osi_approved: bool,
81    #[serde(default)]
82    pub is_fsf_libre: bool,
83}
84
85#[derive(Serialize, Deserialize, Debug)]
86#[serde(rename_all = "camelCase", deny_unknown_fields)]
87pub struct Exception {
88    pub reference: String,
89    pub is_deprecated_license_id: bool,
90    pub details_url: String,
91    pub reference_number: i32,
92    pub name: String,
93    pub license_exception_id: String,
94    pub see_also: Vec<String>,
95}
96
97#[cfg(test)]
98mod test_license_list {
99    use std::fs::read_to_string;
100
101    use super::*;
102
103    #[test]
104    fn licenses_deserialization_works() {
105        let licenses_file =
106            read_to_string("tests/data/licenses.json").expect("Should always exist");
107        let _license_list: LicenseList =
108            serde_json::from_str(&licenses_file).expect("Deseralization should work.");
109    }
110
111    #[test]
112    fn exceptions_deserialization_works() {
113        let licenses_file =
114            read_to_string("tests/data/exceptions.json").expect("Should always exist");
115        let _license_list: LicenseList =
116            serde_json::from_str(&licenses_file).expect("Deseralization should work.");
117    }
118
119    #[test]
120    fn from_github_works() {
121        let license_list = LicenseList::from_github(None).unwrap();
122
123        assert!(!license_list.licenses.is_empty());
124        assert!(!license_list.exceptions.is_empty());
125    }
126
127    #[test]
128    fn correctly_get_older_version_from_github() {
129        let license_list = LicenseList::from_github(Some("v3.14")).unwrap();
130
131        assert!(!license_list.licenses.is_empty());
132        assert!(!license_list.exceptions.is_empty());
133
134        assert_eq!(license_list.license_list_version, "3.14".to_string());
135    }
136
137    #[test]
138    fn bsd_works() {
139        let license_list = LicenseList::from_github(None).unwrap();
140
141        assert!(!license_list.includes_license("BSD"));
142        assert!(!license_list.includes_exception("BSD"));
143    }
144
145    #[test]
146    fn correctly_determine_validity_of_licenses() {
147        let license_list = LicenseList::from_github(None).unwrap();
148        assert!(license_list.is_valid_license("MIT"));
149        assert!(license_list.is_valid_license("GPL-2.0-or-later"));
150        assert!(!license_list.is_valid_license("invalid-license"));
151        assert!(license_list.is_valid_license("LicenseRef-license1"));
152        assert!(license_list.is_valid_license("DocumentRef-document:LicenseRef-license1"));
153        assert!(license_list.is_valid_license("NONE"));
154        assert!(license_list.is_valid_license("NOASSERTION"));
155    }
156}