alpm_types/
license.rs

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