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,
17 ffi::{OsStr, OsString},
18 path::Path,
19 sync::LazyLock,
20 };
21
22 use spdx::{LicenseReq, Licensee};
23
classify_license_file_name(file: impl AsRef<Path>) -> Option<LicenseReq>24 pub(crate) fn classify_license_file_name(file: impl AsRef<Path>) -> Option<LicenseReq> {
25 // File should be relative
26 let file = file.as_ref();
27 let mut basename = if file.extension() == Some(OsStr::new("txt"))
28 || file.extension() == Some(OsStr::new("md"))
29 {
30 file.with_extension("")
31 } else {
32 file.to_owned()
33 };
34 let uppercase_name = basename.as_mut_os_string();
35 uppercase_name.make_ascii_uppercase();
36 if let Some(req) = LICENSE_FILE_NAME_CLASSIFICATION.get(uppercase_name) {
37 return Some(req.clone());
38 }
39 None
40 }
41
42 static LICENSE_FILE_NAME_CLASSIFICATION: LazyLock<BTreeMap<OsString, LicenseReq>> = LazyLock::new(
43 ||
44 // Filenames are case-insensitive.
45 vec![
46 ("LICENSE-MIT", "MIT"),
47 ("LICENSES/MIT", "MIT"),
48
49 ("LICENSE-APACHE", "Apache-2.0"),
50 ("LICENSE-APACHE-2.0", "Apache-2.0"),
51 ("LICENSES/Apache-2.0", "Apache-2.0"),
52
53 ("LICENSE-BSD-3-Clause", "BSD-3-Clause"),
54
55 ("LICENSE-UNICODE", "Unicode-DFS-2016"),
56
57 ("LICENSE-0BSD", "0BSD"),
58
59 ("LICENSE-ZLIB", "Zlib"),
60
61 ("UNLICENSE", "Unlicense"),
62 ]
63 .into_iter()
64 .map(|(file, req)| (OsString::from(file.to_uppercase()), Licensee::parse(req).unwrap().into_req()))
65 .collect(),
66 );
67
68 #[cfg(test)]
69 mod tests {
70 use super::*;
71
72 #[test]
test_classify()73 fn test_classify() {
74 assert!(classify_license_file_name(Path::new("LICENSE")).is_none(), "Generic license name");
75 assert_eq!(
76 classify_license_file_name(Path::new("UNLICENSE")),
77 Some(Licensee::parse("Unlicense").unwrap().into_req()),
78 "Unlicense"
79 );
80 assert_eq!(
81 classify_license_file_name(Path::new("LICENSE-MIT")),
82 Some(Licensee::parse("MIT").unwrap().into_req()),
83 "Standard name"
84 );
85 assert_eq!(
86 classify_license_file_name(Path::new("LICENSE-APACHE")),
87 Some(Licensee::parse("Apache-2.0").unwrap().into_req()),
88 "Standard name"
89 );
90 assert_eq!(
91 classify_license_file_name(Path::new("LICENSE-apache")),
92 Some(Licensee::parse("Apache-2.0").unwrap().into_req()),
93 "Case-insensitive"
94 );
95 assert_eq!(
96 classify_license_file_name(Path::new("LICENSE-APACHE.md")),
97 Some(Licensee::parse("Apache-2.0").unwrap().into_req()),
98 ".md extension ignored"
99 );
100 assert_eq!(
101 classify_license_file_name(Path::new("LICENSE-0BSD.txt")),
102 Some(Licensee::parse("0BSD").unwrap().into_req()),
103 ".txt extension ignored"
104 );
105 assert!(
106 classify_license_file_name(Path::new("LICENSE-0BSD.jpg")).is_none(),
107 "Random extension preserved"
108 );
109 assert_eq!(
110 classify_license_file_name(Path::new("LICENSES/MIT")),
111 Some(Licensee::parse("MIT").unwrap().into_req()),
112 "Subdirectory"
113 );
114 assert_eq!(
115 classify_license_file_name(Path::new("LICENSES/Apache-2.0")),
116 Some(Licensee::parse("Apache-2.0").unwrap().into_req()),
117 "Subdirectory"
118 );
119 }
120 }
121