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