1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct LicenseExpression {
12 pub expression: String,
14 pub is_valid_spdx: bool,
16}
17
18impl LicenseExpression {
19 #[must_use]
21 pub fn new(expression: String) -> Self {
22 let is_valid_spdx = Self::validate_spdx(&expression);
23 Self {
24 expression,
25 is_valid_spdx,
26 }
27 }
28
29 #[must_use]
31 pub fn from_spdx_id(id: &str) -> Self {
32 Self {
33 expression: id.to_string(),
34 is_valid_spdx: true,
35 }
36 }
37
38 fn validate_spdx(expr: &str) -> bool {
43 if expr.is_empty() || expr.contains("NOASSERTION") || expr.contains("NONE") {
44 return false;
45 }
46 spdx::Expression::parse_mode(expr, spdx::ParseMode::LAX).is_ok()
47 }
48
49 #[must_use]
55 pub fn is_permissive(&self) -> bool {
56 spdx::Expression::parse_mode(&self.expression, spdx::ParseMode::LAX).map_or_else(
57 |_| {
58 let expr_lower = self.expression.to_lowercase();
60 expr_lower.contains("mit")
61 || expr_lower.contains("apache")
62 || expr_lower.contains("bsd")
63 || expr_lower.contains("isc")
64 || expr_lower.contains("unlicense")
65 },
66 |expr| {
67 expr.requirements().any(|req| {
68 if let spdx::LicenseItem::Spdx { id, .. } = req.req.license {
69 !id.is_copyleft() && (id.is_osi_approved() || id.is_fsf_free_libre())
70 } else {
71 false
72 }
73 })
74 },
75 )
76 }
77
78 #[must_use]
83 pub fn is_copyleft(&self) -> bool {
84 spdx::Expression::parse_mode(&self.expression, spdx::ParseMode::LAX).map_or_else(
85 |_| {
86 let expr_lower = self.expression.to_lowercase();
87 expr_lower.contains("gpl")
88 || expr_lower.contains("agpl")
89 || expr_lower.contains("lgpl")
90 || expr_lower.contains("mpl")
91 },
92 |expr| {
93 expr.requirements().any(|req| {
94 if let spdx::LicenseItem::Spdx { id, .. } = req.req.license {
95 id.is_copyleft()
96 } else {
97 false
98 }
99 })
100 },
101 )
102 }
103
104 #[must_use]
111 pub fn family(&self) -> LicenseFamily {
112 if let Ok(expr) = spdx::Expression::parse_mode(&self.expression, spdx::ParseMode::LAX) {
113 let mut has_copyleft = false;
114 let mut has_weak_copyleft = false;
115 let mut has_permissive = false;
116 let mut has_or = false;
117
118 for node in expr.iter() {
119 match node {
120 spdx::expression::ExprNode::Op(spdx::expression::Operator::Or) => {
121 has_or = true;
122 }
123 spdx::expression::ExprNode::Req(req) => {
124 if let spdx::LicenseItem::Spdx { id, .. } = req.req.license {
125 match classify_spdx_license(id) {
126 LicenseFamily::Copyleft => has_copyleft = true,
127 LicenseFamily::WeakCopyleft => has_weak_copyleft = true,
128 LicenseFamily::Permissive | LicenseFamily::PublicDomain => {
129 has_permissive = true;
130 }
131 _ => {}
132 }
133 }
134 }
135 spdx::expression::ExprNode::Op(_) => {}
136 }
137 }
138
139 if has_or && has_permissive {
141 return LicenseFamily::Permissive;
142 }
143
144 if has_copyleft {
146 LicenseFamily::Copyleft
147 } else if has_weak_copyleft {
148 LicenseFamily::WeakCopyleft
149 } else if has_permissive {
150 LicenseFamily::Permissive
151 } else {
152 LicenseFamily::Other
153 }
154 } else {
155 self.family_from_substring()
157 }
158 }
159
160 fn family_from_substring(&self) -> LicenseFamily {
162 let expr_lower = self.expression.to_lowercase();
163 if expr_lower.contains("mit")
164 || expr_lower.contains("apache")
165 || expr_lower.contains("bsd")
166 || expr_lower.contains("isc")
167 || expr_lower.contains("unlicense")
168 {
169 LicenseFamily::Permissive
170 } else if expr_lower.contains("gpl")
171 || expr_lower.contains("agpl")
172 || expr_lower.contains("lgpl")
173 || expr_lower.contains("mpl")
174 {
175 LicenseFamily::Copyleft
176 } else if expr_lower.contains("proprietary") {
177 LicenseFamily::Proprietary
178 } else {
179 LicenseFamily::Other
180 }
181 }
182}
183
184fn family_restrictiveness(family: &LicenseFamily) -> u8 {
186 match family {
187 LicenseFamily::Proprietary => 5,
188 LicenseFamily::Copyleft => 4,
189 LicenseFamily::WeakCopyleft => 3,
190 LicenseFamily::Permissive => 2,
191 LicenseFamily::PublicDomain => 1,
192 LicenseFamily::Other => 0,
193 }
194}
195
196fn classify_spdx_license(id: spdx::LicenseId) -> LicenseFamily {
198 let name = id.name;
199
200 if name == "CC0-1.0" || name == "Unlicense" || name == "0BSD" {
202 return LicenseFamily::PublicDomain;
203 }
204
205 if id.is_copyleft() {
206 let name_upper = name.to_uppercase();
208 if name_upper.contains("LGPL")
209 || name_upper.starts_with("MPL")
210 || name_upper.starts_with("EPL")
211 || name_upper.starts_with("CDDL")
212 || name_upper.starts_with("EUPL")
213 || name_upper.starts_with("OSL")
214 {
215 LicenseFamily::WeakCopyleft
216 } else {
217 LicenseFamily::Copyleft
218 }
219 } else if id.is_osi_approved() || id.is_fsf_free_libre() {
220 LicenseFamily::Permissive
221 } else {
222 LicenseFamily::Other
223 }
224}
225
226impl fmt::Display for LicenseExpression {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 write!(f, "{}", self.expression)
229 }
230}
231
232impl Default for LicenseExpression {
233 fn default() -> Self {
234 Self {
235 expression: "NOASSERTION".to_string(),
236 is_valid_spdx: false,
237 }
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
243pub enum LicenseFamily {
244 Permissive,
245 Copyleft,
246 WeakCopyleft,
247 Proprietary,
248 PublicDomain,
249 Other,
250}
251
252impl fmt::Display for LicenseFamily {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 match self {
255 Self::Permissive => write!(f, "Permissive"),
256 Self::Copyleft => write!(f, "Copyleft"),
257 Self::WeakCopyleft => write!(f, "Weak Copyleft"),
258 Self::Proprietary => write!(f, "Proprietary"),
259 Self::PublicDomain => write!(f, "Public Domain"),
260 Self::Other => write!(f, "Other"),
261 }
262 }
263}
264
265#[derive(Debug, Clone, Default, Serialize, Deserialize)]
267pub struct LicenseInfo {
268 pub declared: Vec<LicenseExpression>,
270 pub concluded: Option<LicenseExpression>,
272 pub evidence: Vec<LicenseEvidence>,
274}
275
276impl LicenseInfo {
277 #[must_use]
279 pub fn new() -> Self {
280 Self::default()
281 }
282
283 pub fn add_declared(&mut self, license: LicenseExpression) {
285 self.declared.push(license);
286 }
287
288 #[must_use]
290 pub fn all_licenses(&self) -> Vec<&LicenseExpression> {
291 let mut licenses: Vec<&LicenseExpression> = self.declared.iter().collect();
292 if let Some(concluded) = &self.concluded {
293 licenses.push(concluded);
294 }
295 licenses
296 }
297
298 #[must_use]
307 pub fn effective_family(&self) -> LicenseFamily {
308 self.all_licenses()
309 .into_iter()
310 .map(LicenseExpression::family)
311 .max_by_key(family_restrictiveness)
312 .unwrap_or(LicenseFamily::Other)
313 }
314
315 pub fn has_conflicts(&self) -> bool {
322 let families: Vec<LicenseFamily> = self
323 .all_licenses()
324 .into_iter()
325 .map(LicenseExpression::family)
326 .collect();
327
328 let has_copyleft = families.contains(&LicenseFamily::Copyleft);
329 let has_proprietary = families.contains(&LicenseFamily::Proprietary);
330
331 has_copyleft && has_proprietary
332 }
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct LicenseEvidence {
338 pub license: LicenseExpression,
340 pub confidence: f64,
342 pub file_path: Option<String>,
344 pub line_number: Option<u32>,
346}
347
348impl LicenseEvidence {
349 #[must_use]
351 pub const fn new(license: LicenseExpression, confidence: f64) -> Self {
352 Self {
353 license,
354 confidence,
355 file_path: None,
356 line_number: None,
357 }
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 fn info(declared: &[&str], concluded: Option<&str>) -> LicenseInfo {
366 let mut info = LicenseInfo::new();
367 for lic in declared {
368 info.add_declared(LicenseExpression::new((*lic).to_string()));
369 }
370 info.concluded = concluded.map(|c| LicenseExpression::new(c.to_string()));
371 info
372 }
373
374 #[test]
375 fn effective_family_precedence() {
376 assert_eq!(info(&[], None).effective_family(), LicenseFamily::Other);
377 assert_eq!(
378 info(&["MIT"], None).effective_family(),
379 LicenseFamily::Permissive
380 );
381 assert_eq!(
382 info(&["MIT", "GPL-3.0-only"], None).effective_family(),
383 LicenseFamily::Copyleft
384 );
385 assert_eq!(
386 info(&["MIT", "LGPL-3.0-only"], None).effective_family(),
387 LicenseFamily::WeakCopyleft
388 );
389 assert_eq!(
390 info(&["GPL-3.0-only", "Proprietary"], None).effective_family(),
391 LicenseFamily::Proprietary
392 );
393 assert_eq!(
394 info(&["MIT"], Some("GPL-3.0-only")).effective_family(),
395 LicenseFamily::Copyleft
396 );
397 }
398
399 #[test]
400 fn has_conflicts_includes_concluded() {
401 let conflicted = info(&["Proprietary"], Some("GPL-3.0-only"));
402 assert!(conflicted.has_conflicts());
403
404 let declared_only = info(&["GPL-3.0-only", "Proprietary"], None);
405 assert!(declared_only.has_conflicts());
406
407 let no_conflict = info(&["MIT"], Some("GPL-3.0-only"));
408 assert!(!no_conflict.has_conflicts());
409 }
410}