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 pub fn new(expression: String) -> Self {
21 let is_valid_spdx = Self::validate_spdx(&expression);
22 Self {
23 expression,
24 is_valid_spdx,
25 }
26 }
27
28 pub fn from_spdx_id(id: &str) -> Self {
30 Self {
31 expression: id.to_string(),
32 is_valid_spdx: true,
33 }
34 }
35
36 fn validate_spdx(expr: &str) -> bool {
41 if expr.is_empty() || expr.contains("NOASSERTION") || expr.contains("NONE") {
42 return false;
43 }
44 spdx::Expression::parse_mode(expr, spdx::ParseMode::LAX).is_ok()
45 }
46
47 pub fn is_permissive(&self) -> bool {
53 if let Ok(expr) = spdx::Expression::parse_mode(&self.expression, spdx::ParseMode::LAX) {
54 expr.requirements().any(|req| {
55 if let spdx::LicenseItem::Spdx { id, .. } = req.req.license {
56 !id.is_copyleft() && (id.is_osi_approved() || id.is_fsf_free_libre())
57 } else {
58 false
59 }
60 })
61 } else {
62 let expr_lower = self.expression.to_lowercase();
64 expr_lower.contains("mit")
65 || expr_lower.contains("apache")
66 || expr_lower.contains("bsd")
67 || expr_lower.contains("isc")
68 || expr_lower.contains("unlicense")
69 }
70 }
71
72 pub fn is_copyleft(&self) -> bool {
77 if let Ok(expr) = spdx::Expression::parse_mode(&self.expression, spdx::ParseMode::LAX) {
78 expr.requirements().any(|req| {
79 if let spdx::LicenseItem::Spdx { id, .. } = req.req.license {
80 id.is_copyleft()
81 } else {
82 false
83 }
84 })
85 } else {
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 }
93
94 pub fn family(&self) -> LicenseFamily {
101 if let Ok(expr) = spdx::Expression::parse_mode(&self.expression, spdx::ParseMode::LAX) {
102 let mut has_copyleft = false;
103 let mut has_weak_copyleft = false;
104 let mut has_permissive = false;
105 let mut has_or = false;
106
107 for node in expr.iter() {
108 match node {
109 spdx::expression::ExprNode::Op(spdx::expression::Operator::Or) => {
110 has_or = true;
111 }
112 spdx::expression::ExprNode::Req(req) => {
113 if let spdx::LicenseItem::Spdx { id, .. } = req.req.license {
114 match classify_spdx_license(id) {
115 LicenseFamily::Copyleft => has_copyleft = true,
116 LicenseFamily::WeakCopyleft => has_weak_copyleft = true,
117 LicenseFamily::Permissive | LicenseFamily::PublicDomain => {
118 has_permissive = true;
119 }
120 _ => {}
121 }
122 }
123 }
124 _ => {}
125 }
126 }
127
128 if has_or && has_permissive {
130 return LicenseFamily::Permissive;
131 }
132
133 if has_copyleft {
135 LicenseFamily::Copyleft
136 } else if has_weak_copyleft {
137 LicenseFamily::WeakCopyleft
138 } else if has_permissive {
139 LicenseFamily::Permissive
140 } else {
141 LicenseFamily::Other
142 }
143 } else {
144 self.family_from_substring()
146 }
147 }
148
149 fn family_from_substring(&self) -> LicenseFamily {
151 let expr_lower = self.expression.to_lowercase();
152 if expr_lower.contains("mit")
153 || expr_lower.contains("apache")
154 || expr_lower.contains("bsd")
155 || expr_lower.contains("isc")
156 || expr_lower.contains("unlicense")
157 {
158 LicenseFamily::Permissive
159 } else if expr_lower.contains("gpl")
160 || expr_lower.contains("agpl")
161 || expr_lower.contains("lgpl")
162 || expr_lower.contains("mpl")
163 {
164 LicenseFamily::Copyleft
165 } else if expr_lower.contains("proprietary") {
166 LicenseFamily::Proprietary
167 } else {
168 LicenseFamily::Other
169 }
170 }
171}
172
173fn classify_spdx_license(id: spdx::LicenseId) -> LicenseFamily {
175 let name = id.name;
176
177 if name == "CC0-1.0" || name == "Unlicense" || name == "0BSD" {
179 return LicenseFamily::PublicDomain;
180 }
181
182 if id.is_copyleft() {
183 let name_upper = name.to_uppercase();
185 if name_upper.contains("LGPL")
186 || name_upper.starts_with("MPL")
187 || name_upper.starts_with("EPL")
188 || name_upper.starts_with("CDDL")
189 || name_upper.starts_with("EUPL")
190 || name_upper.starts_with("OSL")
191 {
192 LicenseFamily::WeakCopyleft
193 } else {
194 LicenseFamily::Copyleft
195 }
196 } else if id.is_osi_approved() || id.is_fsf_free_libre() {
197 LicenseFamily::Permissive
198 } else {
199 LicenseFamily::Other
200 }
201}
202
203impl fmt::Display for LicenseExpression {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 write!(f, "{}", self.expression)
206 }
207}
208
209impl Default for LicenseExpression {
210 fn default() -> Self {
211 Self {
212 expression: "NOASSERTION".to_string(),
213 is_valid_spdx: false,
214 }
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
220pub enum LicenseFamily {
221 Permissive,
222 Copyleft,
223 WeakCopyleft,
224 Proprietary,
225 PublicDomain,
226 Other,
227}
228
229impl fmt::Display for LicenseFamily {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 match self {
232 LicenseFamily::Permissive => write!(f, "Permissive"),
233 LicenseFamily::Copyleft => write!(f, "Copyleft"),
234 LicenseFamily::WeakCopyleft => write!(f, "Weak Copyleft"),
235 LicenseFamily::Proprietary => write!(f, "Proprietary"),
236 LicenseFamily::PublicDomain => write!(f, "Public Domain"),
237 LicenseFamily::Other => write!(f, "Other"),
238 }
239 }
240}
241
242#[derive(Debug, Clone, Default, Serialize, Deserialize)]
244pub struct LicenseInfo {
245 pub declared: Vec<LicenseExpression>,
247 pub concluded: Option<LicenseExpression>,
249 pub evidence: Vec<LicenseEvidence>,
251}
252
253impl LicenseInfo {
254 pub fn new() -> Self {
256 Self::default()
257 }
258
259 pub fn add_declared(&mut self, license: LicenseExpression) {
261 self.declared.push(license);
262 }
263
264 pub fn all_licenses(&self) -> Vec<&LicenseExpression> {
266 let mut licenses: Vec<&LicenseExpression> = self.declared.iter().collect();
267 if let Some(concluded) = &self.concluded {
268 licenses.push(concluded);
269 }
270 licenses
271 }
272
273 pub fn has_conflicts(&self) -> bool {
279 let families: Vec<LicenseFamily> = self.declared.iter().map(|l| l.family()).collect();
280
281 let has_copyleft = families.contains(&LicenseFamily::Copyleft);
282 let has_proprietary = families.contains(&LicenseFamily::Proprietary);
283
284 has_copyleft && has_proprietary
285 }
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct LicenseEvidence {
291 pub license: LicenseExpression,
293 pub confidence: f64,
295 pub file_path: Option<String>,
297 pub line_number: Option<u32>,
299}
300
301impl LicenseEvidence {
302 pub fn new(license: LicenseExpression, confidence: f64) -> Self {
304 Self {
305 license,
306 confidence,
307 file_path: None,
308 line_number: None,
309 }
310 }
311}