1 // Copyright (C) 2024 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 use spdx::{Expression, LicenseReq, Licensee};
16 use std::{
17 collections::{BTreeMap, BTreeSet},
18 sync::LazyLock,
19 };
20
21 use crate::LicenseCheckerError;
22
get_chosen_licenses( crate_name: &str, cargo_toml_license: Option<&str>, ) -> Result<BTreeSet<LicenseReq>, LicenseCheckerError>23 pub(crate) fn get_chosen_licenses(
24 crate_name: &str,
25 cargo_toml_license: Option<&str>,
26 ) -> Result<BTreeSet<LicenseReq>, LicenseCheckerError> {
27 Ok(BTreeSet::from_iter(
28 Expression::parse(&get_spdx_expr(crate_name, cargo_toml_license)?)?
29 .minimized_requirements(LICENSE_PREFERENCE.iter())?,
30 ))
31 }
32
get_spdx_expr( crate_name: &str, cargo_toml_license: Option<&str>, ) -> Result<String, LicenseCheckerError>33 fn get_spdx_expr(
34 crate_name: &str,
35 cargo_toml_license: Option<&str>,
36 ) -> Result<String, LicenseCheckerError> {
37 // Check special cases.
38 if let Some((raw, expr)) = LICENSE_EXPR_SPECIAL_CASES.get(crate_name) {
39 if *raw != cargo_toml_license {
40 return Err(LicenseCheckerError::LicenseExpressionSpecialCase {
41 crate_name: crate_name.to_string(),
42 expected_license: raw.unwrap_or_default().to_string(),
43 cargo_toml_license: cargo_toml_license.unwrap_or_default().to_string(),
44 });
45 }
46 return Ok(expr.to_string());
47 }
48 // Default. Look at the license field in Cargo.toml, and treat '/' as OR.
49 if let Some(lic) = cargo_toml_license {
50 if lic.contains('/') {
51 Ok(lic.replace('/', " OR "))
52 } else {
53 Ok(lic.to_string())
54 }
55 } else {
56 Err(LicenseCheckerError::MissingLicenseField(crate_name.to_string()))
57 }
58 }
59
60 static LICENSE_PREFERENCE: LazyLock<Vec<Licensee>> = LazyLock::new(|| {
61 vec![
62 "Apache-2.0",
63 "MIT",
64 "BSD-3-Clause",
65 "BSD-2-Clause",
66 "ISC",
67 "MPL-2.0",
68 "0BSD",
69 "Unlicense",
70 "Zlib",
71 "Unicode-DFS-2016",
72 "NCSA",
73 "OpenSSL",
74 ]
75 .into_iter()
76 .map(|l| Licensee::parse(l).unwrap())
77 .collect()
78 });
79 static LICENSE_EXPR_SPECIAL_CASES: LazyLock<
80 BTreeMap<&'static str, (Option<&'static str>, &'static str)>,
81 > = LazyLock::new(|| {
82 BTreeMap::from([
83 ("libfuzzer-sys", (Some("MIT/Apache-2.0/NCSA"), "(MIT OR Apache-2.0) AND NCSA")),
84 ("ring", (None, "MIT AND ISC AND OpenSSL")),
85 ("webpki", (None, "ISC AND BSD-3-Clause")),
86 ])
87 });
88
89 #[cfg(test)]
90 mod tests {
91 use super::*;
92
93 #[test]
test_get_spdx_expr() -> Result<(), LicenseCheckerError>94 fn test_get_spdx_expr() -> Result<(), LicenseCheckerError> {
95 assert_eq!(get_spdx_expr("foo", Some("MIT"))?, "MIT");
96
97 // No license, no exception
98 assert!(get_spdx_expr("foo", None).is_err());
99
100 // '/' treated as OR
101 assert_eq!(get_spdx_expr("foo", Some("MIT/Apache-2.0"))?, "MIT OR Apache-2.0");
102
103 // Exceptions.
104 assert_eq!(
105 get_spdx_expr("libfuzzer-sys", Some("MIT/Apache-2.0/NCSA"))?,
106 "(MIT OR Apache-2.0) AND NCSA"
107 );
108 assert_eq!(get_spdx_expr("ring", None)?, "MIT AND ISC AND OpenSSL");
109
110 // Exceptions. Raw license in Cargo.toml must match, if present.
111 assert!(get_spdx_expr("libfuzzer-sys", Some("blah")).is_err());
112 assert!(get_spdx_expr("libfuzzer-sys", None).is_err());
113 assert!(get_spdx_expr("ring", Some("blah")).is_err());
114
115 Ok(())
116 }
117
118 #[test]
test_get_chosen_licenses() -> Result<(), LicenseCheckerError>119 fn test_get_chosen_licenses() -> Result<(), LicenseCheckerError> {
120 assert_eq!(
121 get_chosen_licenses("foo", Some("MIT"))?,
122 BTreeSet::from([Licensee::parse("MIT").unwrap().into_req()]),
123 "Simple case"
124 );
125 assert_eq!(
126 get_chosen_licenses("foo", Some("MIT OR Apache-2.0"))?,
127 BTreeSet::from([Licensee::parse("Apache-2.0").unwrap().into_req()]),
128 "Apache preferred to MIT"
129 );
130 assert!(get_chosen_licenses("foo", Some("GPL")).is_err(), "Unacceptable license");
131 assert!(get_chosen_licenses("foo", Some("blah")).is_err(), "Unrecognized license");
132 assert_eq!(
133 get_chosen_licenses("foo", Some("MIT AND Apache-2.0"))?,
134 BTreeSet::from([
135 Licensee::parse("Apache-2.0").unwrap().into_req(),
136 Licensee::parse("MIT").unwrap().into_req()
137 ]),
138 "Apache preferred to MIT"
139 );
140 assert_eq!(
141 get_chosen_licenses("webpki", None)?,
142 BTreeSet::from([
143 Licensee::parse("ISC").unwrap().into_req(),
144 Licensee::parse("BSD-3-Clause").unwrap().into_req()
145 ]),
146 "Exception"
147 );
148 Ok(())
149 }
150 }
151