xref: /aosp_15_r20/development/tools/external_crates/license_checker/src/expression_parser.rs (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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