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 { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result23 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 from_str(s: &str) -> Result<Self, Self::Err>31 fn from_str(s: &str) -> Result<Self, Self::Err> { 32 Self::parse(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] new(license: LicenseItem, exception: Option<ExceptionId>) -> Self41 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 /// ``` parse(original: &str) -> Result<Self, ParseError>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] satisfies(&self, req: &LicenseReq) -> bool202 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] into_req(self) -> LicenseReq263 pub fn into_req(self) -> LicenseReq { 264 self.inner 265 } 266 } 267 268 impl PartialOrd<LicenseReq> for Licensee { 269 #[inline] partial_cmp(&self, o: &LicenseReq) -> Option<std::cmp::Ordering>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] eq(&self, o: &LicenseReq) -> bool277 fn eq(&self, o: &LicenseReq) -> bool { 278 self.inner.eq(o) 279 } 280 } 281 282 impl AsRef<LicenseReq> for Licensee { 283 #[inline] as_ref(&self) -> &LicenseReq284 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] handles_or_later()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] handles_exceptions()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] handles_license_ref()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] handles_close()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