| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | |
| 11 | |
| 12 | |
| 13 | |
| 14 | |
| 15 | #[derive(Debug, Clone, Copy)] |
| 16 | pub(crate) struct LicenseInfo { |
| 17 | |
| 18 | pub name: &'static str, |
| 19 | |
| 20 | pub spdx: &'static str, |
| 21 | |
| 22 | pub description: &'static str, |
| 23 | |
| 24 | pub permissions: &'static [&'static str], |
| 25 | |
| 26 | pub conditions: &'static [&'static str], |
| 27 | |
| 28 | pub limitations: &'static [&'static str], |
| 29 | } |
| 30 | |
| 31 | |
| 32 | |
| 33 | pub(crate) fn is_license_path(path: &str) -> bool { |
| 34 | let name = path.rsplit('/').next().unwrap_or(path).to_ascii_lowercase(); |
| 35 | let stem = name.split('.').next().unwrap_or(&name); |
| 36 | matches!( |
| 37 | stem, |
| 38 | "license" | "licence" | "copying" | "copyright" | "unlicense" |
| 39 | ) |
| 40 | } |
| 41 | |
| 42 | |
| 43 | |
| 44 | pub(crate) fn detect(content: &str) -> Option<&'static LicenseInfo> { |
| 45 | |
| 46 | let normalized: String = content |
| 47 | .to_ascii_lowercase() |
| 48 | .split_whitespace() |
| 49 | .collect::<Vec<_>>() |
| 50 | .join(" "); |
| 51 | let has = |needle: &str| normalized.contains(needle); |
| 52 | |
| 53 | |
| 54 | if has("gnu affero general public license") { |
| 55 | return Some(&AGPL_3_0); |
| 56 | } |
| 57 | if has("gnu lesser general public license") { |
| 58 | return Some(&LGPL_3_0); |
| 59 | } |
| 60 | if has("gnu general public license") { |
| 61 | if has("version 2") { |
| 62 | return Some(&GPL_2_0); |
| 63 | } |
| 64 | return Some(&GPL_3_0); |
| 65 | } |
| 66 | if has("mozilla public license version 2.0") { |
| 67 | return Some(&MPL_2_0); |
| 68 | } |
| 69 | if has("apache license") && has("version 2.0") { |
| 70 | return Some(&APACHE_2_0); |
| 71 | } |
| 72 | if has("this is free and unencumbered software released into the public domain") { |
| 73 | return Some(&UNLICENSE); |
| 74 | } |
| 75 | if has("permission to use, copy, modify, and/or distribute this software") { |
| 76 | |
| 77 | if has("provided that the above copyright") { |
| 78 | return Some(&ISC); |
| 79 | } |
| 80 | return Some(&BSD_0); |
| 81 | } |
| 82 | if has("redistribution and use in source and binary forms") { |
| 83 | if has("neither the name") { |
| 84 | return Some(&BSD_3_CLAUSE); |
| 85 | } |
| 86 | return Some(&BSD_2_CLAUSE); |
| 87 | } |
| 88 | if has("permission is hereby granted, free of charge") { |
| 89 | return Some(&MIT); |
| 90 | } |
| 91 | None |
| 92 | } |
| 93 | |
| 94 | |
| 95 | const COMMERCIAL: &str = "Commercial use"; |
| 96 | const MODIFY: &str = "Modification"; |
| 97 | const DISTRIBUTE: &str = "Distribution"; |
| 98 | const PRIVATE: &str = "Private use"; |
| 99 | const PATENT: &str = "Patent use"; |
| 100 | const LIABILITY: &str = "Liability"; |
| 101 | const WARRANTY: &str = "Warranty"; |
| 102 | const TRADEMARK: &str = "Trademark use"; |
| 103 | const KEEP_NOTICE: &str = "License and copyright notice"; |
| 104 | const STATE_CHANGES: &str = "State changes"; |
| 105 | const DISCLOSE_SOURCE: &str = "Disclose source"; |
| 106 | const SAME_LICENSE: &str = "Same license"; |
| 107 | const NETWORK_DISCLOSE: &str = "Network use is distribution"; |
| 108 | |
| 109 | const PERMISSIVE: &[&str] = &[COMMERCIAL, MODIFY, DISTRIBUTE, PRIVATE]; |
| 110 | const PERMISSIVE_PATENT: &[&str] = &[COMMERCIAL, MODIFY, DISTRIBUTE, PATENT, PRIVATE]; |
| 111 | const NO_WARRANTY: &[&str] = &[LIABILITY, WARRANTY]; |
| 112 | |
| 113 | const MIT: LicenseInfo = LicenseInfo { |
| 114 | name: "MIT License", |
| 115 | spdx: "MIT", |
| 116 | description: "A short, permissive license: do almost anything, provided the copyright and license notice are preserved.", |
| 117 | permissions: PERMISSIVE, |
| 118 | conditions: &[KEEP_NOTICE], |
| 119 | limitations: NO_WARRANTY, |
| 120 | }; |
| 121 | |
| 122 | const APACHE_2_0: LicenseInfo = LicenseInfo { |
| 123 | name: "Apache License 2.0", |
| 124 | spdx: "Apache-2.0", |
| 125 | description: "A permissive license with an express grant of patent rights and requirements to note changes.", |
| 126 | permissions: PERMISSIVE_PATENT, |
| 127 | conditions: &[KEEP_NOTICE, STATE_CHANGES], |
| 128 | limitations: &[TRADEMARK, LIABILITY, WARRANTY], |
| 129 | }; |
| 130 | |
| 131 | const BSD_2_CLAUSE: LicenseInfo = LicenseInfo { |
| 132 | name: "BSD 2-Clause License", |
| 133 | spdx: "BSD-2-Clause", |
| 134 | description: "A permissive license that requires preservation of the copyright and license notices.", |
| 135 | permissions: PERMISSIVE, |
| 136 | conditions: &[KEEP_NOTICE], |
| 137 | limitations: NO_WARRANTY, |
| 138 | }; |
| 139 | |
| 140 | const BSD_3_CLAUSE: LicenseInfo = LicenseInfo { |
| 141 | name: "BSD 3-Clause License", |
| 142 | spdx: "BSD-3-Clause", |
| 143 | description: "A permissive license like BSD 2-Clause, also forbidding use of contributors' names to endorse derivatives.", |
| 144 | permissions: PERMISSIVE, |
| 145 | conditions: &[KEEP_NOTICE], |
| 146 | limitations: NO_WARRANTY, |
| 147 | }; |
| 148 | |
| 149 | const BSD_0: LicenseInfo = LicenseInfo { |
| 150 | name: "BSD Zero Clause License", |
| 151 | spdx: "0BSD", |
| 152 | description: "A public-domain-equivalent license with no attribution requirement whatsoever.", |
| 153 | permissions: PERMISSIVE, |
| 154 | conditions: &[], |
| 155 | limitations: NO_WARRANTY, |
| 156 | }; |
| 157 | |
| 158 | const ISC: LicenseInfo = LicenseInfo { |
| 159 | name: "ISC License", |
| 160 | spdx: "ISC", |
| 161 | description: "A permissive license functionally equivalent to MIT/BSD 2-Clause, with simpler language.", |
| 162 | permissions: PERMISSIVE, |
| 163 | conditions: &[KEEP_NOTICE], |
| 164 | limitations: NO_WARRANTY, |
| 165 | }; |
| 166 | |
| 167 | const MPL_2_0: LicenseInfo = LicenseInfo { |
| 168 | name: "Mozilla Public License 2.0", |
| 169 | spdx: "MPL-2.0", |
| 170 | description: "A file-level copyleft license: modified files must stay open, but they can be combined with proprietary code.", |
| 171 | permissions: PERMISSIVE_PATENT, |
| 172 | conditions: &[DISCLOSE_SOURCE, KEEP_NOTICE, SAME_LICENSE], |
| 173 | limitations: &[TRADEMARK, LIABILITY, WARRANTY], |
| 174 | }; |
| 175 | |
| 176 | const GPL_2_0: LicenseInfo = LicenseInfo { |
| 177 | name: "GNU General Public License v2.0", |
| 178 | spdx: "GPL-2.0", |
| 179 | description: "A strong copyleft license: derivatives must be released under the same terms with source available.", |
| 180 | permissions: PERMISSIVE, |
| 181 | conditions: &[DISCLOSE_SOURCE, KEEP_NOTICE, SAME_LICENSE, STATE_CHANGES], |
| 182 | limitations: NO_WARRANTY, |
| 183 | }; |
| 184 | |
| 185 | const GPL_3_0: LicenseInfo = LicenseInfo { |
| 186 | name: "GNU General Public License v3.0", |
| 187 | spdx: "GPL-3.0", |
| 188 | description: "A strong copyleft license with a patent grant: derivatives must stay open under the same terms.", |
| 189 | permissions: PERMISSIVE_PATENT, |
| 190 | conditions: &[DISCLOSE_SOURCE, KEEP_NOTICE, SAME_LICENSE, STATE_CHANGES], |
| 191 | limitations: NO_WARRANTY, |
| 192 | }; |
| 193 | |
| 194 | const LGPL_3_0: LicenseInfo = LicenseInfo { |
| 195 | name: "GNU Lesser General Public License v3.0", |
| 196 | spdx: "LGPL-3.0", |
| 197 | description: "A weaker copyleft: the library stays open under the same terms, but may be linked from other software.", |
| 198 | permissions: PERMISSIVE_PATENT, |
| 199 | conditions: &[DISCLOSE_SOURCE, KEEP_NOTICE, SAME_LICENSE, STATE_CHANGES], |
| 200 | limitations: NO_WARRANTY, |
| 201 | }; |
| 202 | |
| 203 | const AGPL_3_0: LicenseInfo = LicenseInfo { |
| 204 | name: "GNU Affero General Public License v3.0", |
| 205 | spdx: "AGPL-3.0", |
| 206 | description: "Like GPLv3, but network use counts as distribution — users interacting over a network must get the source.", |
| 207 | permissions: PERMISSIVE_PATENT, |
| 208 | conditions: &[ |
| 209 | DISCLOSE_SOURCE, |
| 210 | KEEP_NOTICE, |
| 211 | NETWORK_DISCLOSE, |
| 212 | SAME_LICENSE, |
| 213 | STATE_CHANGES, |
| 214 | ], |
| 215 | limitations: NO_WARRANTY, |
| 216 | }; |
| 217 | |
| 218 | const UNLICENSE: LicenseInfo = LicenseInfo { |
| 219 | name: "The Unlicense", |
| 220 | spdx: "Unlicense", |
| 221 | description: "A public-domain dedication: do anything, with no conditions at all.", |
| 222 | permissions: PERMISSIVE, |
| 223 | conditions: &[], |
| 224 | limitations: NO_WARRANTY, |
| 225 | }; |
| 226 | |
| 227 | #[cfg(test)] |
| 228 | mod tests { |
| 229 | use super::*; |
| 230 | |
| 231 | #[test] |
| 232 | fn recognizes_license_filenames() { |
| 233 | assert!(is_license_path("LICENSE")); |
| 234 | assert!(is_license_path("license.md")); |
| 235 | assert!(is_license_path("path/to/COPYING")); |
| 236 | assert!(is_license_path("LICENCE.txt")); |
| 237 | assert!(!is_license_path("src/main.rs")); |
| 238 | assert!(!is_license_path("readme.md")); |
| 239 | } |
| 240 | |
| 241 | #[test] |
| 242 | fn detects_common_licenses() { |
| 243 | assert_eq!( |
| 244 | detect("Permission is hereby granted, free of charge, to any person").map(|l| l.spdx), |
| 245 | Some("MIT") |
| 246 | ); |
| 247 | assert_eq!( |
| 248 | detect("Apache License\nVersion 2.0, January 2004").map(|l| l.spdx), |
| 249 | Some("Apache-2.0") |
| 250 | ); |
| 251 | assert_eq!( |
| 252 | detect("GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007").map(|l| l.spdx), |
| 253 | Some("GPL-3.0") |
| 254 | ); |
| 255 | let zero_bsd = "BSD Zero Clause License\n\nPermission to use, copy, modify, \ |
| 256 | and/or distribute this software for any purpose with or without fee is \ |
| 257 | hereby granted."; |
| 258 | assert_eq!(detect(zero_bsd).map(|l| l.spdx), Some("0BSD")); |
| 259 | assert!(detect("just some random text file").is_none()); |
| 260 | } |
| 261 | } |