1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct LicenseExpression {
12 pub expression: String,
14 pub is_valid_spdx: bool,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub resolved_name: Option<String>,
24}
25
26impl PartialEq for LicenseExpression {
29 fn eq(&self, other: &Self) -> bool {
30 self.expression == other.expression && self.is_valid_spdx == other.is_valid_spdx
31 }
32}
33impl Eq for LicenseExpression {}
34impl std::hash::Hash for LicenseExpression {
35 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
36 self.expression.hash(state);
37 self.is_valid_spdx.hash(state);
38 }
39}
40
41impl LicenseExpression {
42 #[must_use]
44 pub fn new(expression: String) -> Self {
45 let is_valid_spdx = Self::validate_spdx(&expression);
46 Self {
47 expression,
48 is_valid_spdx,
49 resolved_name: None,
50 }
51 }
52
53 #[must_use]
58 pub fn display_name(&self) -> &str {
59 self.resolved_name.as_deref().unwrap_or(&self.expression)
60 }
61
62 #[must_use]
64 pub fn from_spdx_id(id: &str) -> Self {
65 Self::new(id.to_string())
68 }
69
70 fn validate_spdx(expr: &str) -> bool {
75 let has_no_info_token = expr
79 .split(|c: char| c.is_whitespace() || c == '(' || c == ')')
80 .any(|tok| tok == "NOASSERTION" || tok == "NONE");
81 if expr.is_empty() || has_no_info_token {
82 return false;
83 }
84 spdx::Expression::parse_mode(expr, spdx::ParseMode::LAX).is_ok()
85 }
86
87 #[must_use]
93 pub fn is_permissive(&self) -> bool {
94 spdx::Expression::parse_mode(&self.expression, spdx::ParseMode::LAX).map_or_else(
95 |_| {
96 let expr_lower = self.expression.to_lowercase();
98 expr_lower.contains("mit")
99 || expr_lower.contains("apache")
100 || expr_lower.contains("bsd")
101 || expr_lower.contains("isc")
102 || expr_lower.contains("unlicense")
103 },
104 |expr| {
105 expr.requirements().any(|req| {
106 if let spdx::LicenseItem::Spdx { id, .. } = req.req.license {
107 !id.is_copyleft() && (id.is_osi_approved() || id.is_fsf_free_libre())
108 } else {
109 false
110 }
111 })
112 },
113 )
114 }
115
116 #[must_use]
121 pub fn is_copyleft(&self) -> bool {
122 spdx::Expression::parse_mode(&self.expression, spdx::ParseMode::LAX).map_or_else(
123 |_| {
124 let expr_lower = self.expression.to_lowercase();
125 expr_lower.contains("gpl")
126 || expr_lower.contains("agpl")
127 || expr_lower.contains("lgpl")
128 || expr_lower.contains("mpl")
129 },
130 |expr| {
131 expr.requirements().any(|req| {
132 if let spdx::LicenseItem::Spdx { id, .. } = req.req.license {
133 id.is_copyleft()
134 } else {
135 false
136 }
137 })
138 },
139 )
140 }
141
142 #[must_use]
149 pub fn family(&self) -> LicenseFamily {
150 if let Ok(expr) = spdx::Expression::parse_mode(&self.expression, spdx::ParseMode::LAX) {
151 let mut has_copyleft = false;
152 let mut has_weak_copyleft = false;
153 let mut has_permissive = false;
154 let mut has_or = false;
155
156 for node in expr.iter() {
157 match node {
158 spdx::expression::ExprNode::Op(spdx::expression::Operator::Or) => {
159 has_or = true;
160 }
161 spdx::expression::ExprNode::Req(req) => {
162 if let spdx::LicenseItem::Spdx { id, .. } = req.req.license {
163 match classify_spdx_license(id) {
164 LicenseFamily::Copyleft => has_copyleft = true,
165 LicenseFamily::WeakCopyleft => has_weak_copyleft = true,
166 LicenseFamily::Permissive | LicenseFamily::PublicDomain => {
167 has_permissive = true;
168 }
169 _ => {}
170 }
171 }
172 }
173 spdx::expression::ExprNode::Op(_) => {}
174 }
175 }
176
177 if has_or && has_permissive {
179 return LicenseFamily::Permissive;
180 }
181
182 if has_copyleft {
184 LicenseFamily::Copyleft
185 } else if has_weak_copyleft {
186 LicenseFamily::WeakCopyleft
187 } else if has_permissive {
188 LicenseFamily::Permissive
189 } else {
190 LicenseFamily::Other
191 }
192 } else {
193 self.family_from_substring()
195 }
196 }
197
198 fn family_from_substring(&self) -> LicenseFamily {
200 let expr_lower = self.expression.to_lowercase();
201 if expr_lower.contains("mit")
202 || expr_lower.contains("apache")
203 || expr_lower.contains("bsd")
204 || expr_lower.contains("isc")
205 || expr_lower.contains("unlicense")
206 {
207 LicenseFamily::Permissive
208 } else if expr_lower.contains("gpl")
209 || expr_lower.contains("agpl")
210 || expr_lower.contains("lgpl")
211 || expr_lower.contains("mpl")
212 {
213 LicenseFamily::Copyleft
214 } else if expr_lower.contains("proprietary") {
215 LicenseFamily::Proprietary
216 } else {
217 LicenseFamily::Other
218 }
219 }
220}
221
222fn family_restrictiveness(family: &LicenseFamily) -> u8 {
224 match family {
225 LicenseFamily::Proprietary => 5,
226 LicenseFamily::Copyleft => 4,
227 LicenseFamily::WeakCopyleft => 3,
228 LicenseFamily::Permissive => 2,
229 LicenseFamily::PublicDomain => 1,
230 LicenseFamily::Other => 0,
231 }
232}
233
234fn classify_spdx_license(id: spdx::LicenseId) -> LicenseFamily {
236 let name = id.name;
237
238 if name == "CC0-1.0" || name == "Unlicense" || name == "0BSD" {
240 return LicenseFamily::PublicDomain;
241 }
242
243 if id.is_copyleft() {
244 let name_upper = name.to_uppercase();
246 if name_upper.contains("LGPL")
247 || name_upper.starts_with("MPL")
248 || name_upper.starts_with("EPL")
249 || name_upper.starts_with("CDDL")
250 || name_upper.starts_with("EUPL")
251 || name_upper.starts_with("OSL")
252 {
253 LicenseFamily::WeakCopyleft
254 } else {
255 LicenseFamily::Copyleft
256 }
257 } else if id.is_osi_approved() || id.is_fsf_free_libre() {
258 LicenseFamily::Permissive
259 } else {
260 LicenseFamily::Other
261 }
262}
263
264impl fmt::Display for LicenseExpression {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 write!(f, "{}", self.expression)
267 }
268}
269
270impl Default for LicenseExpression {
271 fn default() -> Self {
272 Self {
273 expression: "NOASSERTION".to_string(),
274 is_valid_spdx: false,
275 resolved_name: None,
276 }
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
282pub enum LicenseFamily {
283 Permissive,
284 Copyleft,
285 WeakCopyleft,
286 Proprietary,
287 PublicDomain,
288 Other,
289}
290
291impl fmt::Display for LicenseFamily {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 match self {
294 Self::Permissive => write!(f, "Permissive"),
295 Self::Copyleft => write!(f, "Copyleft"),
296 Self::WeakCopyleft => write!(f, "Weak Copyleft"),
297 Self::Proprietary => write!(f, "Proprietary"),
298 Self::PublicDomain => write!(f, "Public Domain"),
299 Self::Other => write!(f, "Other"),
300 }
301 }
302}
303
304#[derive(Debug, Clone, Default, Serialize, Deserialize)]
306pub struct LicenseInfo {
307 pub declared: Vec<LicenseExpression>,
309 pub concluded: Option<LicenseExpression>,
311 pub evidence: Vec<LicenseEvidence>,
313}
314
315impl LicenseInfo {
316 #[must_use]
318 pub fn new() -> Self {
319 Self::default()
320 }
321
322 pub fn add_declared(&mut self, license: LicenseExpression) {
324 self.declared.push(license);
325 }
326
327 #[must_use]
329 pub fn all_licenses(&self) -> Vec<&LicenseExpression> {
330 let mut licenses: Vec<&LicenseExpression> = self.declared.iter().collect();
331 if let Some(concluded) = &self.concluded {
332 licenses.push(concluded);
333 }
334 licenses
335 }
336
337 #[must_use]
346 pub fn effective_family(&self) -> LicenseFamily {
347 self.all_licenses()
348 .into_iter()
349 .map(LicenseExpression::family)
350 .max_by_key(family_restrictiveness)
351 .unwrap_or(LicenseFamily::Other)
352 }
353
354 pub fn has_conflicts(&self) -> bool {
361 let families: Vec<LicenseFamily> = self
362 .all_licenses()
363 .into_iter()
364 .map(LicenseExpression::family)
365 .collect();
366
367 let has_copyleft = families.contains(&LicenseFamily::Copyleft);
368 let has_proprietary = families.contains(&LicenseFamily::Proprietary);
369
370 has_copyleft && has_proprietary
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct LicenseEvidence {
377 pub license: LicenseExpression,
379 pub confidence: f64,
381 pub file_path: Option<String>,
383 pub line_number: Option<u32>,
385}
386
387impl LicenseEvidence {
388 #[must_use]
390 pub const fn new(license: LicenseExpression, confidence: f64) -> Self {
391 Self {
392 license,
393 confidence,
394 file_path: None,
395 line_number: None,
396 }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 fn info(declared: &[&str], concluded: Option<&str>) -> LicenseInfo {
405 let mut info = LicenseInfo::new();
406 for lic in declared {
407 info.add_declared(LicenseExpression::new((*lic).to_string()));
408 }
409 info.concluded = concluded.map(|c| LicenseExpression::new(c.to_string()));
410 info
411 }
412
413 #[test]
414 fn effective_family_precedence() {
415 assert_eq!(info(&[], None).effective_family(), LicenseFamily::Other);
416 assert_eq!(
417 info(&["MIT"], None).effective_family(),
418 LicenseFamily::Permissive
419 );
420 assert_eq!(
421 info(&["MIT", "GPL-3.0-only"], None).effective_family(),
422 LicenseFamily::Copyleft
423 );
424 assert_eq!(
425 info(&["MIT", "LGPL-3.0-only"], None).effective_family(),
426 LicenseFamily::WeakCopyleft
427 );
428 assert_eq!(
429 info(&["GPL-3.0-only", "Proprietary"], None).effective_family(),
430 LicenseFamily::Proprietary
431 );
432 assert_eq!(
433 info(&["MIT"], Some("GPL-3.0-only")).effective_family(),
434 LicenseFamily::Copyleft
435 );
436 }
437
438 #[test]
439 fn display_name_prefers_resolved_name() {
440 let mut lic = LicenseExpression::new("LicenseRef-foo".to_string());
441 assert_eq!(lic.display_name(), "LicenseRef-foo");
442
443 lic.resolved_name = Some("Foo Proprietary License".to_string());
444 assert_eq!(lic.display_name(), "Foo Proprietary License");
445 assert_eq!(lic, LicenseExpression::new("LicenseRef-foo".to_string()));
447 }
448
449 #[test]
450 fn has_conflicts_includes_concluded() {
451 let conflicted = info(&["Proprietary"], Some("GPL-3.0-only"));
452 assert!(conflicted.has_conflicts());
453
454 let declared_only = info(&["GPL-3.0-only", "Proprietary"], None);
455 assert!(declared_only.has_conflicts());
456
457 let no_conflict = info(&["MIT"], Some("GPL-3.0-only"));
458 assert!(!no_conflict.has_conflicts());
459 }
460}