Skip to main content

alpm_types/
license.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use spdx::Expression;
9
10use crate::Error;
11
12/// Represents a license expression that can be either a valid SPDX identifier
13/// or a non-standard one.
14///
15/// ## Examples
16/// ```
17/// use std::str::FromStr;
18///
19/// use alpm_types::License;
20///
21/// # fn main() -> Result<(), alpm_types::Error> {
22/// // Create License from a valid SPDX identifier
23/// let license = License::from_str("MIT")?;
24/// assert!(license.is_spdx());
25/// assert_eq!(license.to_string(), "MIT");
26///
27/// // Create License from an invalid/non-SPDX identifier
28/// let license = License::from_str("My-Custom-License")?;
29/// assert!(!license.is_spdx());
30/// assert_eq!(license.to_string(), "My-Custom-License");
31/// # Ok(())
32/// # }
33/// ```
34#[derive(Clone, Debug, PartialEq)]
35pub enum License {
36    /// A valid SPDX license expression
37    ///
38    /// This variant is boxed to avoid large allocations
39    Spdx(Box<spdx::Expression>),
40    /// A non-standard license identifier
41    Unknown(String),
42}
43
44#[cfg(feature = "serde")]
45impl Serialize for License {
46    /// Custom serde serialization as Spdx doesn't provide a serde [`Serialize`] implementation.
47    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
48    where
49        S: Serializer,
50    {
51        serializer.serialize_str(&self.to_string())
52    }
53}
54
55#[cfg(feature = "serde")]
56impl<'de> Deserialize<'de> for License {
57    /// Custom serde serialization as Spdx doesn't provide a serde [`Deserialize`] implementation.
58    /// This implements deserialization from a string type.
59    ///
60    /// Attempt to parse the given input as an [spdx::Expression] and to return a [License::Spdx].
61    /// If that fails, treat it as a [License::Unknown] that contains the original string.
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: Deserializer<'de>,
65    {
66        let s = String::deserialize(deserializer)?;
67
68        if let Ok(expr) = spdx::Expression::from_str(&s) {
69            return Ok(License::Spdx(Box::new(expr)));
70        }
71
72        Ok(License::Unknown(s))
73    }
74}
75
76impl License {
77    /// Creates a new license
78    ///
79    /// This function accepts both SPDX and non-standard identifiers
80    /// and it is the same as as calling [`License::from_str`]
81    pub fn new(license: String) -> Result<Self, Error> {
82        Self::from_valid_spdx(license.clone()).or(Ok(Self::Unknown(license)))
83    }
84
85    /// Creates a new license from a valid SPDX identifier
86    ///
87    /// ## Examples
88    ///
89    /// ```
90    /// use alpm_types::{Error, License};
91    ///
92    /// # fn main() -> Result<(), alpm_types::Error> {
93    /// let license = License::from_valid_spdx("Apache-2.0".to_string())?;
94    /// assert!(license.is_spdx());
95    /// assert_eq!(license.to_string(), "Apache-2.0");
96    ///
97    /// assert!(License::from_valid_spdx("GPL-0.0".to_string()).is_err());
98    /// assert!(License::from_valid_spdx("Custom-License".to_string()).is_err());
99    /// assert!(License::from_valid_spdx("GPL-2.0".to_string()).is_err());
100    /// # Ok(())
101    /// # }
102    /// ```
103    ///
104    /// # Note
105    ///
106    /// This function uses [strict parsing] which means:
107    ///
108    /// 1. Only license identifiers in the SPDX license list, or Document/LicenseRef, are allowed.
109    ///    The license identifiers are also case-sensitive.
110    /// 2. `WITH`, `AND`, and `OR`, case-insensitive, are the only valid operators.
111    /// 3. Deprecated licenses are not allowed and will return an error
112    ///    ([`Error::DeprecatedLicense`]).
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if the given input cannot be parsed or is a deprecated license.
117    ///
118    /// [strict parsing]: https://docs.rs/spdx/latest/spdx/lexer/struct.ParseMode.html#associatedconstant.STRICT
119    pub fn from_valid_spdx(identifier: String) -> Result<Self, Error> {
120        let expression = match Expression::parse(&identifier) {
121            Ok(expr) => expr,
122            Err(e) => {
123                if e.reason == spdx::error::Reason::DeprecatedLicenseId {
124                    return Err(Error::DeprecatedLicense(identifier));
125                } else {
126                    return Err(Error::InvalidLicense(e));
127                }
128            }
129        };
130
131        Ok(Self::Spdx(Box::new(expression)))
132    }
133
134    /// Returns `true` if the license is a valid SPDX identifier
135    pub fn is_spdx(&self) -> bool {
136        matches!(self, License::Spdx(_))
137    }
138}
139
140impl FromStr for License {
141    type Err = Error;
142
143    /// Creates a new `License` instance from a string slice.
144    ///
145    /// If the input is a valid SPDX license expression,
146    /// it will be marked as such; otherwise, it will be treated as
147    /// a non-standard license identifier.
148    ///
149    /// ## Examples
150    ///
151    /// ```
152    /// use std::str::FromStr;
153    ///
154    /// use alpm_types::License;
155    ///
156    /// # fn main() -> Result<(), alpm_types::Error> {
157    /// let license = License::from_str("Apache-2.0")?;
158    /// assert!(license.is_spdx());
159    /// assert_eq!(license.to_string(), "Apache-2.0");
160    ///
161    /// let license = License::from_str("NonStandard-License")?;
162    /// assert!(!license.is_spdx());
163    /// assert_eq!(license.to_string(), "NonStandard-License");
164    /// # Ok(())
165    /// # }
166    /// ```
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if the given input is a deprecated SPDX license.
171    fn from_str(s: &str) -> Result<Self, Self::Err> {
172        Self::new(s.to_string())
173    }
174}
175
176impl Display for License {
177    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
178        match &self {
179            License::Spdx(expr) => write!(f, "{expr}"),
180            License::Unknown(s) => write!(f, "{s}"),
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use rstest::rstest;
188    use testresult::TestResult;
189
190    use super::*;
191
192    #[rstest]
193    #[case("MIT", License::Spdx(Box::new(Expression::parse("MIT")?)))]
194    #[case("Apache-2.0", License::Spdx(Box::new(Expression::parse("Apache-2.0")?)))]
195    #[case("Apache-2.0+", License::Spdx(Box::new(Expression::parse("Apache-2.0+")?)))]
196    #[case(
197        "Apache-2.0 WITH LLVM-exception",
198        License::Spdx(Box::new(Expression::parse("Apache-2.0 WITH LLVM-exception")?))
199    )]
200    #[case("GPL-3.0-or-later", License::Spdx(Box::new(Expression::parse("GPL-3.0-or-later")?)))]
201    #[case("HPND-Fenneberg-Livingston", License::Spdx(Box::new(Expression::parse("HPND-Fenneberg-Livingston")?)))]
202    #[case(
203        "NonStandard-License",
204        License::Unknown(String::from("NonStandard-License"))
205    )]
206    fn test_parse_license(#[case] input: &str, #[case] expected: License) -> TestResult {
207        let license = input.parse::<License>()?;
208        assert_eq!(license, expected);
209        assert_eq!(license.to_string(), input.to_string());
210        Ok(())
211    }
212
213    /// Ensures, that [`License::from_valid_spdx`] fails on unexpected additions.
214    #[test]
215    fn license_from_valid_spdx_fails_on_unexpected_addition() {
216        let Err(Error::InvalidLicense(spdx::ParseError {
217            reason: spdx::error::Reason::Unexpected(_),
218            ..
219        })) = License::from_valid_spdx("Apache-2.0 WITH".into())
220        else {
221            panic!("Expected to fail with Error::InvalidLicense(spdx::ParseError)");
222        };
223    }
224
225    /// Ensures, that [`License::from_valid_spdx`] fails on unknown terms.
226    #[test]
227    fn license_from_valid_spdx_fails_on_unknown_term() {
228        let Err(Error::InvalidLicense(spdx::ParseError {
229            reason: spdx::error::Reason::UnknownTerm,
230            ..
231        })) = License::from_valid_spdx("Custom-License".to_string())
232        else {
233            panic!("Expected to fail on Error::InvalidLicense(spdx::ParseError");
234        };
235    }
236
237    /// Ensures, that [`License::from_valid_spdx`] fails on deprecated SPDX license identifier
238    /// strings.
239    #[rstest]
240    #[case("BSD-2-Clause-FreeBSD")]
241    #[case("BSD-2-Clause-NetBSD")]
242    #[case("bzip2-1.0.5")]
243    #[case("GPL-2.0")]
244    fn test_deprecated_spdx(#[case] input: &str) {
245        let Err(Error::DeprecatedLicense(error_string)) =
246            License::from_valid_spdx(input.to_string())
247        else {
248            panic!("Expected to fail with Error::DeprecatedLicense");
249        };
250        assert_eq!(input, error_string);
251    }
252
253    #[rstest]
254    #[case("MIT", true)]
255    #[case("Custom-License", false)]
256    fn test_license_kind(#[case] input: &str, #[case] is_spdx: bool) -> TestResult {
257        let spdx_license = License::from_str(input)?;
258        assert_eq!(spdx_license.is_spdx(), is_spdx);
259
260        Ok(())
261    }
262}