diff options
Diffstat (limited to 'vendor/spdx-expression/src')
-rw-r--r-- | vendor/spdx-expression/src/error.rs | 21 | ||||
-rw-r--r-- | vendor/spdx-expression/src/expression.rs | 322 | ||||
-rw-r--r-- | vendor/spdx-expression/src/expression_variant.rs | 451 | ||||
-rw-r--r-- | vendor/spdx-expression/src/lib.rs | 16 | ||||
-rw-r--r-- | vendor/spdx-expression/src/parser.rs | 409 |
5 files changed, 1219 insertions, 0 deletions
diff --git a/vendor/spdx-expression/src/error.rs b/vendor/spdx-expression/src/error.rs new file mode 100644 index 000000000..7e432f3d4 --- /dev/null +++ b/vendor/spdx-expression/src/error.rs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2022 HH Partners +// +// SPDX-License-Identifier: MIT + +//! Errors of the library. + +/// Custom error struct. +#[derive(thiserror::Error, Debug)] +pub enum SpdxExpressionError { + #[error("Parsing for expression `{0}` failed.")] + Parse(String), + + #[error("Error parsing the SPDX Expression {0}.")] + Nom(String), +} + +impl From<nom::Err<nom::error::Error<&str>>> for SpdxExpressionError { + fn from(err: nom::Err<nom::error::Error<&str>>) -> Self { + Self::Nom(err.to_string()) + } +} diff --git a/vendor/spdx-expression/src/expression.rs b/vendor/spdx-expression/src/expression.rs new file mode 100644 index 000000000..a303dbae7 --- /dev/null +++ b/vendor/spdx-expression/src/expression.rs @@ -0,0 +1,322 @@ +// SPDX-FileCopyrightText: 2022 HH Partners +// +// SPDX-License-Identifier: MIT + +//! The main struct of the library. + +use std::{collections::HashSet, fmt::Display, string::ToString}; + +use serde::{de::Visitor, Deserialize, Serialize}; + +use crate::{ + error::SpdxExpressionError, + expression_variant::{ExpressionVariant, SimpleExpression}, +}; + +/// Main struct for SPDX License Expressions. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpdxExpression { + /// The parsed expression. + inner: ExpressionVariant, +} + +impl SpdxExpression { + /// Parse `Self` from a string. The input expression needs to be a syntactically valid SPDX + /// expression, `NONE` or `NOASSERTION`. The parser accepts license identifiers that are not + /// valid SPDX. + /// + /// # Examples + /// + /// ``` + /// # use spdx_expression::SpdxExpression; + /// # use spdx_expression::SpdxExpressionError; + /// # + /// let expression = SpdxExpression::parse("MIT")?; + /// # Ok::<(), SpdxExpressionError>(()) + /// ``` + /// + /// License expressions need to be syntactically valid, but they can include license + /// identifiers not on the SPDX license list or not specified with `LicenseRef`. + /// + /// ``` + /// # use spdx_expression::SpdxExpression; + /// # use spdx_expression::SpdxExpressionError; + /// # + /// let expression = SpdxExpression::parse("MIT OR InvalidLicenseId")?; + /// # Ok::<(), SpdxExpressionError>(()) + /// ``` + /// + /// # Errors + /// + /// Returns `SpdxExpressionError` if the license expression is not syntactically valid. + pub fn parse(expression: &str) -> Result<Self, SpdxExpressionError> { + Ok(Self { + inner: ExpressionVariant::parse(expression) + .map_err(|err| SpdxExpressionError::Parse(err.to_string()))?, + }) + } + + /// Get all license and exception identifiers from the `SpdxExpression`. + /// + /// # Examples + /// + /// ``` + /// # use std::collections::HashSet; + /// # use std::iter::FromIterator; + /// # use spdx_expression::SpdxExpression; + /// # use spdx_expression::SpdxExpressionError; + /// # + /// let expression = SpdxExpression::parse("MIT OR Apache-2.0")?; + /// let licenses = expression.identifiers(); + /// assert_eq!(licenses, HashSet::from_iter(["Apache-2.0".to_string(), "MIT".to_string()])); + /// # Ok::<(), SpdxExpressionError>(()) + /// ``` + /// + /// ``` + /// # use std::collections::HashSet; + /// # use std::iter::FromIterator; + /// # use spdx_expression::SpdxExpression; + /// # use spdx_expression::SpdxExpressionError; + /// # + /// let expression = SpdxExpression::parse("MIT OR GPL-2.0-only WITH Classpath-exception-2.0")?; + /// let licenses = expression.identifiers(); + /// assert_eq!( + /// licenses, + /// HashSet::from_iter([ + /// "MIT".to_string(), + /// "GPL-2.0-only".to_string(), + /// "Classpath-exception-2.0".to_string() + /// ]) + /// ); + /// # Ok::<(), SpdxExpressionError>(()) + /// ``` + pub fn identifiers(&self) -> HashSet<String> { + let mut identifiers = self + .licenses() + .iter() + .map(ToString::to_string) + .collect::<HashSet<_>>(); + + identifiers.extend(self.exceptions().iter().map(ToString::to_string)); + + identifiers + } + + /// Get all simple license expressions in `Self`. For licenses with exceptions, returns the + /// license without the exception + /// + /// # Examples + /// + /// ``` + /// # use std::collections::HashSet; + /// # use std::iter::FromIterator; + /// # use spdx_expression::SpdxExpression; + /// # use spdx_expression::SpdxExpressionError; + /// # + /// let expression = SpdxExpression::parse( + /// "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", + /// ) + /// .unwrap(); + /// + /// let licenses = expression.licenses(); + /// + /// assert_eq!( + /// licenses + /// .iter() + /// .map(|&license| license.identifier.as_str()) + /// .collect::<HashSet<_>>(), + /// HashSet::from_iter(["Apache-2.0", "GPL-2.0-only", "ISC", "MIT"]) + /// ); + /// # Ok::<(), SpdxExpressionError>(()) + /// ``` + pub fn licenses(&self) -> HashSet<&SimpleExpression> { + self.inner.licenses() + } + + /// Get all exception identifiers for `Self`. + /// + /// # Examples + /// + /// ``` + /// # use std::collections::HashSet; + /// # use std::iter::FromIterator; + /// # use spdx_expression::SpdxExpression; + /// # use spdx_expression::SpdxExpressionError; + /// # + /// let expression = SpdxExpression::parse( + /// "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", + /// ) + /// .unwrap(); + /// + /// let exceptions = expression.exceptions(); + /// + /// assert_eq!(exceptions, HashSet::from_iter(["Classpath-exception-2.0"])); + /// # Ok::<(), SpdxExpressionError>(()) + /// ``` + pub fn exceptions(&self) -> HashSet<&str> { + self.inner.exceptions() + } +} + +impl Default for SpdxExpression { + fn default() -> Self { + Self::parse("NOASSERTION").expect("will not fail") + } +} + +impl Serialize for SpdxExpression { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.collect_str(self) + } +} + +struct SpdxExpressionVisitor; + +impl<'de> Visitor<'de> for SpdxExpressionVisitor { + type Value = SpdxExpression; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a syntactically valid SPDX expression") + } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + SpdxExpression::parse(v) + .map_err(|err| E::custom(format!("error parsing the expression: {}", err))) + } + + fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + self.visit_str(v) + } + + fn visit_string<E>(self, v: String) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + self.visit_str(&v) + } +} + +impl<'de> Deserialize<'de> for SpdxExpression { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(SpdxExpressionVisitor) + } +} + +impl Display for SpdxExpression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +#[cfg(test)] +mod tests { + use std::iter::FromIterator; + + use serde_json::Value; + + use super::*; + + #[test] + fn test_parsing_works() { + let expression = SpdxExpression::parse("MIT AND (Apache-2.0 OR ISC)").unwrap(); + assert_eq!(expression.to_string(), "MIT AND (Apache-2.0 OR ISC)"); + } + + #[test] + fn test_identifiers_from_simple_expression() { + let expression = SpdxExpression::parse("MIT").unwrap(); + let licenses = expression.identifiers(); + assert_eq!(licenses, HashSet::from_iter(["MIT".to_string()])); + } + + #[test] + fn test_identifiers_from_compound_or_expression() { + let expression = SpdxExpression::parse("MIT OR Apache-2.0").unwrap(); + let licenses = expression.identifiers(); + assert_eq!( + licenses, + HashSet::from_iter(["Apache-2.0".to_string(), "MIT".to_string()]) + ); + } + + #[test] + fn test_identifiers_from_compound_parentheses_expression() { + let expression = SpdxExpression::parse( + "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", + ) + .unwrap(); + let licenses = expression.identifiers(); + assert_eq!( + licenses, + HashSet::from_iter([ + "Apache-2.0".to_string(), + "Classpath-exception-2.0".to_string(), + "GPL-2.0-only".to_string(), + "ISC".to_string(), + "MIT".to_string() + ]) + ); + } + + #[test] + fn test_licenses_from_compound_parentheses_expression() { + let expression = SpdxExpression::parse( + "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", + ) + .unwrap(); + + let licenses = expression.licenses(); + + assert_eq!( + licenses + .iter() + .map(|&license| license.identifier.as_str()) + .collect::<HashSet<_>>(), + HashSet::from_iter(["Apache-2.0", "GPL-2.0-only", "ISC", "MIT"]) + ); + } + + #[test] + fn test_exceptions_from_compound_parentheses_expression() { + let expression = SpdxExpression::parse( + "(MIT OR Apache-2.0 AND (GPL-2.0-only WITH Classpath-exception-2.0 OR ISC))", + ) + .unwrap(); + + let exceptions = expression.exceptions(); + + assert_eq!(exceptions, HashSet::from_iter(["Classpath-exception-2.0"])); + } + + #[test] + fn serialize_expression_correctly() { + let expression = SpdxExpression::parse("MIT OR ISC").unwrap(); + + let value = serde_json::to_value(expression).unwrap(); + + assert_eq!(value, Value::String("MIT OR ISC".to_string())); + } + + #[test] + fn deserialize_expression_correctly() { + let expected = SpdxExpression::parse("MIT OR ISC").unwrap(); + + let value = Value::String("MIT OR ISC".to_string()); + + let actual: SpdxExpression = serde_json::from_value(value).unwrap(); + + assert_eq!(actual, expected); + } +} diff --git a/vendor/spdx-expression/src/expression_variant.rs b/vendor/spdx-expression/src/expression_variant.rs new file mode 100644 index 000000000..626d8e2e1 --- /dev/null +++ b/vendor/spdx-expression/src/expression_variant.rs @@ -0,0 +1,451 @@ +// SPDX-FileCopyrightText: 2022 HH Partners +// +// SPDX-License-Identifier: MIT + +//! Private inner structs for [`crate::SpdxExpression`]. + +use std::{collections::HashSet, fmt::Display}; + +use nom::Finish; +use serde::{de::Visitor, Deserialize, Serialize}; + +use crate::{ + error::SpdxExpressionError, + parser::{parse_expression, simple_expression}, +}; + +/// Simple SPDX license expression. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SimpleExpression { + /// The license identifier. + pub identifier: String, + + /// Optional DocumentRef for the expression. + pub document_ref: Option<String>, + + /// `true` if the expression is a user defined license reference. + pub license_ref: bool, +} + +impl Serialize for SimpleExpression { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.collect_str(self) + } +} + +struct SimpleExpressionVisitor; + +impl<'de> Visitor<'de> for SimpleExpressionVisitor { + type Value = SimpleExpression; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a syntactically valid SPDX simple expression") + } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + SimpleExpression::parse(v) + .map_err(|err| E::custom(format!("error parsing the expression: {}", err))) + } + + fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + self.visit_str(v) + } + + fn visit_string<E>(self, v: String) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + self.visit_str(&v) + } +} + +impl<'de> Deserialize<'de> for SimpleExpression { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(SimpleExpressionVisitor) + } +} + +impl Display for SimpleExpression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let document_ref = match &self.document_ref { + Some(document_ref) => { + format!("DocumentRef-{}:", document_ref) + } + None => "".to_string(), + }; + + let license_ref = if self.license_ref { "LicenseRef-" } else { "" }; + write!( + f, + "{document_ref}{license_ref}{identifier}", + identifier = self.identifier + ) + } +} + +impl SimpleExpression { + /// Create a new simple expression. + pub const fn new(identifier: String, document_ref: Option<String>, license_ref: bool) -> Self { + Self { + identifier, + document_ref, + license_ref, + } + } + + /// Parse a simple expression. + /// + /// # Examples + /// + /// ``` + /// # use spdx_expression::SimpleExpression; + /// # use spdx_expression::SpdxExpressionError; + /// # + /// let expression = SimpleExpression::parse("MIT")?; + /// # Ok::<(), SpdxExpressionError>(()) + /// ``` + /// + /// The function will only accept simple expressions, compound expressions will fail. + /// + /// ``` + /// # use spdx_expression::SimpleExpression; + /// # + /// let expression = SimpleExpression::parse("MIT OR ISC"); + /// assert!(expression.is_err()); + /// ``` + /// + /// # Errors + /// + /// Fails if parsing fails. + pub fn parse(expression: &str) -> Result<Self, SpdxExpressionError> { + let (remaining, result) = simple_expression(expression)?; + + if remaining.is_empty() { + Ok(result) + } else { + Err(SpdxExpressionError::Parse(expression.to_string())) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WithExpression { + pub license: SimpleExpression, + pub exception: String, +} + +impl WithExpression { + pub const fn new(license: SimpleExpression, exception: String) -> Self { + Self { license, exception } + } +} + +impl Display for WithExpression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{license} WITH {exception}", + license = self.license, + exception = self.exception + ) + } +} + +#[derive(Debug, PartialEq, Clone, Eq)] +pub enum ExpressionVariant { + Simple(SimpleExpression), + With(WithExpression), + And(Box<Self>, Box<Self>), + Or(Box<Self>, Box<Self>), + Parens(Box<Self>), +} + +impl Display for ExpressionVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use self::ExpressionVariant::{And, Or, Parens, Simple, With}; + + match self { + Simple(expression) => write!(f, "{expression}"), + With(expression) => write!(f, "{expression}"), + And(left, right) => write!(f, "{left} AND {right}"), + Or(left, right) => write!(f, "{left} OR {right}"), + Parens(expression) => write!(f, "({expression})"), + } + } +} + +impl ExpressionVariant { + pub fn parse(i: &str) -> Result<Self, SpdxExpressionError> { + let (remaining, expression) = parse_expression(i) + .finish() + .map_err(|_| SpdxExpressionError::Parse(i.to_string()))?; + + if remaining.is_empty() { + Ok(expression) + } else { + Err(SpdxExpressionError::Parse(i.to_string())) + } + } + + pub fn licenses(&self) -> HashSet<&SimpleExpression> { + let mut expressions = HashSet::new(); + + match self { + ExpressionVariant::Simple(expression) => { + expressions.insert(expression); + } + ExpressionVariant::With(expression) => { + expressions.insert(&expression.license); + } + ExpressionVariant::And(left, right) | ExpressionVariant::Or(left, right) => { + expressions.extend(left.licenses()); + expressions.extend(right.licenses()); + } + ExpressionVariant::Parens(expression) => { + expressions.extend(expression.licenses()); + } + } + + expressions + } + + pub fn exceptions(&self) -> HashSet<&str> { + let mut expressions = HashSet::new(); + + match self { + ExpressionVariant::Simple(_) => {} + ExpressionVariant::With(expression) => { + expressions.insert(expression.exception.as_str()); + } + ExpressionVariant::And(left, right) | ExpressionVariant::Or(left, right) => { + expressions.extend(left.exceptions()); + expressions.extend(right.exceptions()); + } + ExpressionVariant::Parens(expression) => { + expressions.extend(expression.exceptions()); + } + } + + expressions + } +} + +#[cfg(test)] +mod tests { + use std::iter::FromIterator; + + use serde_json::Value; + + use super::*; + + #[test] + fn display_simple_correctly() { + let expression = + ExpressionVariant::Simple(SimpleExpression::new("MIT".to_string(), None, false)); + assert_eq!(expression.to_string(), "MIT".to_string()); + } + + #[test] + fn display_licenseref_correctly() { + let expression = + ExpressionVariant::Simple(SimpleExpression::new("license".to_string(), None, true)); + assert_eq!(expression.to_string(), "LicenseRef-license".to_string()); + } + + #[test] + fn display_documentref_correctly() { + let expression = ExpressionVariant::Simple(SimpleExpression::new( + "license".to_string(), + Some("document".to_string()), + true, + )); + assert_eq!( + expression.to_string(), + "DocumentRef-document:LicenseRef-license".to_string() + ); + } + + #[test] + fn display_with_expression_correctly() { + let expression = ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license".to_string(), None, false), + "exception".to_string(), + )); + assert_eq!(expression.to_string(), "license WITH exception".to_string()); + } + + #[test] + fn display_and_expression_correctly() { + let expression = ExpressionVariant::And( + Box::new(ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1".to_string(), + None, + false, + ))), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license2".to_string(), + None, + false, + ))), + )), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license3".to_string(), + None, + false, + ))), + ); + assert_eq!( + expression.to_string(), + "license1 AND license2 AND license3".to_string() + ); + } + + #[test] + fn display_or_expression_correctly() { + let expression = ExpressionVariant::Or( + Box::new(ExpressionVariant::Or( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1".to_string(), + None, + false, + ))), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license2".to_string(), + None, + false, + ))), + )), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license3".to_string(), + None, + false, + ))), + ); + assert_eq!( + expression.to_string(), + "license1 OR license2 OR license3".to_string() + ); + } + + #[test] + fn get_licenses_correctly() { + let expression = ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1+".to_string(), + None, + false, + ))), + Box::new(ExpressionVariant::Parens(Box::new(ExpressionVariant::Or( + Box::new(ExpressionVariant::Parens(Box::new( + ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license2".to_string(), None, false), + "exception1".to_string(), + )), + ))), + Box::new(ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license3+".to_string(), + None, + false, + ))), + Box::new(ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license4".to_string(), None, false), + "exception2".to_string(), + ))), + )), + )))), + ); + + assert_eq!( + expression.licenses(), + HashSet::from_iter([ + &SimpleExpression::new("license1+".to_string(), None, false), + &SimpleExpression::new("license2".to_string(), None, false), + &SimpleExpression::new("license3+".to_string(), None, false), + &SimpleExpression::new("license4".to_string(), None, false), + ]) + ); + } + #[test] + fn get_exceptions_correctly() { + let expression = ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1+".to_string(), + None, + false, + ))), + Box::new(ExpressionVariant::Parens(Box::new(ExpressionVariant::Or( + Box::new(ExpressionVariant::Parens(Box::new( + ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license2".to_string(), None, false), + "exception1".to_string(), + )), + ))), + Box::new(ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license3+".to_string(), + None, + false, + ))), + Box::new(ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license4".to_string(), None, false), + "exception2".to_string(), + ))), + )), + )))), + ); + + assert_eq!( + expression.exceptions(), + HashSet::from_iter(["exception1", "exception2"]) + ); + } + + #[test] + fn parse_simple_expression() { + let expression = SimpleExpression::parse("MIT").unwrap(); + assert_eq!( + expression, + SimpleExpression::new("MIT".to_string(), None, false) + ); + + let expression = SimpleExpression::parse("MIT OR ISC"); + assert!(expression.is_err()); + + let expression = SimpleExpression::parse("GPL-2.0-only WITH Classpath-exception-2.0"); + assert!(expression.is_err()); + } + + #[test] + fn serialize_simple_expression_correctly() { + let expression = SimpleExpression::parse("MIT").unwrap(); + + let value = serde_json::to_value(expression).unwrap(); + + assert_eq!(value, Value::String("MIT".to_string())); + } + + #[test] + fn deserialize_simple_expression_correctly() { + let expected = SimpleExpression::parse("LicenseRef-license1").unwrap(); + + let value = Value::String("LicenseRef-license1".to_string()); + + let actual: SimpleExpression = serde_json::from_value(value).unwrap(); + + assert_eq!(actual, expected); + } +} diff --git a/vendor/spdx-expression/src/lib.rs b/vendor/spdx-expression/src/lib.rs new file mode 100644 index 000000000..e790ec7f6 --- /dev/null +++ b/vendor/spdx-expression/src/lib.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2022 HH Partners +// +// SPDX-License-Identifier: MIT + +#![doc = include_str!("../README.md")] +#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)] + +mod error; +mod expression; +mod expression_variant; +mod parser; + +pub use error::SpdxExpressionError; +pub use expression::SpdxExpression; +pub use expression_variant::SimpleExpression; diff --git a/vendor/spdx-expression/src/parser.rs b/vendor/spdx-expression/src/parser.rs new file mode 100644 index 000000000..cb93c5b2a --- /dev/null +++ b/vendor/spdx-expression/src/parser.rs @@ -0,0 +1,409 @@ +// SPDX-FileCopyrightText: 2022 HH Partners +// +// SPDX-License-Identifier: MIT + +//! Parsing logic for SPDX Expressions. +//! +//! The code is heavily inspired by +//! <https://github.com/Geal/nom/blob/294ffb3d9e0ade2c3b7ddfff52484b6d643dcce1/tests/arithmetic_ast.rs> +//! which is licensed under the MIT License. The source project includes the following copyright +//! statement: Copyright (c) 2014-2019 Geoffroy Couprie. + +use nom::{ + branch::alt, + bytes::complete::{tag, tag_no_case, take_while1}, + character::{ + complete::{multispace0, multispace1}, + streaming::char, + }, + combinator::{complete, map, opt, recognize}, + multi::many0, + sequence::{delimited, pair, preceded, separated_pair}, + AsChar, IResult, +}; + +use crate::expression_variant::{ExpressionVariant, SimpleExpression, WithExpression}; + +#[derive(Debug)] +enum Operator { + And, + Or, +} + +fn parentheses(i: &str) -> IResult<&str, ExpressionVariant> { + delimited( + multispace0, + delimited( + tag("("), + map(or_expression, |e| ExpressionVariant::Parens(Box::new(e))), + tag(")"), + ), + multispace0, + )(i) +} + +fn terminal_expression(i: &str) -> IResult<&str, ExpressionVariant> { + alt(( + delimited(multispace0, with_expression, multispace0), + map( + delimited(multispace0, simple_expression, multispace0), + ExpressionVariant::Simple, + ), + parentheses, + ))(i) +} + +fn with_expression(i: &str) -> IResult<&str, ExpressionVariant> { + map( + separated_pair( + simple_expression, + delimited(multispace1, tag_no_case("WITH"), multispace1), + idstring, + ), + |(lic, exc)| ExpressionVariant::With(WithExpression::new(lic, exc.to_string())), + )(i) +} + +fn fold_expressions( + initial: ExpressionVariant, + remainder: Vec<(Operator, ExpressionVariant)>, +) -> ExpressionVariant { + remainder.into_iter().fold(initial, |acc, pair| { + let (oper, expr) = pair; + match oper { + Operator::And => ExpressionVariant::And(Box::new(acc), Box::new(expr)), + Operator::Or => ExpressionVariant::Or(Box::new(acc), Box::new(expr)), + } + }) +} + +fn and_expression(i: &str) -> IResult<&str, ExpressionVariant> { + let (i, initial) = terminal_expression(i)?; + let (i, remainder) = many0(|i| { + let (i, and) = preceded(tag_no_case("AND"), terminal_expression)(i)?; + Ok((i, (Operator::And, and))) + })(i)?; + + Ok((i, fold_expressions(initial, remainder))) +} + +fn or_expression(i: &str) -> IResult<&str, ExpressionVariant> { + let (i, initial) = and_expression(i)?; + let (i, remainder) = many0(|i| { + let (i, or) = preceded(tag_no_case("OR"), and_expression)(i)?; + Ok((i, (Operator::Or, or))) + })(i)?; + + Ok((i, fold_expressions(initial, remainder))) +} + +pub fn parse_expression(i: &str) -> IResult<&str, ExpressionVariant> { + or_expression(i) +} + +fn idstring(i: &str) -> IResult<&str, &str> { + take_while1(|c: char| c.is_alphanum() || c == '-' || c == '.')(i) +} + +fn license_idstring(i: &str) -> IResult<&str, &str> { + recognize(pair(idstring, opt(complete(char('+')))))(i) +} + +fn document_ref(i: &str) -> IResult<&str, &str> { + delimited(tag("DocumentRef-"), idstring, char(':'))(i) +} + +fn license_ref(i: &str) -> IResult<&str, (Option<&str>, &str)> { + separated_pair(opt(document_ref), tag("LicenseRef-"), idstring)(i) +} + +pub fn simple_expression(i: &str) -> IResult<&str, SimpleExpression> { + alt(( + map(license_ref, |(document_ref, id)| { + let document_ref = document_ref.map(std::string::ToString::to_string); + SimpleExpression::new(id.to_string(), document_ref, true) + }), + map(license_idstring, |id| { + SimpleExpression::new(id.to_string(), None, false) + }), + ))(i) +} + +#[cfg(test)] +mod tests { + //! A lot of the test cases for parsing are copied from + //! <https://github.com/oss-review-toolkit/ort/blob/6eb18b6d36f59c6d7ec221bad1cf5d4cd6acfc8b/utils/spdx/src/test/kotlin/SpdxExpressionParserTest.kt> + //! which is licensed under the Apache License, Version 2.0 and includes the following copyright + //! statement: + //! Copyright (C) 2017-2019 HERE Europe B.V. + + use super::*; + + use pretty_assertions::assert_eq; + + #[test] + fn parse_a_license_id_correctly() { + let parsed = ExpressionVariant::parse("spdx.license-id").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::Simple(SimpleExpression::new( + "spdx.license-id".to_string(), + None, + false + )) + ); + } + + #[test] + fn parse_a_license_id_starting_with_a_digit_correctly() { + let parsed = ExpressionVariant::parse("0license").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::Simple(SimpleExpression::new("0license".to_string(), None, false)) + ); + } + + #[test] + fn parse_a_license_id_with_any_later_version_correctly() { + let parsed = ExpressionVariant::parse("license+").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::Simple(SimpleExpression::new("license+".to_string(), None, false)) + ); + } + + #[test] + fn parse_a_document_ref_correctly() { + let parsed = ExpressionVariant::parse("DocumentRef-document:LicenseRef-license").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::Simple(SimpleExpression::new( + "license".to_string(), + Some("document".to_string()), + true + )) + ); + } + + #[test] + fn parse_a_license_ref_correctly() { + let parsed = ExpressionVariant::parse("LicenseRef-license").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::Simple(SimpleExpression::new("license".to_string(), None, true)) + ); + } + + #[test] + fn parse_a_with_expression_correctly() { + let parsed = ExpressionVariant::parse("license WITH exception").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license".to_string(), None, false), + "exception".to_string() + )) + ); + } + + #[test] + fn parse_a_complex_expression_correctly() { + let parsed = ExpressionVariant::parse( + "license1+ and ((license2 with exception1) OR license3+ AND license4 WITH exception2)", + ) + .unwrap(); + + assert_eq!( + parsed, + ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1+".to_string(), + None, + false + ))), + Box::new(ExpressionVariant::Parens(Box::new(ExpressionVariant::Or( + Box::new(ExpressionVariant::Parens(Box::new( + ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license2".to_string(), None, false), + "exception1".to_string() + )) + ))), + Box::new(ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license3+".to_string(), + None, + false + ))), + Box::new(ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license4".to_string(), None, false), + "exception2".to_string() + ))) + )), + )))) + ) + ); + } + + #[test] + fn bind_plus_stronger_than_with() { + let parsed = ExpressionVariant::parse("license+ WITH exception").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license+".to_string(), None, false), + "exception".to_string() + )) + ); + } + + #[test] + fn bind_with_stronger_than_and() { + let parsed = ExpressionVariant::parse("license1 AND license2 WITH exception").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1".to_string(), + None, + false + ))), + Box::new(ExpressionVariant::With(WithExpression::new( + SimpleExpression::new("license2".to_string(), None, false), + "exception".to_string() + ))) + ) + ); + } + + #[test] + fn bind_and_stronger_than_or() { + let parsed = ExpressionVariant::parse("license1 OR license2 AND license3").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::Or( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1".to_string(), + None, + false + ))), + Box::new(ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license2".to_string(), + None, + false + ))), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license3".to_string(), + None, + false + ))) + )) + ) + ); + } + + #[test] + fn bind_the_and_operator_left_associative() { + let parsed = ExpressionVariant::parse("license1 AND license2 AND license3").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::And( + Box::new(ExpressionVariant::And( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1".to_string(), + None, + false + ))), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license2".to_string(), + None, + false + ))) + )), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license3".to_string(), + None, + false + ))), + ) + ); + } + + #[test] + fn bind_the_or_operator_left_associative() { + let parsed = ExpressionVariant::parse("license1 OR license2 OR license3").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::Or( + Box::new(ExpressionVariant::Or( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1".to_string(), + None, + false + ))), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license2".to_string(), + None, + false + ))) + )), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license3".to_string(), + None, + false + ))), + ) + ); + } + + #[test] + fn respect_parentheses_for_binding_strength_of_operators() { + let parsed = ExpressionVariant::parse("(license1 OR license2) AND license3").unwrap(); + assert_eq!( + parsed, + ExpressionVariant::And( + Box::new(ExpressionVariant::Parens(Box::new(ExpressionVariant::Or( + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license1".to_string(), + None, + false + ))), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license2".to_string(), + None, + false + ))) + )))), + Box::new(ExpressionVariant::Simple(SimpleExpression::new( + "license3".to_string(), + None, + false + ))), + ) + ); + } + + #[test] + fn fail_if_plus_is_used_in_an_exception_expression() { + let parsed = ExpressionVariant::parse("license WITH exception+"); + assert!(parsed.is_err()); + } + + #[test] + fn fail_if_a_compound_expressions_is_used_before_with() { + let parsed = ExpressionVariant::parse("(license1 AND license2) WITH exception"); + assert!(parsed.is_err()); + } + + #[test] + fn fail_on_an_invalid_symbol() { + let parsed = ExpressionVariant::parse("/"); + assert!(parsed.is_err()); + } + + #[test] + fn fail_on_a_syntax_error() { + let parsed = ExpressionVariant::parse("(("); + assert!(parsed.is_err()); + } +} |