xref: /aosp_15_r20/development/tools/external_crates/license_checker/src/lib.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 std::{
16     collections::{BTreeMap, BTreeSet},
17     fs::read_to_string,
18     path::{Path, PathBuf},
19 };
20 
21 use spdx::LicenseReq;
22 use thiserror::Error;
23 
24 mod content_checker;
25 mod expression_parser;
26 mod file_name_checker;
27 mod license_file_finder;
28 
29 #[derive(Error, Debug)]
30 pub enum LicenseCheckerError {
31     #[error("Couldn't convert filesystem path {} (lossy) to a string for globbing.", .0.to_string_lossy())]
32     PathToString(PathBuf),
33     #[error("Glob error")]
34     GlobError(#[from] glob::GlobError),
35     #[error("Glob pattern error")]
36     PatternError(#[from] glob::PatternError),
37     #[error("Strip prefix error")]
38     StripPrefixError(#[from] std::path::StripPrefixError),
39     #[error("Found a license expression special case for crate {crate_name} but the Cargo.toml license field doesn't match. Expected '{expected_license}', found '{cargo_toml_license}'")]
40     LicenseExpressionSpecialCase {
41         crate_name: String,
42         expected_license: String,
43         cargo_toml_license: String,
44     },
45     #[error("Crate {0} doesn't have a license field in Cargo.toml, and no special case was found for this crate")]
46     MissingLicenseField(String),
47     #[error("SPDX expression parse error")]
48     ParseError(#[from] spdx::ParseError),
49     #[error("SPDX expression minimize error")]
50     MinimizeError(#[from] spdx::expression::MinimizeError),
51     #[error("Unknown license checker error")]
52     Unknown,
53 }
54 
55 #[derive(Debug)]
56 pub struct LicenseState {
57     pub unsatisfied: BTreeSet<LicenseReq>,
58     pub satisfied: BTreeMap<LicenseReq, PathBuf>,
59 }
60 
find_licenses( crate_path: impl AsRef<Path>, crate_name: &str, cargo_toml_license: Option<&str>, ) -> Result<LicenseState, LicenseCheckerError>61 pub fn find_licenses(
62     crate_path: impl AsRef<Path>,
63     crate_name: &str,
64     cargo_toml_license: Option<&str>,
65 ) -> Result<LicenseState, LicenseCheckerError> {
66     let crate_path = crate_path.as_ref();
67     let mut state = LicenseState { unsatisfied: BTreeSet::new(), satisfied: BTreeMap::new() };
68 
69     state.unsatisfied = expression_parser::get_chosen_licenses(crate_name, cargo_toml_license)?;
70     let mut possible_license_files = license_file_finder::find_license_files(crate_path)?;
71 
72     possible_license_files.retain(|file| {
73         if let Some(req) = file_name_checker::classify_license_file_name(file) {
74             if state.unsatisfied.remove(&req) {
75                 state.satisfied.insert(req, file.clone());
76                 return false;
77             }
78         }
79         true
80     });
81 
82     if !state.unsatisfied.is_empty() {
83         possible_license_files.retain(|file| {
84             let contents = read_to_string(crate_path.join(file)).unwrap();
85             if let Some(req) = content_checker::classify_license_file_contents(&contents) {
86                 if state.unsatisfied.remove(&req) {
87                     state.satisfied.insert(req, file.clone());
88                     return false;
89                 }
90             }
91             true
92         });
93     }
94 
95     Ok(state)
96 }
97