ossctl_core/contract/
spdx.rs1const SPDX_LICENSE_IDS: &[&str] = &[
17 "0bsd",
18 "afl-3.0",
19 "agpl-3.0",
20 "agpl-3.0-only",
21 "agpl-3.0-or-later",
22 "apache-2.0",
23 "artistic-2.0",
24 "blueoak-1.0.0",
25 "bsd-2-clause",
26 "bsd-2-clause-patent",
27 "bsd-3-clause",
28 "bsd-3-clause-clear",
29 "bsl-1.0",
30 "cc-by-4.0",
31 "cc-by-sa-4.0",
32 "cc0-1.0",
33 "cecill-2.1",
34 "ecl-2.0",
35 "epl-1.0",
36 "epl-2.0",
37 "eupl-1.1",
38 "eupl-1.2",
39 "gpl-2.0",
40 "gpl-2.0-only",
41 "gpl-2.0-or-later",
42 "gpl-3.0",
43 "gpl-3.0-only",
44 "gpl-3.0-or-later",
45 "isc",
46 "lgpl-2.1",
47 "lgpl-2.1-only",
48 "lgpl-2.1-or-later",
49 "lgpl-3.0",
50 "lgpl-3.0-only",
51 "lgpl-3.0-or-later",
52 "mit",
53 "mit-0",
54 "mpl-2.0",
55 "ms-pl",
56 "ms-rl",
57 "ncsa",
58 "ofl-1.1",
59 "osl-3.0",
60 "postgresql",
61 "python-2.0",
62 "ruby",
63 "unlicense",
64 "upl-1.0",
65 "vim",
66 "wtfpl",
67 "zlib",
68 "zpl-2.1",
69];
70
71fn valid_license_id(tok: &str) -> bool {
75 let base = tok.strip_suffix('+').unwrap_or(tok);
76 if base.starts_with("LicenseRef-") || base.starts_with("DocumentRef-") {
77 return !base.is_empty()
80 && base
81 .chars()
82 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | ':'));
83 }
84 let lower = base.to_ascii_lowercase();
85 SPDX_LICENSE_IDS.contains(&lower.as_str())
86}
87
88#[must_use]
91pub fn spdx_valid(expr: &str) -> bool {
92 if expr.trim().is_empty() {
93 return false;
94 }
95 let spaced = expr.replace('(', " ( ").replace(')', " ) ");
97 let toks: Vec<&str> = spaced.split_whitespace().collect();
98 let mut parser = Parser {
99 toks: &toks,
100 pos: 0,
101 };
102 parser.parse_expr() && parser.pos == parser.toks.len()
103}
104
105struct Parser<'a> {
106 toks: &'a [&'a str],
107 pos: usize,
108}
109
110impl<'a> Parser<'a> {
111 fn peek(&self) -> Option<&'a str> {
112 self.toks.get(self.pos).copied()
113 }
114
115 fn is_op(tok: Option<&str>) -> bool {
116 matches!(tok, Some(t) if t.eq_ignore_ascii_case("AND") || t.eq_ignore_ascii_case("OR"))
117 }
118
119 fn parse_expr(&mut self) -> bool {
120 if !self.parse_term() {
121 return false;
122 }
123 while Self::is_op(self.peek()) {
124 self.pos += 1;
125 if !self.parse_term() {
126 return false;
127 }
128 }
129 true
130 }
131
132 fn parse_term(&mut self) -> bool {
133 let Some(t) = self.peek() else {
134 return false;
135 };
136 if t == "(" {
137 self.pos += 1;
138 if !self.parse_expr() {
139 return false;
140 }
141 if self.peek() != Some(")") {
142 return false;
143 }
144 self.pos += 1;
145 return true;
146 }
147 if t == "(" || t == ")" || Self::is_op(Some(t)) || t.eq_ignore_ascii_case("WITH") {
149 return false;
150 }
151 self.pos += 1; if !valid_license_id(t) {
153 return false;
154 }
155 if matches!(self.peek(), Some(w) if w.eq_ignore_ascii_case("WITH")) {
156 self.pos += 1;
157 match self.peek() {
159 Some(exc)
160 if exc != "("
161 && exc != ")"
162 && !Self::is_op(Some(exc))
163 && !exc.eq_ignore_ascii_case("WITH") =>
164 {
165 self.pos += 1;
166 }
167 _ => return false,
168 }
169 }
170 true
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::spdx_valid;
177
178 #[test]
179 fn simple_ids() {
180 assert!(spdx_valid("MIT"));
181 assert!(spdx_valid("Apache-2.0"));
182 assert!(spdx_valid("mit")); assert!(spdx_valid("GPL-3.0-only"));
184 }
185
186 #[test]
187 fn or_later_suffix() {
188 assert!(spdx_valid("GPL-3.0+"));
189 assert!(spdx_valid("Apache-2.0+"));
190 }
191
192 #[test]
193 fn compound_expressions() {
194 assert!(spdx_valid("MIT OR Apache-2.0"));
195 assert!(spdx_valid("MIT AND Apache-2.0"));
196 assert!(spdx_valid("(MIT OR Apache-2.0) AND ISC"));
197 assert!(spdx_valid("mit or apache-2.0")); }
199
200 #[test]
201 fn with_exception() {
202 assert!(spdx_valid("GPL-3.0-only WITH Classpath-exception-2.0"));
203 assert!(!spdx_valid("GPL-3.0-only WITH")); }
205
206 #[test]
207 fn custom_refs() {
208 assert!(spdx_valid("LicenseRef-Acme-Proprietary"));
209 assert!(spdx_valid("DocumentRef-x:LicenseRef-y"));
210 }
211
212 #[test]
213 fn rejects_unknown_and_malformed() {
214 assert!(!spdx_valid(""));
215 assert!(!spdx_valid(" "));
216 assert!(!spdx_valid("Proprietary-Acme")); assert!(!spdx_valid("MIT AND")); assert!(!spdx_valid("(MIT")); assert!(!spdx_valid("MIT OR OR Apache-2.0")); assert!(!spdx_valid("MIT Apache-2.0")); assert!(!spdx_valid("()")); }
223}