1use std::borrow::Borrow;
2use std::collections::BTreeMap;
3use std::path::PathBuf;
4
5use anyhow::{anyhow, bail, Result};
6use chrono::{Datelike, Local};
7use handlebars::Handlebars;
8use once_cell::sync::Lazy;
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11
12static LICENSES: Lazy<Vec<License>> = Lazy::new(|| {
13 let licenses_toml = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/licenses.toml"));
14
15 let mut licenses: BTreeMap<String, Vec<License>> =
16 toml_edit::easy::from_str(licenses_toml).unwrap();
17
18 licenses.remove("license").unwrap()
19});
20
21#[derive(Debug, PartialEq, Deserialize)]
23pub struct License {
24 pub identifier: String,
28
29 pub spdx: String,
31
32 pub text: String,
34}
35
36pub fn parse_author_names<'a>(authors: &[&'a str]) -> Result<Vec<&'a str>> {
39 if authors.is_empty() {
40 bail!("at least one author is required");
41 }
42
43 let names = authors
44 .iter()
45 .map(|author| match parse_git_style_author(author) {
46 Some(name) => name,
47 None => author,
48 })
49 .collect();
50
51 Ok(names)
52}
53
54fn is_valid_spdx_id(id: &str) -> bool {
56 #[derive(Debug, Deserialize)]
57 #[serde(rename_all = "camelCase")]
58 struct LicenseList {
59 licenses: Vec<License>,
60 }
61
62 #[derive(Debug, Deserialize)]
63 #[serde(rename_all = "camelCase")]
64 struct License {
65 license_id: String,
66 }
67
68 static SPDX_LICENSE_LIST: Lazy<LicenseList> = Lazy::new(|| {
69 serde_json::from_str(include_str!(concat!(
70 env!("CARGO_MANIFEST_DIR"),
71 "/src/spdx-licenses.json"
72 )))
73 .unwrap()
74 });
75
76 SPDX_LICENSE_LIST
77 .licenses
78 .iter()
79 .any(|license| license.license_id == id)
80}
81
82pub fn parse_spdx(license_expr: &str) -> Result<Vec<&'static License>> {
87 let split: Box<dyn Iterator<Item = &str>> = if license_expr.contains('/') {
88 Box::new(license_expr.split('/'))
89 } else {
90 Box::new(license_expr.split_whitespace())
91 };
92
93 split
94 .flat_map(|token| match token {
95 "WITH" | "OR" | "AND" => None,
96 token => Some(token),
97 })
98 .map(|id| {
99 if is_valid_spdx_id(id) {
100 LICENSES
101 .iter()
102 .find(|license| license.spdx == id)
103 .ok_or_else(|| anyhow!("SPDX ID '{}' is valid, but unsupported by this program. Please open a PR!", id))
104 } else {
105 Err(anyhow!("invalid SPDX license ID: {}", id))
106 }
107 })
108 .collect()
109}
110
111pub fn render_license_text<S: Borrow<str>>(
116 licenses: &[&License],
117 authors: &[S],
118) -> Result<BTreeMap<PathBuf, String>> {
119 let mut reg = Handlebars::new();
120
121 for license in LICENSES.iter() {
122 reg.register_template_string(&license.spdx, &license.text)
123 .expect("syntax error in license template");
124 }
125
126 #[derive(Debug, Serialize)]
127 struct TemplateData {
128 year: i32,
129 copyright_holders: String,
130 }
131
132 licenses
133 .iter()
134 .map(|license| {
135 let name = if licenses.len() == 1 {
136 String::from("LICENSE")
137 } else {
138 format!("LICENSE-{}", license.identifier)
139 };
140
141 let contents = reg.render(
142 &license.spdx,
143 &TemplateData {
144 year: Local::today().year(),
145 copyright_holders: authors.join(", "),
146 },
147 )?;
148
149 Ok((PathBuf::from(name), contents))
150 })
151 .collect()
152}
153
154fn parse_git_style_author(name: &str) -> Option<&str> {
155 static GIT_NAME_RE: Lazy<Regex> =
156 Lazy::new(|| Regex::new(r"(?P<name>.+) <(?P<email>.+)>").unwrap());
157
158 GIT_NAME_RE
159 .captures(name)
160 .map(|caps| caps.name("name").unwrap().as_str())
161}
162
163#[cfg(test)]
164mod tests {
165 use crate::{is_valid_spdx_id, parse_spdx, License, LICENSES};
166
167 fn get_license(id: &str) -> &'static License {
168 LICENSES.iter().find(|l| l.spdx == id).unwrap()
169 }
170
171 #[test]
172 fn parse_licenses() {
173 assert!(LICENSES.iter().any(|l| l.spdx == "MIT"));
174 }
175
176 #[test]
177 fn valid_spdx_ids() {
178 assert!(is_valid_spdx_id("MIT"));
179 assert!(!is_valid_spdx_id("foobar"));
180 }
181
182 #[test]
183 fn simple() {
184 assert_eq!(parse_spdx("GPL-3.0").unwrap(), &[get_license("GPL-3.0")]);
185 }
186
187 #[test]
188 fn compound() {
189 assert_eq!(
190 parse_spdx("MIT OR Apache-2.0").unwrap(),
191 &[get_license("MIT"), get_license("Apache-2.0")],
192 );
193 }
194
195 #[test]
196 fn cargo_manifest_licenses() {
197 assert_eq!(
198 parse_spdx("MIT/Apache-2.0").unwrap(),
199 &[get_license("MIT"), get_license("Apache-2.0")]
200 );
201 }
202}