| 1 | // SPDX-FileCopyrightText: 2022 HH Partners | 
| 2 | // | 
|---|
| 3 | // SPDX-License-Identifier: MIT | 
|---|
| 4 |  | 
|---|
| 5 | //! The main struct of the library. | 
|---|
| 6 |  | 
|---|
| 7 | use std::{collections::HashSet, fmt::Display, string::ToString}; | 
|---|
| 8 |  | 
|---|
| 9 | use serde::{de::Visitor, Deserialize, Serialize}; | 
|---|
| 10 |  | 
|---|
| 11 | use crate::{ | 
|---|
| 12 | error::SpdxExpressionError, | 
|---|
| 13 | expression_variant::{ExpressionVariant, SimpleExpression}, | 
|---|
| 14 | }; | 
|---|
| 15 |  | 
|---|
| 16 | /// Main struct for SPDX License Expressions. | 
|---|
| 17 | #[ derive(Debug, Clone, PartialEq, Eq)] | 
|---|
| 18 | pub struct SpdxExpression { | 
|---|
| 19 | /// The parsed expression. | 
|---|
| 20 | inner: ExpressionVariant, | 
|---|
| 21 | } | 
|---|
| 22 |  | 
|---|
| 23 | impl SpdxExpression { | 
|---|
| 24 | /// Parse `Self` from a string. The input expression needs to be a syntactically valid SPDX | 
|---|
| 25 | /// expression, `NONE` or `NOASSERTION`. The parser accepts license identifiers that are not | 
|---|
| 26 | /// valid SPDX. | 
|---|
| 27 | /// | 
|---|
| 28 | /// # Examples | 
|---|
| 29 | /// | 
|---|
| 30 | /// ``` | 
|---|
| 31 | /// # use spdx_expression::SpdxExpression; | 
|---|
| 32 | /// # use spdx_expression::SpdxExpressionError; | 
|---|
| 33 | /// # | 
|---|
| 34 | /// let expression = SpdxExpression::parse( "MIT")?; | 
|---|
| 35 | /// # Ok::<(), SpdxExpressionError>(()) | 
|---|
| 36 | /// ``` | 
|---|
| 37 | /// | 
|---|
| 38 | /// License expressions need to be syntactically valid, but they can include license | 
|---|
| 39 | /// identifiers not on the SPDX license list or not specified with `LicenseRef`. | 
|---|
| 40 | /// | 
|---|
| 41 | /// ``` | 
|---|
| 42 | /// # use spdx_expression::SpdxExpression; | 
|---|
| 43 | /// # use spdx_expression::SpdxExpressionError; | 
|---|
| 44 | /// # | 
|---|
| 45 | /// let expression = SpdxExpression::parse( "MIT OR InvalidLicenseId")?; | 
|---|
| 46 | /// # Ok::<(), SpdxExpressionError>(()) | 
|---|
| 47 | /// ``` | 
|---|
| 48 | /// | 
|---|
| 49 | /// # Errors | 
|---|
| 50 | /// | 
|---|
| 51 | /// Returns `SpdxExpressionError` if the license expression is not syntactically valid. | 
|---|
| 52 | pub fn parse(expression: &str) -> Result<Self, SpdxExpressionError> { | 
|---|
| 53 | Ok(Self { | 
|---|
| 54 | inner: ExpressionVariant::parse(expression) | 
|---|
| 55 | .map_err(|err| SpdxExpressionError::Parse(err.to_string()))?, | 
|---|
| 56 | }) | 
|---|
| 57 | } | 
|---|
| 58 |  | 
|---|
| 59 | /// Get all license and exception identifiers from the `SpdxExpression`. | 
|---|
| 60 | /// | 
|---|
| 61 | /// # Examples | 
|---|
| 62 | /// | 
|---|
| 63 | /// ``` | 
|---|
| 64 | /// # use std::collections::HashSet; | 
|---|
| 65 | /// # use std::iter::FromIterator; | 
|---|
| 66 | /// # use spdx_expression::SpdxExpression; | 
|---|
| 67 | /// # use spdx_expression::SpdxExpressionError; | 
|---|
| 68 | /// # | 
|---|
| 69 | /// let expression = SpdxExpression::parse( "MIT OR Apache-2.0")?; | 
|---|
| 70 | /// let licenses = expression.identifiers(); | 
|---|
| 71 | /// assert_eq!(licenses, HashSet::from_iter([ "Apache-2.0".to_string(), "MIT".to_string()])); | 
|---|
| 72 | /// # Ok::<(), SpdxExpressionError>(()) | 
|---|
| 73 | /// ``` | 
|---|
| 74 | /// | 
|---|
| 75 | /// ``` | 
|---|
| 76 | /// # use std::collections::HashSet; | 
|---|
| 77 | /// # use std::iter::FromIterator; | 
|---|
| 78 | /// # use spdx_expression::SpdxExpression; | 
|---|
| 79 | /// # use spdx_expression::SpdxExpressionError; | 
|---|
| 80 | /// # | 
|---|
| 81 | /// let expression = SpdxExpression::parse( "MIT OR GPL-2.0-only WITH Classpath-exception-2.0")?; | 
|---|
| 82 | /// let licenses = expression.identifiers(); | 
|---|
| 83 | /// assert_eq!( | 
|---|
| 84 | ///     licenses, | 
|---|
| 85 | ///     HashSet::from_iter([ | 
|---|
| 86 | /// "MIT".to_string(), | 
|---|
| 87 | /// "GPL-2.0-only".to_string(), | 
|---|
| 88 | /// "Classpath-exception-2.0".to_string() | 
|---|
| 89 | ///     ]) | 
|---|
| 90 | /// ); | 
|---|
| 91 | /// # Ok::<(), SpdxExpressionError>(()) | 
|---|
| 92 | /// ``` | 
|---|
| 93 | pub fn identifiers(&self) -> HashSet<String> { | 
|---|
| 94 | let mut identifiers = self | 
|---|
| 95 | .licenses() | 
|---|
| 96 | .iter() | 
|---|
| 97 | .map(ToString::to_string) | 
|---|
| 98 | .collect::<HashSet<_>>(); | 
|---|
| 99 |  | 
|---|
| 100 | identifiers.extend(self.exceptions().iter().map(ToString::to_string)); | 
|---|
| 101 |  | 
|---|
| 102 | identifiers | 
|---|
| 103 | } | 
|---|
| 104 |  | 
|---|
| 105 | /// Get all simple license expressions in `Self`. For licenses with exceptions, returns the | 
|---|
| 106 | /// license without the exception | 
|---|
| 107 | /// | 
|---|
| 108 | /// # Examples | 
|---|
| 109 | /// | 
|---|
| 110 | /// ``` | 
|---|
| 111 | /// # use std::collections::HashSet; | 
|---|
| 112 | /// # use std::iter::FromIterator; | 
|---|
| 113 | /// # use spdx_expression::SpdxExpression; | 
|---|
| 114 | /// # use spdx_expression::SpdxExpressionError; | 
|---|
| 115 | /// # | 
|---|
| 116 | /// let expression = SpdxExpression::parse( | 
|---|
| 117 | /// "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", | 
|---|
| 118 | /// ) | 
|---|
| 119 | /// .unwrap(); | 
|---|
| 120 | /// | 
|---|
| 121 | /// let licenses = expression.licenses(); | 
|---|
| 122 | /// | 
|---|
| 123 | /// assert_eq!( | 
|---|
| 124 | ///     licenses | 
|---|
| 125 | ///         .iter() | 
|---|
| 126 | ///         .map(|&license| license.identifier.as_str()) | 
|---|
| 127 | ///         .collect::<HashSet<_>>(), | 
|---|
| 128 | ///     HashSet::from_iter([ "Apache-2.0", "GPL-2.0-only", "ISC", "MIT"]) | 
|---|
| 129 | /// ); | 
|---|
| 130 | /// # Ok::<(), SpdxExpressionError>(()) | 
|---|
| 131 | /// ``` | 
|---|
| 132 | pub fn licenses(&self) -> HashSet<&SimpleExpression> { | 
|---|
| 133 | self.inner.licenses() | 
|---|
| 134 | } | 
|---|
| 135 |  | 
|---|
| 136 | /// Get all exception identifiers for `Self`. | 
|---|
| 137 | /// | 
|---|
| 138 | /// # Examples | 
|---|
| 139 | /// | 
|---|
| 140 | /// ``` | 
|---|
| 141 | /// # use std::collections::HashSet; | 
|---|
| 142 | /// # use std::iter::FromIterator; | 
|---|
| 143 | /// # use spdx_expression::SpdxExpression; | 
|---|
| 144 | /// # use spdx_expression::SpdxExpressionError; | 
|---|
| 145 | /// # | 
|---|
| 146 | /// let expression = SpdxExpression::parse( | 
|---|
| 147 | /// "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", | 
|---|
| 148 | /// ) | 
|---|
| 149 | /// .unwrap(); | 
|---|
| 150 | /// | 
|---|
| 151 | /// let exceptions = expression.exceptions(); | 
|---|
| 152 | /// | 
|---|
| 153 | /// assert_eq!(exceptions, HashSet::from_iter([ "Classpath-exception-2.0"])); | 
|---|
| 154 | /// # Ok::<(), SpdxExpressionError>(()) | 
|---|
| 155 | /// ``` | 
|---|
| 156 | pub fn exceptions(&self) -> HashSet<&str> { | 
|---|
| 157 | self.inner.exceptions() | 
|---|
| 158 | } | 
|---|
| 159 | } | 
|---|
| 160 |  | 
|---|
| 161 | impl Default for SpdxExpression { | 
|---|
| 162 | fn default() -> Self { | 
|---|
| 163 | Self::parse( "NOASSERTION").expect(msg: "will not fail") | 
|---|
| 164 | } | 
|---|
| 165 | } | 
|---|
| 166 |  | 
|---|
| 167 | impl Serialize for SpdxExpression { | 
|---|
| 168 | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | 
|---|
| 169 | where | 
|---|
| 170 | S: serde::Serializer, | 
|---|
| 171 | { | 
|---|
| 172 | serializer.collect_str(self) | 
|---|
| 173 | } | 
|---|
| 174 | } | 
|---|
| 175 |  | 
|---|
| 176 | struct SpdxExpressionVisitor; | 
|---|
| 177 |  | 
|---|
| 178 | impl<'de> Visitor<'de> for SpdxExpressionVisitor { | 
|---|
| 179 | type Value = SpdxExpression; | 
|---|
| 180 |  | 
|---|
| 181 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { | 
|---|
| 182 | formatter.write_str( "a syntactically valid SPDX expression") | 
|---|
| 183 | } | 
|---|
| 184 |  | 
|---|
| 185 | fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> | 
|---|
| 186 | where | 
|---|
| 187 | E: serde::de::Error, | 
|---|
| 188 | { | 
|---|
| 189 | SpdxExpression::parse(v) | 
|---|
| 190 | .map_err(|err| E::custom(format!( "error parsing the expression: {} ", err))) | 
|---|
| 191 | } | 
|---|
| 192 |  | 
|---|
| 193 | fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> | 
|---|
| 194 | where | 
|---|
| 195 | E: serde::de::Error, | 
|---|
| 196 | { | 
|---|
| 197 | self.visit_str(v) | 
|---|
| 198 | } | 
|---|
| 199 |  | 
|---|
| 200 | fn visit_string<E>(self, v: String) -> Result<Self::Value, E> | 
|---|
| 201 | where | 
|---|
| 202 | E: serde::de::Error, | 
|---|
| 203 | { | 
|---|
| 204 | self.visit_str(&v) | 
|---|
| 205 | } | 
|---|
| 206 | } | 
|---|
| 207 |  | 
|---|
| 208 | impl<'de> Deserialize<'de> for SpdxExpression { | 
|---|
| 209 | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | 
|---|
| 210 | where | 
|---|
| 211 | D: serde::Deserializer<'de>, | 
|---|
| 212 | { | 
|---|
| 213 | deserializer.deserialize_str(visitor:SpdxExpressionVisitor) | 
|---|
| 214 | } | 
|---|
| 215 | } | 
|---|
| 216 |  | 
|---|
| 217 | impl Display for SpdxExpression { | 
|---|
| 218 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | 
|---|
| 219 | write!(f, "{} ", self.inner) | 
|---|
| 220 | } | 
|---|
| 221 | } | 
|---|
| 222 |  | 
|---|
| 223 | #[ cfg(test)] | 
|---|
| 224 | mod tests { | 
|---|
| 225 | use std::iter::FromIterator; | 
|---|
| 226 |  | 
|---|
| 227 | use serde_json::Value; | 
|---|
| 228 |  | 
|---|
| 229 | use super::*; | 
|---|
| 230 |  | 
|---|
| 231 | #[ test] | 
|---|
| 232 | fn test_parsing_works() { | 
|---|
| 233 | let expression = SpdxExpression::parse( "MIT AND (Apache-2.0 OR ISC)").unwrap(); | 
|---|
| 234 | assert_eq!(expression.to_string(), "MIT AND (Apache-2.0 OR ISC)"); | 
|---|
| 235 | } | 
|---|
| 236 |  | 
|---|
| 237 | #[ test] | 
|---|
| 238 | fn test_identifiers_from_simple_expression() { | 
|---|
| 239 | let expression = SpdxExpression::parse( "MIT").unwrap(); | 
|---|
| 240 | let licenses = expression.identifiers(); | 
|---|
| 241 | assert_eq!(licenses, HashSet::from_iter([ "MIT".to_string()])); | 
|---|
| 242 | } | 
|---|
| 243 |  | 
|---|
| 244 | #[ test] | 
|---|
| 245 | fn test_identifiers_from_compound_or_expression() { | 
|---|
| 246 | let expression = SpdxExpression::parse( "MIT OR Apache-2.0").unwrap(); | 
|---|
| 247 | let licenses = expression.identifiers(); | 
|---|
| 248 | assert_eq!( | 
|---|
| 249 | licenses, | 
|---|
| 250 | HashSet::from_iter([ "Apache-2.0".to_string(), "MIT".to_string()]) | 
|---|
| 251 | ); | 
|---|
| 252 | } | 
|---|
| 253 |  | 
|---|
| 254 | #[ test] | 
|---|
| 255 | fn test_identifiers_from_compound_parentheses_expression() { | 
|---|
| 256 | let expression = SpdxExpression::parse( | 
|---|
| 257 | "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", | 
|---|
| 258 | ) | 
|---|
| 259 | .unwrap(); | 
|---|
| 260 | let licenses = expression.identifiers(); | 
|---|
| 261 | assert_eq!( | 
|---|
| 262 | licenses, | 
|---|
| 263 | HashSet::from_iter([ | 
|---|
| 264 | "Apache-2.0".to_string(), | 
|---|
| 265 | "Classpath-exception-2.0".to_string(), | 
|---|
| 266 | "GPL-2.0-only".to_string(), | 
|---|
| 267 | "ISC".to_string(), | 
|---|
| 268 | "MIT".to_string() | 
|---|
| 269 | ]) | 
|---|
| 270 | ); | 
|---|
| 271 | } | 
|---|
| 272 |  | 
|---|
| 273 | #[ test] | 
|---|
| 274 | fn test_licenses_from_compound_parentheses_expression() { | 
|---|
| 275 | let expression = SpdxExpression::parse( | 
|---|
| 276 | "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", | 
|---|
| 277 | ) | 
|---|
| 278 | .unwrap(); | 
|---|
| 279 |  | 
|---|
| 280 | let licenses = expression.licenses(); | 
|---|
| 281 |  | 
|---|
| 282 | assert_eq!( | 
|---|
| 283 | licenses | 
|---|
| 284 | .iter() | 
|---|
| 285 | .map(|&license| license.identifier.as_str()) | 
|---|
| 286 | .collect::<HashSet<_>>(), | 
|---|
| 287 | HashSet::from_iter([ "Apache-2.0", "GPL-2.0-only", "ISC", "MIT"]) | 
|---|
| 288 | ); | 
|---|
| 289 | } | 
|---|
| 290 |  | 
|---|
| 291 | #[ test] | 
|---|
| 292 | fn test_exceptions_from_compound_parentheses_expression() { | 
|---|
| 293 | let expression = SpdxExpression::parse( | 
|---|
| 294 | "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", | 
|---|
| 295 | ) | 
|---|
| 296 | .unwrap(); | 
|---|
| 297 |  | 
|---|
| 298 | let exceptions = expression.exceptions(); | 
|---|
| 299 |  | 
|---|
| 300 | assert_eq!(exceptions, HashSet::from_iter([ "Classpath-exception-2.0"])); | 
|---|
| 301 | } | 
|---|
| 302 |  | 
|---|
| 303 | #[ test] | 
|---|
| 304 | fn serialize_expression_correctly() { | 
|---|
| 305 | let expression = SpdxExpression::parse( "MIT OR ISC").unwrap(); | 
|---|
| 306 |  | 
|---|
| 307 | let value = serde_json::to_value(expression).unwrap(); | 
|---|
| 308 |  | 
|---|
| 309 | assert_eq!(value, Value::String( "MIT OR ISC".to_string())); | 
|---|
| 310 | } | 
|---|
| 311 |  | 
|---|
| 312 | #[ test] | 
|---|
| 313 | fn deserialize_expression_correctly() { | 
|---|
| 314 | let expected = SpdxExpression::parse( "MIT OR ISC").unwrap(); | 
|---|
| 315 |  | 
|---|
| 316 | let value = Value::String( "MIT OR ISC".to_string()); | 
|---|
| 317 |  | 
|---|
| 318 | let actual: SpdxExpression = serde_json::from_value(value).unwrap(); | 
|---|
| 319 |  | 
|---|
| 320 | assert_eq!(actual, expected); | 
|---|
| 321 | } | 
|---|
| 322 | } | 
|---|
| 323 |  | 
|---|