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#[derive(Serialize, Deserialize, Debug, PartialEq)]
12#[serde(deny_unknown_fields)]
13pub struct ScancodeLicense {
14 pub key: String,
16
17 pub short_name: String,
19
20 pub name: String,
22
23 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 #[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 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#[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
154fn 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#[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#[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#[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}