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 ///
100 /// assert_eq!(
101 /// License::from_valid_spdx("GPL-2.0".to_string()),
102 /// Err(Error::DeprecatedLicense("GPL-2.0".to_string()))
103 /// );
104 /// # Ok(())
105 /// # }
106 /// ```
107 ///
108 /// # Note
109 ///
110 /// This function uses [strict parsing] which means:
111 ///
112 /// 1. Only license identifiers in the SPDX license list, or Document/LicenseRef, are allowed.
113 /// The license identifiers are also case-sensitive.
114 /// 2. `WITH`, `AND`, and `OR`, case-insensitive, are the only valid operators.
115 /// 3. Deprecated licenses are not allowed and will return an error
116 /// ([`Error::DeprecatedLicense`]).
117 ///
118 /// # Errors
119 ///
120 /// Returns an error if the given input cannot be parsed or is a deprecated license.
121 ///
122 /// [strict parsing]: https://docs.rs/spdx/latest/spdx/lexer/struct.ParseMode.html#associatedconstant.STRICT
123 pub fn from_valid_spdx(identifier: String) -> Result<Self, Error> {
124 let expression = match Expression::parse(&identifier) {
125 Ok(expr) => expr,
126 Err(e) => {
127 if e.reason == spdx::error::Reason::DeprecatedLicenseId {
128 return Err(Error::DeprecatedLicense(identifier));
129 } else {
130 return Err(Error::InvalidLicense(e));
131 }
132 }
133 };
134
135 Ok(Self::Spdx(Box::new(expression)))
136 }
137
138 /// Returns `true` if the license is a valid SPDX identifier
139 pub fn is_spdx(&self) -> bool {
140 matches!(self, License::Spdx(_))
141 }
142}
143
144impl FromStr for License {
145 type Err = Error;
146
147 /// Creates a new `License` instance from a string slice.
148 ///
149 /// If the input is a valid SPDX license expression,
150 /// it will be marked as such; otherwise, it will be treated as
151 /// a non-standard license identifier.
152 ///
153 /// ## Examples
154 ///
155 /// ```
156 /// use std::str::FromStr;
157 ///
158 /// use alpm_types::License;
159 ///
160 /// # fn main() -> Result<(), alpm_types::Error> {
161 /// let license = License::from_str("Apache-2.0")?;
162 /// assert!(license.is_spdx());
163 /// assert_eq!(license.to_string(), "Apache-2.0");
164 ///
165 /// let license = License::from_str("NonStandard-License")?;
166 /// assert!(!license.is_spdx());
167 /// assert_eq!(license.to_string(), "NonStandard-License");
168 /// # Ok(())
169 /// # }
170 /// ```
171 ///
172 /// # Errors
173 ///
174 /// Returns an error if the given input is a deprecated SPDX license.
175 fn from_str(s: &str) -> Result<Self, Self::Err> {
176 Self::new(s.to_string())
177 }
178}
179
180impl Display for License {
181 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
182 match &self {
183 License::Spdx(expr) => write!(f, "{expr}"),
184 License::Unknown(s) => write!(f, "{s}"),
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use rstest::rstest;
192
193 use super::*;
194
195 #[rstest]
196 #[case("MIT", License::Spdx(Box::new(Expression::parse("MIT").unwrap())))]
197 #[case("Apache-2.0", License::Spdx(Box::new(Expression::parse("Apache-2.0").unwrap())))]
198 #[case("Apache-2.0+", License::Spdx(Box::new(Expression::parse("Apache-2.0+").unwrap())))]
199 #[case(
200 "Apache-2.0 WITH LLVM-exception",
201 License::Spdx(Box::new(Expression::parse("Apache-2.0 WITH LLVM-exception").unwrap()))
202 )]
203 #[case("GPL-3.0-or-later", License::Spdx(Box::new(Expression::parse("GPL-3.0-or-later").unwrap())))]
204 #[case("HPND-Fenneberg-Livingston", License::Spdx(Box::new(Expression::parse("HPND-Fenneberg-Livingston").unwrap())))]
205 #[case(
206 "NonStandard-License",
207 License::Unknown(String::from("NonStandard-License"))
208 )]
209 fn test_parse_license(
210 #[case] input: &str,
211 #[case] expected: License,
212 ) -> testresult::TestResult<()> {
213 let license = input.parse::<License>()?;
214 assert_eq!(license, expected);
215 assert_eq!(license.to_string(), input.to_string());
216 Ok(())
217 }
218
219 #[rstest]
220 #[case("Apache-2.0 WITH",
221 Err(spdx::ParseError {
222 original: String::from("Apache-2.0 WITH"),
223 span: 15..15,
224 reason: spdx::error::Reason::Unexpected(&["<addition>"])
225 }.into())
226 )]
227 #[case("Custom-License",
228 Err(spdx::ParseError {
229 original: String::from("Custom-License"),
230 span: 0..14,
231 reason: spdx::error::Reason::UnknownTerm
232 }.into())
233 )]
234 fn test_invalid_spdx(#[case] input: &str, #[case] expected: Result<License, Error>) {
235 let result = License::from_valid_spdx(input.to_string());
236 assert_eq!(result, expected);
237 }
238
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 result = License::from_valid_spdx(input.to_string());
246 assert_eq!(result, Err(Error::DeprecatedLicense(input.to_string())));
247 }
248
249 #[rstest]
250 #[case("MIT", true)]
251 #[case("Custom-License", false)]
252 fn test_license_kind(#[case] input: &str, #[case] is_spdx: bool) -> testresult::TestResult<()> {
253 let spdx_license = License::from_str(input)?;
254 assert_eq!(spdx_license.is_spdx(), is_spdx);
255
256 Ok(())
257 }
258}