sbom_tools/model/
license.rs1use 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 classify_spdx_license(id: spdx::LicenseId) -> LicenseFamily {
186 let name = id.name;
187
188 if name == "CC0-1.0" || name == "Unlicense" || name == "0BSD" {
190 return LicenseFamily::PublicDomain;
191 }
192
193 if id.is_copyleft() {
194 let name_upper = name.to_uppercase();
196 if name_upper.contains("LGPL")
197 || name_upper.starts_with("MPL")
198 || name_upper.starts_with("EPL")
199 || name_upper.starts_with("CDDL")
200 || name_upper.starts_with("EUPL")
201 || name_upper.starts_with("OSL")
202 {
203 LicenseFamily::WeakCopyleft
204 } else {
205 LicenseFamily::Copyleft
206 }
207 } else if id.is_osi_approved() || id.is_fsf_free_libre() {
208 LicenseFamily::Permissive
209 } else {
210 LicenseFamily::Other
211 }
212}
213
214impl fmt::Display for LicenseExpression {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 write!(f, "{}", self.expression)
217 }
218}
219
220impl Default for LicenseExpression {
221 fn default() -> Self {
222 Self {
223 expression: "NOASSERTION".to_string(),
224 is_valid_spdx: false,
225 }
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
231pub enum LicenseFamily {
232 Permissive,
233 Copyleft,
234 WeakCopyleft,
235 Proprietary,
236 PublicDomain,
237 Other,
238}
239
240impl fmt::Display for LicenseFamily {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 Self::Permissive => write!(f, "Permissive"),
244 Self::Copyleft => write!(f, "Copyleft"),
245 Self::WeakCopyleft => write!(f, "Weak Copyleft"),
246 Self::Proprietary => write!(f, "Proprietary"),
247 Self::PublicDomain => write!(f, "Public Domain"),
248 Self::Other => write!(f, "Other"),
249 }
250 }
251}
252
253#[derive(Debug, Clone, Default, Serialize, Deserialize)]
255pub struct LicenseInfo {
256 pub declared: Vec<LicenseExpression>,
258 pub concluded: Option<LicenseExpression>,
260 pub evidence: Vec<LicenseEvidence>,
262}
263
264impl LicenseInfo {
265 #[must_use]
267 pub fn new() -> Self {
268 Self::default()
269 }
270
271 pub fn add_declared(&mut self, license: LicenseExpression) {
273 self.declared.push(license);
274 }
275
276 #[must_use]
278 pub fn all_licenses(&self) -> Vec<&LicenseExpression> {
279 let mut licenses: Vec<&LicenseExpression> = self.declared.iter().collect();
280 if let Some(concluded) = &self.concluded {
281 licenses.push(concluded);
282 }
283 licenses
284 }
285
286 pub fn has_conflicts(&self) -> bool {
292 let families: Vec<LicenseFamily> = self
293 .declared
294 .iter()
295 .map(LicenseExpression::family)
296 .collect();
297
298 let has_copyleft = families.contains(&LicenseFamily::Copyleft);
299 let has_proprietary = families.contains(&LicenseFamily::Proprietary);
300
301 has_copyleft && has_proprietary
302 }
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct LicenseEvidence {
308 pub license: LicenseExpression,
310 pub confidence: f64,
312 pub file_path: Option<String>,
314 pub line_number: Option<u32>,
316}
317
318impl LicenseEvidence {
319 #[must_use]
321 pub const fn new(license: LicenseExpression, confidence: f64) -> Self {
322 Self {
323 license,
324 confidence,
325 file_path: None,
326 line_number: None,
327 }
328 }
329}