| 1 | use crate::{ |
| 2 | error::{ParseError, Reason}, |
| 3 | lexer::{Lexer, Token}, |
| 4 | ExceptionId, LicenseItem, LicenseReq, |
| 5 | }; |
| 6 | use std::fmt; |
| 7 | |
| 8 | /// A convenience wrapper for a license and optional exception that can be |
| 9 | /// checked against a license requirement to see if it satisfies the requirement |
| 10 | /// placed by a license holder |
| 11 | /// |
| 12 | /// ``` |
| 13 | /// let licensee = spdx::Licensee::parse("GPL-2.0" ).unwrap(); |
| 14 | /// |
| 15 | /// assert!(licensee.satisfies(&spdx::LicenseReq::from(spdx::license_id("GPL-2.0-only" ).unwrap()))); |
| 16 | /// ``` |
| 17 | #[derive (PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] |
| 18 | pub struct Licensee { |
| 19 | inner: LicenseReq, |
| 20 | } |
| 21 | |
| 22 | impl fmt::Display for Licensee { |
| 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 24 | self.inner.fmt(f) |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | impl std::str::FromStr for Licensee { |
| 29 | type Err = ParseError; |
| 30 | |
| 31 | fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 32 | Self::parse(original:s) |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | impl Licensee { |
| 37 | /// Creates a licensee from its component parts. Note that use of SPDX's |
| 38 | /// `or_later` is completely ignored for licensees as it only applies |
| 39 | /// to the license holder(s), not the licensee |
| 40 | #[must_use ] |
| 41 | pub fn new(license: LicenseItem, exception: Option<ExceptionId>) -> Self { |
| 42 | if let LicenseItem::Spdx { or_later, .. } = &license { |
| 43 | debug_assert!(!or_later); |
| 44 | } |
| 45 | |
| 46 | Self { |
| 47 | inner: LicenseReq { license, exception }, |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | /// Parses an simplified version of an SPDX license expression that can |
| 52 | /// contain at most 1 valid SDPX license with an optional exception joined |
| 53 | /// by a `WITH`. |
| 54 | /// |
| 55 | /// ``` |
| 56 | /// use spdx::Licensee; |
| 57 | /// |
| 58 | /// // Normal single license |
| 59 | /// Licensee::parse("MIT" ).unwrap(); |
| 60 | /// |
| 61 | /// // SPDX allows license identifiers outside of the official license list |
| 62 | /// // via the LicenseRef- prefix |
| 63 | /// Licensee::parse("LicenseRef-My-Super-Extra-Special-License" ).unwrap(); |
| 64 | /// |
| 65 | /// // License and exception |
| 66 | /// Licensee::parse("Apache-2.0 WITH LLVM-exception" ).unwrap(); |
| 67 | /// |
| 68 | /// // `+` is only allowed to be used by license requirements from the license holder |
| 69 | /// Licensee::parse("Apache-2.0+" ).unwrap_err(); |
| 70 | /// |
| 71 | /// Licensee::parse("GPL-2.0" ).unwrap(); |
| 72 | /// |
| 73 | /// // GNU suffix license (GPL, AGPL, LGPL, GFDL) must not contain the suffix |
| 74 | /// Licensee::parse("GPL-3.0-or-later" ).unwrap_err(); |
| 75 | /// |
| 76 | /// // GFDL licenses are only allowed to contain the `invariants` suffix |
| 77 | /// Licensee::parse("GFDL-1.3-invariants" ).unwrap(); |
| 78 | /// ``` |
| 79 | pub fn parse(original: &str) -> Result<Self, ParseError> { |
| 80 | let mut lexer = Lexer::new(original); |
| 81 | |
| 82 | let license = { |
| 83 | let lt = lexer.next().ok_or_else(|| ParseError { |
| 84 | original: original.to_owned(), |
| 85 | span: 0..original.len(), |
| 86 | reason: Reason::Empty, |
| 87 | })??; |
| 88 | |
| 89 | match lt.token { |
| 90 | Token::Spdx(id) => { |
| 91 | // If we have one of the GNU licenses which use the `-only` |
| 92 | // or `-or-later` suffixes return an error rather than |
| 93 | // silently truncating, the `-only` and `-or-later` suffixes |
| 94 | // are for the license holder(s) to specify what license(s) |
| 95 | // they can be licensed under, not for the licensee, |
| 96 | // similarly to the `+` |
| 97 | if id.is_gnu() { |
| 98 | let is_only = original.ends_with("-only" ); |
| 99 | let or_later = original.ends_with("-or-later" ); |
| 100 | |
| 101 | if is_only || or_later { |
| 102 | return Err(ParseError { |
| 103 | original: original.to_owned(), |
| 104 | span: if is_only { |
| 105 | original.len() - 5..original.len() |
| 106 | } else { |
| 107 | original.len() - 9..original.len() |
| 108 | }, |
| 109 | reason: Reason::Unexpected(&["<bare-gnu-license>" ]), |
| 110 | }); |
| 111 | } |
| 112 | |
| 113 | // GFDL has `no-invariants` and `invariants` variants, we |
| 114 | // treat `no-invariants` as invalid, just the same as |
| 115 | // only, it would be the same as a bare GFDL-<version>. |
| 116 | // However, the `invariants`...variant we do allow since |
| 117 | // it is a modifier on the license...and should therefore |
| 118 | // by a WITH exception but GNU licenses are the worst |
| 119 | if original.starts_with("GFDL" ) && original.contains("-no-invariants" ) { |
| 120 | return Err(ParseError { |
| 121 | original: original.to_owned(), |
| 122 | span: 8..original.len(), |
| 123 | reason: Reason::Unexpected(&["<bare-gfdl-license>" ]), |
| 124 | }); |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | LicenseItem::Spdx { |
| 129 | id, |
| 130 | or_later: false, |
| 131 | } |
| 132 | } |
| 133 | Token::LicenseRef { doc_ref, lic_ref } => LicenseItem::Other { |
| 134 | doc_ref: doc_ref.map(String::from), |
| 135 | lic_ref: lic_ref.to_owned(), |
| 136 | }, |
| 137 | _ => { |
| 138 | return Err(ParseError { |
| 139 | original: original.to_owned(), |
| 140 | span: lt.span, |
| 141 | reason: Reason::Unexpected(&["<license>" ]), |
| 142 | }) |
| 143 | } |
| 144 | } |
| 145 | }; |
| 146 | |
| 147 | let exception = match lexer.next() { |
| 148 | None => None, |
| 149 | Some(lt) => { |
| 150 | let lt = lt?; |
| 151 | match lt.token { |
| 152 | Token::With => { |
| 153 | let lt = lexer.next().ok_or(ParseError { |
| 154 | original: original.to_owned(), |
| 155 | span: lt.span, |
| 156 | reason: Reason::Empty, |
| 157 | })??; |
| 158 | |
| 159 | match lt.token { |
| 160 | Token::Exception(exc) => Some(exc), |
| 161 | _ => { |
| 162 | return Err(ParseError { |
| 163 | original: original.to_owned(), |
| 164 | span: lt.span, |
| 165 | reason: Reason::Unexpected(&["<exception>" ]), |
| 166 | }) |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | _ => { |
| 171 | return Err(ParseError { |
| 172 | original: original.to_owned(), |
| 173 | span: lt.span, |
| 174 | reason: Reason::Unexpected(&["WITH" ]), |
| 175 | }) |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | }; |
| 180 | |
| 181 | Ok(Licensee { |
| 182 | inner: LicenseReq { license, exception }, |
| 183 | }) |
| 184 | } |
| 185 | |
| 186 | /// Determines whether the specified license requirement is satisfied by |
| 187 | /// this license (+exception) |
| 188 | /// |
| 189 | /// ``` |
| 190 | /// let licensee = spdx::Licensee::parse("Apache-2.0 WITH LLVM-exception" ).unwrap(); |
| 191 | /// |
| 192 | /// assert!(licensee.satisfies(&spdx::LicenseReq { |
| 193 | /// license: spdx::LicenseItem::Spdx { |
| 194 | /// id: spdx::license_id("Apache-2.0" ).unwrap(), |
| 195 | /// // Means the license holder is fine with Apache-2.0 or higher |
| 196 | /// or_later: true, |
| 197 | /// }, |
| 198 | /// exception: spdx::exception_id("LLVM-exception" ), |
| 199 | /// })); |
| 200 | /// ``` |
| 201 | #[must_use ] |
| 202 | pub fn satisfies(&self, req: &LicenseReq) -> bool { |
| 203 | match (&self.inner.license, &req.license) { |
| 204 | (LicenseItem::Spdx { id: a, .. }, LicenseItem::Spdx { id: b, or_later }) => { |
| 205 | if a.index != b.index { |
| 206 | if *or_later { |
| 207 | let (a_name, a_gfdl_invariants) = if a.name.starts_with("GFDL" ) { |
| 208 | a.name |
| 209 | .strip_suffix("-invariants" ) |
| 210 | .map_or((a.name, false), |name| (name, true)) |
| 211 | } else { |
| 212 | (a.name, false) |
| 213 | }; |
| 214 | |
| 215 | let (b_name, b_gfdl_invariants) = if b.name.starts_with("GFDL" ) { |
| 216 | b.name |
| 217 | .strip_suffix("-invariants" ) |
| 218 | .map_or((b.name, false), |name| (name, true)) |
| 219 | } else { |
| 220 | (b.name, false) |
| 221 | }; |
| 222 | |
| 223 | if a_gfdl_invariants != b_gfdl_invariants { |
| 224 | return false; |
| 225 | } |
| 226 | |
| 227 | // Many of the SPDX identifiers end with `-<version number>`, |
| 228 | // so chop that off and ensure the base strings match, and if so, |
| 229 | // just a do a lexical compare, if this "allowed license" is >, |
| 230 | // then we satisfed the license requirement |
| 231 | let a_test_name = &a_name[..a_name.rfind('-' ).unwrap_or(a_name.len())]; |
| 232 | let b_test_name = &b_name[..b_name.rfind('-' ).unwrap_or(b_name.len())]; |
| 233 | |
| 234 | if a_test_name != b_test_name || a_name < b_name { |
| 235 | return false; |
| 236 | } |
| 237 | } else { |
| 238 | return false; |
| 239 | } |
| 240 | } |
| 241 | } |
| 242 | ( |
| 243 | LicenseItem::Other { |
| 244 | doc_ref: doc_a, |
| 245 | lic_ref: lic_a, |
| 246 | }, |
| 247 | LicenseItem::Other { |
| 248 | doc_ref: doc_b, |
| 249 | lic_ref: lic_b, |
| 250 | }, |
| 251 | ) => { |
| 252 | if doc_a != doc_b || lic_a != lic_b { |
| 253 | return false; |
| 254 | } |
| 255 | } |
| 256 | _ => return false, |
| 257 | } |
| 258 | |
| 259 | req.exception == self.inner.exception |
| 260 | } |
| 261 | |
| 262 | #[must_use ] |
| 263 | pub fn into_req(self) -> LicenseReq { |
| 264 | self.inner |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | impl PartialOrd<LicenseReq> for Licensee { |
| 269 | #[inline ] |
| 270 | fn partial_cmp(&self, o: &LicenseReq) -> Option<std::cmp::Ordering> { |
| 271 | self.inner.partial_cmp(o) |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | impl PartialEq<LicenseReq> for Licensee { |
| 276 | #[inline ] |
| 277 | fn eq(&self, o: &LicenseReq) -> bool { |
| 278 | self.inner.eq(o) |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | impl AsRef<LicenseReq> for Licensee { |
| 283 | #[inline ] |
| 284 | fn as_ref(&self) -> &LicenseReq { |
| 285 | &self.inner |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | #[cfg (test)] |
| 290 | mod test { |
| 291 | use crate::{exception_id, license_id, LicenseItem, LicenseReq, Licensee}; |
| 292 | |
| 293 | const LICENSEES: &[&str] = &[ |
| 294 | "LicenseRef-Embark-Proprietary" , |
| 295 | "BSD-2-Clause" , |
| 296 | "Apache-2.0 WITH LLVM-exception" , |
| 297 | "BSD-2-Clause-FreeBSD" , |
| 298 | "BSL-1.0" , |
| 299 | "Zlib" , |
| 300 | "CC0-1.0" , |
| 301 | "FTL" , |
| 302 | "ISC" , |
| 303 | "MIT" , |
| 304 | "MPL-2.0" , |
| 305 | "BSD-3-Clause" , |
| 306 | "Unicode-DFS-2016" , |
| 307 | "Unlicense" , |
| 308 | "Apache-2.0" , |
| 309 | ]; |
| 310 | |
| 311 | #[test ] |
| 312 | fn handles_or_later() { |
| 313 | let mut licensees: Vec<_> = LICENSEES |
| 314 | .iter() |
| 315 | .map(|l| Licensee::parse(l).unwrap()) |
| 316 | .collect(); |
| 317 | licensees.sort(); |
| 318 | |
| 319 | let mpl_id = license_id("MPL-2.0" ).unwrap(); |
| 320 | let req = LicenseReq { |
| 321 | license: LicenseItem::Spdx { |
| 322 | id: mpl_id, |
| 323 | or_later: true, |
| 324 | }, |
| 325 | exception: None, |
| 326 | }; |
| 327 | |
| 328 | // Licensees can't have the `or_later` |
| 329 | assert!(licensees.binary_search_by(|l| l.inner.cmp(&req)).is_err()); |
| 330 | |
| 331 | match &licensees[licensees |
| 332 | .binary_search_by(|l| l.partial_cmp(&req).unwrap()) |
| 333 | .unwrap()] |
| 334 | .inner |
| 335 | .license |
| 336 | { |
| 337 | LicenseItem::Spdx { id, .. } => assert_eq!(*id, mpl_id), |
| 338 | o @ LicenseItem::Other { .. } => panic!("unexpected {:?}" , o), |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | #[test ] |
| 343 | fn handles_exceptions() { |
| 344 | let mut licensees: Vec<_> = LICENSEES |
| 345 | .iter() |
| 346 | .map(|l| Licensee::parse(l).unwrap()) |
| 347 | .collect(); |
| 348 | licensees.sort(); |
| 349 | |
| 350 | let apache_id = license_id("Apache-2.0" ).unwrap(); |
| 351 | let llvm_exc = exception_id("LLVM-exception" ).unwrap(); |
| 352 | let req = LicenseReq { |
| 353 | license: LicenseItem::Spdx { |
| 354 | id: apache_id, |
| 355 | or_later: false, |
| 356 | }, |
| 357 | exception: Some(llvm_exc), |
| 358 | }; |
| 359 | |
| 360 | assert_eq!( |
| 361 | &req, |
| 362 | &licensees[licensees |
| 363 | .binary_search_by(|l| l.partial_cmp(&req).unwrap()) |
| 364 | .unwrap()] |
| 365 | .inner |
| 366 | ); |
| 367 | } |
| 368 | |
| 369 | #[test ] |
| 370 | fn handles_license_ref() { |
| 371 | let mut licensees: Vec<_> = LICENSEES |
| 372 | .iter() |
| 373 | .map(|l| Licensee::parse(l).unwrap()) |
| 374 | .collect(); |
| 375 | licensees.sort(); |
| 376 | |
| 377 | let req = LicenseReq { |
| 378 | license: LicenseItem::Other { |
| 379 | doc_ref: None, |
| 380 | lic_ref: "Embark-Proprietary" .to_owned(), |
| 381 | }, |
| 382 | exception: None, |
| 383 | }; |
| 384 | |
| 385 | assert_eq!( |
| 386 | &req, |
| 387 | &licensees[licensees |
| 388 | .binary_search_by(|l| l.partial_cmp(&req).unwrap()) |
| 389 | .unwrap()] |
| 390 | .inner |
| 391 | ); |
| 392 | } |
| 393 | |
| 394 | #[test ] |
| 395 | fn handles_close() { |
| 396 | let mut licensees: Vec<_> = LICENSEES |
| 397 | .iter() |
| 398 | .map(|l| Licensee::parse(l).unwrap()) |
| 399 | .collect(); |
| 400 | licensees.sort(); |
| 401 | |
| 402 | for id in &["BSD-2-Clause" , "BSD-2-Clause-FreeBSD" ] { |
| 403 | let lic_id = license_id(id).unwrap(); |
| 404 | let req = LicenseReq { |
| 405 | license: LicenseItem::Spdx { |
| 406 | id: lic_id, |
| 407 | or_later: true, |
| 408 | }, |
| 409 | exception: None, |
| 410 | }; |
| 411 | |
| 412 | // Licensees can't have the `or_later` |
| 413 | assert!(licensees.binary_search_by(|l| l.inner.cmp(&req)).is_err()); |
| 414 | |
| 415 | match &licensees[licensees |
| 416 | .binary_search_by(|l| l.partial_cmp(&req).unwrap()) |
| 417 | .unwrap()] |
| 418 | .inner |
| 419 | .license |
| 420 | { |
| 421 | LicenseItem::Spdx { id, .. } => assert_eq!(*id, lic_id), |
| 422 | o @ LicenseItem::Other { .. } => panic!("unexpected {:?}" , o), |
| 423 | } |
| 424 | } |
| 425 | } |
| 426 | } |
| 427 | |