Skip to main content

scancode_rs/
models.rs

1use std::fs::read_to_string;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use thiserror::Error;
7
8pub(crate) type Result<T> = std::result::Result<T, ScancodeError>;
9
10/// License from [ScanCode](https://github.com/nexB/scancode-toolkit).
11#[derive(Serialize, Deserialize, Debug, PartialEq)]
12#[serde(deny_unknown_fields)]
13pub struct ScancodeLicense {
14    /// Mandatory unique key: lower case ASCII characters, digits, underscore and dots.
15    pub key: String,
16
17    /// Commonly used short name, often abbreviated.
18    pub short_name: String,
19
20    /// Full name.
21    pub name: String,
22
23    /// Permissive, Copyleft, etc.
24    pub category: Category,
25
26    pub owner: String,
27
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub homepage_url: Option<String>,
30
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub notes: Option<String>,
33
34    #[serde(
35        deserialize_with = "bool_from_yes",
36        skip_serializing_if = "is_false",
37        serialize_with = "yes_from_bool"
38    )]
39    #[serde(default)]
40    pub is_deprecated: bool,
41
42    /// SPDX key for SPDX licenses
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub spdx_license_key: Option<String>,
45
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub text_urls: Vec<String>,
48
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub osi_url: Option<String>,
51
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub osi_license_key: Option<String>,
54
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub faq_url: Option<String>,
57
58    #[serde(default, skip_serializing_if = "Vec::is_empty")]
59    pub other_urls: Vec<String>,
60
61    #[serde(
62        deserialize_with = "bool_from_yes",
63        skip_serializing_if = "is_false",
64        serialize_with = "yes_from_bool"
65    )]
66    #[serde(default)]
67    pub is_exception: bool,
68
69    #[serde(default, skip_serializing_if = "Vec::is_empty")]
70    pub other_spdx_license_keys: Vec<String>,
71
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub ignorable_copyrights: Vec<String>,
74
75    #[serde(default, skip_serializing_if = "Vec::is_empty")]
76    pub ignorable_holders: Vec<String>,
77
78    #[serde(default, skip_serializing_if = "Vec::is_empty")]
79    pub ignorable_authors: Vec<String>,
80
81    #[serde(default, skip_serializing_if = "Vec::is_empty")]
82    pub ignorable_urls: Vec<String>,
83
84    #[serde(default, skip_serializing_if = "Vec::is_empty")]
85    pub ignorable_emails: Vec<String>,
86
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub minimum_coverage: Option<i32>,
89
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub standard_notice: Option<String>,
92
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub language: Option<String>,
95
96    #[serde(skip)]
97    pub text: String,
98}
99
100impl ScancodeLicense {
101    /// Deserialize [Self] from YAML files.
102    ///
103    /// Scancode stores the license information in two files, one for the metadata and one for the
104    /// license text. This expects the file for the license text to exist in the same directory as
105    /// the metadata file with same filename but `LICENSE` extension.
106    pub(crate) fn from_yaml_file<P: AsRef<Path>>(yaml_path: P) -> Result<Self> {
107        let license_string =
108            read_to_string(yaml_path.as_ref()).map_err(|err| ScancodeError::Io {
109                source: err,
110                path: yaml_path.as_ref().to_path_buf(),
111            })?;
112        let mut license = serde_yaml::from_str::<Self>(&license_string).map_err(|err| {
113            ScancodeError::SerdeYaml {
114                source: err,
115                path: yaml_path.as_ref().to_path_buf(),
116            }
117        })?;
118        let text_path = yaml_path.as_ref().with_extension("LICENSE");
119        let license_text = read_to_string(&text_path).map_err(|err| ScancodeError::Io {
120            source: err,
121            path: text_path,
122        });
123        license.text = match license_text {
124            Ok(value) => value,
125            Err(_) => "".into(),
126        };
127
128        Ok(license)
129    }
130}
131
132/// Different license cateogires from ScanCode.
133#[derive(Serialize, Deserialize, Debug, PartialEq)]
134pub enum Category {
135    Copyleft,
136    #[serde(rename = "Copyleft Limited")]
137    CopyleftLimited,
138    #[serde(rename = "Patent License")]
139    PatentLicense,
140    Permissive,
141    #[serde(rename = "Public Domain")]
142    PublicDomain,
143    Commercial,
144    #[serde(rename = "Proprietary Free")]
145    ProprietaryFree,
146    #[serde(rename = "Free Restricted")]
147    FreeRestricted,
148    #[serde(rename = "Source-available")]
149    SourceAvailable,
150    #[serde(rename = "Unstated License")]
151    UnstatedLicense,
152}
153
154/// Deserialize string "yes" as boolean true.
155fn bool_from_yes<'de, D>(deserializer: D) -> std::result::Result<bool, D::Error>
156where
157    D: Deserializer<'de>,
158{
159    let s = String::deserialize(deserializer)?;
160    match s.as_str() {
161        "yes" => Ok(true),
162        _ => Ok(false),
163    }
164}
165
166/// Serialize a boolean true as "yes".
167#[allow(clippy::trivially_copy_pass_by_ref)]
168fn yes_from_bool<S>(boolean: &bool, s: S) -> std::result::Result<S::Ok, S::Error>
169where
170    S: Serializer,
171{
172    match boolean {
173        true => s.serialize_str("yes"),
174        false => s.serialize_str("no"),
175    }
176}
177
178/// Check if false.
179#[allow(clippy::trivially_copy_pass_by_ref)]
180fn is_false(boolean: &bool) -> bool {
181    boolean == &false
182}
183
184#[cfg(test)]
185mod tests {
186    use std::{fs::read_to_string, path::PathBuf};
187
188    use pretty_assertions::assert_eq;
189    use yaml_rust::YamlLoader;
190
191    use crate::import::{from_git_database, from_scancode_database};
192
193    use super::*;
194
195    fn test_data_directory() -> PathBuf {
196        let directory = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
197        directory.join("tests/data")
198    }
199
200    #[test]
201    fn deseralize_test_licenses() -> Result<()> {
202        let paths = std::fs::read_dir(test_data_directory().join("licenses")).unwrap();
203
204        for path in paths {
205            let path = path.unwrap();
206            if path.path().extension().unwrap() == "yml" {
207                ScancodeLicense::from_yaml_file(&path.path())?;
208            }
209        }
210        Ok(())
211    }
212
213    #[test]
214    fn serialize_test_licenses_immutably() -> Result<()> {
215        let paths = std::fs::read_dir(test_data_directory().join("licenses")).unwrap();
216
217        for path in paths {
218            let path = path.unwrap();
219            if path.path().extension().unwrap() == "yml" {
220                let license_string = read_to_string(path.path()).unwrap();
221                let license_yaml = YamlLoader::load_from_str(&license_string).unwrap();
222                let license = ScancodeLicense::from_yaml_file(&path.path())?;
223                let serialized_yaml =
224                    YamlLoader::load_from_str(&serde_yaml::to_string(&license).unwrap()).unwrap();
225                assert_eq!(license_yaml, serialized_yaml);
226            }
227        }
228
229        Ok(())
230    }
231
232    #[test]
233    fn deserialize_correct_values() {
234        let test_licenses_dir = test_data_directory().join("licenses");
235        let actual_license: ScancodeLicense =
236            serde_yaml::from_str(&read_to_string(test_licenses_dir.join("gpl-3.0.yml")).unwrap())
237                .unwrap();
238
239        let expected_license = ScancodeLicense {
240            key: "gpl-3.0".into(),
241            short_name: "GPL 3.0".into(),
242            name: "GNU General Public License 3.0".into(),
243            category: Category::Copyleft,
244            owner: "Free Software Foundation (FSF)".into(),
245            homepage_url: Some("http://www.gnu.org/licenses/gpl-3.0.html".into()),
246            notes: Some("notes from SPDX:\nThis license was released: 29 June 2007 This license is OSI certified.".into()),
247            spdx_license_key: Some("GPL-3.0".into()),
248            text_urls: vec![
249                "http://www.gnu.org/licenses/gpl-3.0.txt".into(),
250                "http://www.gnu.org/licenses/gpl-3.0-standalone.html".into()],
251            osi_url: Some("http://opensource.org/licenses/gpl-3.0.html".into()),
252            faq_url: Some("http://www.gnu.org/licenses/gpl-faq.html".into()),
253            other_urls: vec![
254                "http://www.gnu.org/licenses/quick-guide-gplv3.html".into()],
255            is_exception: false,
256            is_deprecated: false,
257            minimum_coverage: None,
258            ignorable_copyrights: Vec::new(),
259            ignorable_holders: Vec::new(),
260            ignorable_authors: Vec::new(),
261            ignorable_urls: Vec::new(),
262            ignorable_emails: Vec::new(),
263            other_spdx_license_keys: Vec::new(),
264            standard_notice: None,
265            osi_license_key: None,
266            language: None,
267            text: "".to_string()
268        };
269
270        assert_eq!(actual_license, expected_license);
271
272        let actual_exception: ScancodeLicense = serde_yaml::from_str(
273            &read_to_string(test_licenses_dir.join("gpl-2.0-library.yml")).unwrap(),
274        )
275        .unwrap();
276
277        let expected_exception = ScancodeLicense {
278            key: "gpl-2.0-library".into(),
279            short_name: "GPL 2.0 with Library exception".into(),
280            name: "GNU General Public License 2.0 with Library exception".into(),
281            category: Category::CopyleftLimited,
282            owner: "Grammatica".into(),
283            homepage_url: None,
284            notes: None,
285            spdx_license_key: None,
286            text_urls: Vec::new(),
287            osi_url: None,
288            faq_url: None,
289            other_urls: vec!["http://grammatica.percederberg.net/index.html".into()],
290            is_exception: true,
291            is_deprecated: false,
292            minimum_coverage: None,
293            ignorable_copyrights: Vec::new(),
294            ignorable_holders: Vec::new(),
295            ignorable_authors: Vec::new(),
296            ignorable_urls: Vec::new(),
297            ignorable_emails: Vec::new(),
298            other_spdx_license_keys: Vec::new(),
299            standard_notice: None,
300            osi_license_key: None,
301            language: None,
302            text: "".to_string(),
303        };
304
305        assert_eq!(actual_exception, expected_exception);
306    }
307
308    #[test]
309    fn deserialize_all_licenses_from_scancode_licensedb() -> Result<()> {
310        let licenses = from_scancode_database()?;
311
312        assert!(licenses.len() > 1000);
313
314        Ok(())
315    }
316
317    #[test]
318    #[ignore = "currently contains invalid yaml"]
319    fn deserialize_all_licenses_from_scancode_source() -> Result<()> {
320        let licenses = from_git_database(
321            "https://github.com/nexB/scancode-toolkit.git",
322            "src/licensedcode/data/licenses",
323        )?;
324
325        assert!(licenses.len() > 1000);
326
327        Ok(())
328    }
329}
330
331/// Error while interacting with ScanCode items.
332#[derive(Debug, Error)]
333pub enum ScancodeError {
334    #[error("Error with path {path:?}.")]
335    Io {
336        source: std::io::Error,
337        path: PathBuf,
338    },
339
340    #[error("Error with IO.")]
341    OtherIo(#[from] std::io::Error),
342
343    #[error("Error with git.")]
344    Git(#[from] git2::Error),
345
346    #[error("SerdeYaml error with path {path:?}.")]
347    SerdeYaml {
348        source: serde_yaml::Error,
349        path: PathBuf,
350    },
351
352    #[error("Error with serde_yaml.")]
353    OtherSerdeYaml(#[from] serde_yaml::Error),
354}