new: drive the license menu, parsing and sniffing from data/licenses.yml
This commit is contained in:
@@ -0,0 +1,139 @@
|
|||||||
|
## License knowledge of `pkh new`, in one place: the wizard menu labels
|
||||||
|
## (src/new/questions.rs), the spellings accepted by License::parse and the
|
||||||
|
## SPDX URL template (src/new/options.rs), and the license-file sniffing
|
||||||
|
## inputs (src/new/detect.rs) all read this table, so the three lists —
|
||||||
|
## previously kept in sync by comments only — cannot drift apart anymore.
|
||||||
|
## Adding or changing a curated license is one entry below.
|
||||||
|
##
|
||||||
|
## Keys:
|
||||||
|
## id: SPDX identifier: written to debian/copyright, returned
|
||||||
|
## by the license sniff and substituted into
|
||||||
|
## license_url_template (minus a trailing '+' of the
|
||||||
|
## "or later" spellings)
|
||||||
|
## menu: label offered by the wizard license question (the
|
||||||
|
## free-text "Other (enter a SPDX identifier)" entry
|
||||||
|
## stays in Rust — it is UX, not data)
|
||||||
|
## spellings: inputs accepted by License::parse, matched
|
||||||
|
## case-insensitively; each entry must accept its own id
|
||||||
|
## (a consistency test in options.rs locks ids, spellings
|
||||||
|
## and the License enum together)
|
||||||
|
## detect_markers: marker sets driving the LICENSE/COPYING text sniff of
|
||||||
|
## detect.rs. A set matches when every marker of `all`
|
||||||
|
## occurs in the lowercased license text and none of
|
||||||
|
## `unless` does; an entry matches when any of its sets
|
||||||
|
## does. The sets are written to be mutually exclusive:
|
||||||
|
## the `unless` markers keep multi-license texts on the
|
||||||
|
## entry carrying the stronger reference (a MIT-named
|
||||||
|
## file also quoting the GPL or the Apache license is a
|
||||||
|
## GPL/Apache file) and keep GPL sets off LGPL texts,
|
||||||
|
## whose name contains theirs. The entry order below
|
||||||
|
## (menu order) therefore only breaks ties.
|
||||||
|
##
|
||||||
|
## detect_files (top level): the candidate license file names the sniff
|
||||||
|
## reads, in preference order, shared by every license (the case-variant
|
||||||
|
## directory scan around them stays in Rust).
|
||||||
|
##
|
||||||
|
## license_url_template: the SPDX license page URL, with {id} substituted
|
||||||
|
## for the debian/copyright reference paragraph.
|
||||||
|
##
|
||||||
|
## The behavioral lock for the markers is the LICENSE_TEXTS test table in
|
||||||
|
## src/new/detect.rs: a bad marker edit fails those tests, not packages.
|
||||||
|
|
||||||
|
license_url_template: https://spdx.org/licenses/{id}.html
|
||||||
|
|
||||||
|
detect_files:
|
||||||
|
- LICENSE
|
||||||
|
- LICENSE.md
|
||||||
|
- LICENSE.txt
|
||||||
|
- COPYING
|
||||||
|
- COPYING.txt
|
||||||
|
|
||||||
|
# Entries in wizard-menu order.
|
||||||
|
licenses:
|
||||||
|
- id: MIT
|
||||||
|
menu: MIT
|
||||||
|
spellings: [MIT]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- mit license
|
||||||
|
unless:
|
||||||
|
- apache license
|
||||||
|
- general public license
|
||||||
|
- all:
|
||||||
|
- permission is hereby granted, free of charge
|
||||||
|
unless:
|
||||||
|
- apache license
|
||||||
|
- general public license
|
||||||
|
- id: Apache-2.0
|
||||||
|
menu: Apache-2.0
|
||||||
|
spellings: [Apache-2.0]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- apache license
|
||||||
|
- version 2
|
||||||
|
- id: GPL-2.0+
|
||||||
|
menu: GPL-2.0+
|
||||||
|
spellings: [GPL-2.0+]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- general public license
|
||||||
|
unless:
|
||||||
|
- version 3
|
||||||
|
- lesser general public license
|
||||||
|
- id: GPL-3.0+
|
||||||
|
menu: GPL-3.0+
|
||||||
|
spellings: [GPL-3.0+]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- general public license
|
||||||
|
- version 3
|
||||||
|
unless:
|
||||||
|
- lesser general public license
|
||||||
|
- id: LGPL-2.1+
|
||||||
|
menu: LGPL-2.1+
|
||||||
|
spellings: [LGPL-2.1+]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- lesser general public license
|
||||||
|
unless:
|
||||||
|
- version 3
|
||||||
|
- all:
|
||||||
|
- lesser general public license
|
||||||
|
- version 2.1
|
||||||
|
- id: LGPL-3.0+
|
||||||
|
menu: LGPL-3.0+
|
||||||
|
spellings: [LGPL-3.0+]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- lesser general public license
|
||||||
|
- version 3
|
||||||
|
unless:
|
||||||
|
- version 2.1
|
||||||
|
- id: BSD-2-Clause
|
||||||
|
menu: BSD-2-Clause
|
||||||
|
spellings: [BSD-2-Clause]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- redistribution and use in source and binary forms
|
||||||
|
unless:
|
||||||
|
- endorse or promote
|
||||||
|
- isc license
|
||||||
|
- permission to use, copy, modify, and/or distribute this software
|
||||||
|
- id: BSD-3-Clause
|
||||||
|
menu: BSD-3-Clause
|
||||||
|
spellings: [BSD-3-Clause]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- redistribution and use in source and binary forms
|
||||||
|
- endorse or promote
|
||||||
|
unless:
|
||||||
|
- isc license
|
||||||
|
- permission to use, copy, modify, and/or distribute this software
|
||||||
|
- id: ISC
|
||||||
|
menu: ISC
|
||||||
|
spellings: [ISC]
|
||||||
|
detect_markers:
|
||||||
|
- all:
|
||||||
|
- isc license
|
||||||
|
- all:
|
||||||
|
- permission to use, copy, modify, and/or distribute this software
|
||||||
+26
-52
@@ -23,6 +23,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
|
use super::licenses;
|
||||||
use super::options::TemplateId;
|
use super::options::TemplateId;
|
||||||
|
|
||||||
/// Outcome of the detection.
|
/// Outcome of the detection.
|
||||||
@@ -109,23 +110,14 @@ fn has_shebang(path: &Path) -> bool {
|
|||||||
content.starts_with(b"#!")
|
content.starts_with(b"#!")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// License files looked at by [`sniff_license`], in preference order.
|
|
||||||
const LICENSE_FILES: [&str; 5] = [
|
|
||||||
"LICENSE",
|
|
||||||
"LICENSE.md",
|
|
||||||
"LICENSE.txt",
|
|
||||||
"COPYING",
|
|
||||||
"COPYING.txt",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Sniff the license of the project in `dir` from its `LICENSE`/`COPYING`
|
/// Sniff the license of the project in `dir` from its `LICENSE`/`COPYING`
|
||||||
/// file: an `SPDX-License-Identifier:` line wins, otherwise the text is
|
/// file: an `SPDX-License-Identifier:` line wins, otherwise the text is
|
||||||
/// matched against a short list of recognizable licenses (MIT, BSD-2/3,
|
/// matched against the marker sets of the bundled license table
|
||||||
/// Apache-2.0, GPL-2/3, LGPL-2.1/3, ISC). `None` when no license file
|
/// (`data/licenses.yml`: MIT, BSD-2/3, Apache-2.0, GPL-2/3, LGPL-2.1/3,
|
||||||
/// exists or nothing recognizable is found.
|
/// ISC). `None` when no license file exists or nothing recognizable is
|
||||||
|
/// found.
|
||||||
pub fn sniff_license(dir: &Path) -> Option<String> {
|
pub fn sniff_license(dir: &Path) -> Option<String> {
|
||||||
let content = LICENSE_FILES
|
let content = licenses::detect_files()
|
||||||
.iter()
|
|
||||||
.find_map(|name| std::fs::read_to_string(dir.join(name)).ok())
|
.find_map(|name| std::fs::read_to_string(dir.join(name)).ok())
|
||||||
// Case variants and suffixes (LICENSE-MIT, LICENCE, cpYING…): the
|
// Case variants and suffixes (LICENSE-MIT, LICENCE, cpYING…): the
|
||||||
// first top-level file whose name looks like a license notice.
|
// first top-level file whose name looks like a license notice.
|
||||||
@@ -166,43 +158,10 @@ pub fn sniff_license(dir: &Path) -> Option<String> {
|
|||||||
return Some(id);
|
return Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The recognizable-license markers live in the bundled table; the
|
||||||
|
// LICENSE_TEXTS test below is their behavioral lock.
|
||||||
let text = content.to_ascii_lowercase();
|
let text = content.to_ascii_lowercase();
|
||||||
if text.contains("apache license") && text.contains("version 2") {
|
licenses::detect_from_text(&text).map(str::to_string)
|
||||||
return Some("Apache-2.0".to_string());
|
|
||||||
}
|
|
||||||
if text.contains("lesser general public license") {
|
|
||||||
return if text.contains("version 3") && !text.contains("version 2.1") {
|
|
||||||
Some("LGPL-3.0+".to_string())
|
|
||||||
} else {
|
|
||||||
Some("LGPL-2.1+".to_string())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if text.contains("general public license") {
|
|
||||||
return if text.contains("version 3") {
|
|
||||||
Some("GPL-3.0+".to_string())
|
|
||||||
} else {
|
|
||||||
Some("GPL-2.0+".to_string())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if text.contains("mit license") || text.contains("permission is hereby granted, free of charge")
|
|
||||||
{
|
|
||||||
return Some("MIT".to_string());
|
|
||||||
}
|
|
||||||
if text.contains("isc license")
|
|
||||||
|| text.contains("permission to use, copy, modify, and/or distribute this software")
|
|
||||||
{
|
|
||||||
return Some("ISC".to_string());
|
|
||||||
}
|
|
||||||
if text.contains("redistribution and use in source and binary forms") {
|
|
||||||
// The third clause (name endorsement) is what sets BSD-3 apart
|
|
||||||
// from BSD-2.
|
|
||||||
return if text.contains("endorse or promote") {
|
|
||||||
Some("BSD-3-Clause".to_string())
|
|
||||||
} else {
|
|
||||||
Some("BSD-2-Clause".to_string())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -302,8 +261,10 @@ mod tests {
|
|||||||
assert_eq!(detect(dir.path()), Detection::Empty);
|
assert_eq!(detect(dir.path()), Detection::Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Distinctive (shortened) excerpts of the recognizable license texts.
|
/// Distinctive (shortened) excerpts of the recognizable license texts:
|
||||||
const LICENSE_TEXTS: [(&str, &str); 9] = [
|
/// the behavioral lock of the marker sets in `data/licenses.yml` — a
|
||||||
|
/// bad marker edit fails here, not on real packages.
|
||||||
|
const LICENSE_TEXTS: [(&str, &str); 11] = [
|
||||||
(
|
(
|
||||||
"MIT",
|
"MIT",
|
||||||
"MIT License\n\nPermission is hereby granted, free of charge, to any person",
|
"MIT License\n\nPermission is hereby granted, free of charge, to any person",
|
||||||
@@ -337,6 +298,19 @@ mod tests {
|
|||||||
"ISC",
|
"ISC",
|
||||||
"ISC License\nPermission to use, copy, modify, and/or distribute this software",
|
"ISC License\nPermission to use, copy, modify, and/or distribute this software",
|
||||||
),
|
),
|
||||||
|
// Dual-licensed preamble: the GPL reference outranks the MIT
|
||||||
|
// boilerplate, like the old hardcoded cascade decided.
|
||||||
|
(
|
||||||
|
"GPL-2.0+",
|
||||||
|
"MIT License\n\nAlternatively, under the terms of the GNU General Public License,\
|
||||||
|
\nversion 2 of the License.",
|
||||||
|
),
|
||||||
|
// LGPL text naming both versions: the 2.1 wording wins.
|
||||||
|
(
|
||||||
|
"LGPL-2.1+",
|
||||||
|
"GNU LESSER GENERAL PUBLIC LICENSE\nVersion 2.1, February 1999\n\
|
||||||
|
This is version 2.1; version 3 is available separately.",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
//! License reference data for `pkh new`, bundled as `data/licenses.yml`.
|
||||||
|
//!
|
||||||
|
//! The license knowledge of the scaffolder used to live in three places —
|
||||||
|
//! the wizard menu (`questions.rs`), the parse/SPDX mapping (`options.rs`)
|
||||||
|
//! and the license-file sniffing inputs (`detect.rs`) — kept in sync by
|
||||||
|
//! comments only. It is one table now: [`entries`] carries each curated
|
||||||
|
//! license's menu label, accepted spellings and detection markers,
|
||||||
|
//! [`detect_files`] the candidate license file names and [`url_template`]
|
||||||
|
//! the SPDX page URL template, so the lists cannot drift apart and adding
|
||||||
|
//! a license is a YAML entry.
|
||||||
|
//!
|
||||||
|
//! The `License` enum stays in `options.rs` (`NewOptions` and the template
|
||||||
|
//! rendering match on its variants); the enum's variants and the table's
|
||||||
|
//! ids are locked together by a consistency test there. The free-text
|
||||||
|
//! "Other (enter a SPDX identifier)" wizard entry is UX, not data, and
|
||||||
|
//! stays in Rust — like `License::Custom`'s code-driven parse path.
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::data::embed_data;
|
||||||
|
|
||||||
|
/// One marker set of the license-text sniff: the set matches when every
|
||||||
|
/// marker of `all` occurs in the lowercased license text and none of
|
||||||
|
/// `unless` does (all-markers-within-a-list, any-marker-list semantics).
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct DetectMarkerSet {
|
||||||
|
/// Substrings that must all occur in the license text
|
||||||
|
pub(crate) all: Vec<String>,
|
||||||
|
/// Substrings that must all be absent for the set to match
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) unless: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One curated license of the bundled table: everything `pkh new` knows
|
||||||
|
/// about it — identifier, menu label, accepted spellings and sniff markers
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct LicenseEntry {
|
||||||
|
/// SPDX identifier: written to `debian/copyright`, returned by the
|
||||||
|
/// license sniff, substituted into [`url_template`]
|
||||||
|
pub(crate) id: String,
|
||||||
|
/// Label offered by the wizard license menu
|
||||||
|
pub(crate) menu: String,
|
||||||
|
/// Inputs accepted by `License::parse` (case-insensitive); must
|
||||||
|
/// include the id itself
|
||||||
|
pub(crate) spellings: Vec<String>,
|
||||||
|
/// Marker sets of the license-text sniff (see [`detect_from_text`])
|
||||||
|
pub(crate) detect_markers: Vec<DetectMarkerSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bundled license table (`data/licenses.yml`): the curated licenses
|
||||||
|
/// plus the shared sniffing inputs and the SPDX URL template
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct LicensesData {
|
||||||
|
/// SPDX license page URL template (`{id}` placeholder)
|
||||||
|
license_url_template: String,
|
||||||
|
/// Candidate license file names of the sniff, in preference order
|
||||||
|
detect_files: Vec<String>,
|
||||||
|
/// The curated licenses, in wizard-menu order
|
||||||
|
licenses: Vec<LicenseEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_data! {
|
||||||
|
static ref LICENSES_DATA: LicensesData = "../../data/licenses.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The curated license entries, in the order offered by the wizard menu
|
||||||
|
pub(crate) fn entries() -> &'static [LicenseEntry] {
|
||||||
|
&LICENSES_DATA.licenses
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The entry whose `spellings` contain `input` (case-insensitive): the
|
||||||
|
/// table lookup behind `License::parse`'s curated arm
|
||||||
|
pub(crate) fn entry_for_spelling(input: &str) -> Option<&'static LicenseEntry> {
|
||||||
|
entries().iter().find(|entry| {
|
||||||
|
entry
|
||||||
|
.spellings
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.eq_ignore_ascii_case(input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The candidate license file names looked at by the license sniff, in
|
||||||
|
/// preference order, shared by every license
|
||||||
|
pub(crate) fn detect_files() -> impl Iterator<Item = &'static str> {
|
||||||
|
LICENSES_DATA.detect_files.iter().map(String::as_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The SPDX identifier of the (lowercased) license `text`, `None` when
|
||||||
|
/// nothing is recognizable: the first entry (menu order) with a matching
|
||||||
|
/// marker set wins. The caller lowercases the text; the markers are
|
||||||
|
/// lowercase substrings.
|
||||||
|
pub(crate) fn detect_from_text(text: &str) -> Option<&'static str> {
|
||||||
|
entries()
|
||||||
|
.iter()
|
||||||
|
.find(|entry| markers_match(entry, text))
|
||||||
|
.map(|entry| entry.id.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether any marker set of `entry` matches `text` (see
|
||||||
|
/// [`DetectMarkerSet`] for the semantics)
|
||||||
|
fn markers_match(entry: &LicenseEntry, text: &str) -> bool {
|
||||||
|
entry.detect_markers.iter().any(|set| {
|
||||||
|
set.all.iter().all(|marker| text.contains(marker))
|
||||||
|
&& set.unless.iter().all(|marker| !text.contains(marker))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The SPDX license page URL template of the table, carrying the license
|
||||||
|
/// identifier as an `{id}` placeholder
|
||||||
|
pub(crate) fn url_template() -> &'static str {
|
||||||
|
LICENSES_DATA.license_url_template.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Every entry is menu-ready: non-empty id/menu, at least one spelling
|
||||||
|
/// and one non-empty marker set, and no duplicate ids or menus (the
|
||||||
|
/// wizard select would silently shadow a duplicate menu label).
|
||||||
|
#[test]
|
||||||
|
fn entries_are_well_formed() {
|
||||||
|
assert!(!entries().is_empty());
|
||||||
|
let mut ids: Vec<&str> = Vec::new();
|
||||||
|
let mut menus: Vec<&str> = Vec::new();
|
||||||
|
for entry in entries() {
|
||||||
|
assert!(!entry.id.is_empty());
|
||||||
|
assert!(!entry.menu.is_empty());
|
||||||
|
assert!(!entry.spellings.is_empty());
|
||||||
|
assert!(
|
||||||
|
!entry.detect_markers.is_empty(),
|
||||||
|
"entry '{}' has no marker set",
|
||||||
|
entry.id
|
||||||
|
);
|
||||||
|
for set in &entry.detect_markers {
|
||||||
|
assert!(
|
||||||
|
!set.all.is_empty(),
|
||||||
|
"entry '{}' has an empty marker set",
|
||||||
|
entry.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ids.push(entry.id.as_str());
|
||||||
|
menus.push(entry.menu.as_str());
|
||||||
|
}
|
||||||
|
ids.sort_unstable();
|
||||||
|
ids.dedup();
|
||||||
|
menus.sort_unstable();
|
||||||
|
menus.dedup();
|
||||||
|
assert_eq!(ids.len(), entries().len(), "duplicate ids");
|
||||||
|
assert_eq!(menus.len(), entries().len(), "duplicate menus");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The shared sniffing inputs and the URL template: the canonical file
|
||||||
|
/// names in preference order, and a template the spdx_url
|
||||||
|
/// substitution can render.
|
||||||
|
#[test]
|
||||||
|
fn sniff_inputs_and_url_template() {
|
||||||
|
let files: Vec<&str> = detect_files().collect();
|
||||||
|
assert!(!files.is_empty());
|
||||||
|
assert_eq!(files[0], "LICENSE");
|
||||||
|
assert!(files.contains(&"COPYING"));
|
||||||
|
assert!(url_template().starts_with("https://spdx.org/licenses/"));
|
||||||
|
assert!(url_template().contains("{id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The marker semantics on hand-built texts: every `all` marker must
|
||||||
|
/// occur, every `unless` marker must not, any set of an entry
|
||||||
|
/// suffices, and the sets are mutually exclusive across entries —
|
||||||
|
/// the LGPL/GPL substring relation and the multi-license texts land
|
||||||
|
/// on the same entry the old hardcoded cascade picked.
|
||||||
|
#[test]
|
||||||
|
fn marker_sets_keep_the_old_cascade_results() {
|
||||||
|
// Nothing recognizable.
|
||||||
|
assert_eq!(detect_from_text("do whatever you want"), None);
|
||||||
|
// Version discrimination of the GPL family.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu general public license\nversion 3"),
|
||||||
|
Some("GPL-3.0+")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu general public license\nversion 2, june 1991"),
|
||||||
|
Some("GPL-2.0+")
|
||||||
|
);
|
||||||
|
// "lesser general public license" contains "general public
|
||||||
|
// license": LGPL texts must stay on their entries.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu lesser general public license\nversion 3"),
|
||||||
|
Some("LGPL-3.0+")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu lesser general public license\nversion 2.1"),
|
||||||
|
Some("LGPL-2.1+")
|
||||||
|
);
|
||||||
|
// A text naming both versions is the 2.1 wording (version 2.1
|
||||||
|
// wins), like the old unless-less branch pair did.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("gnu lesser general public license\nversion 3, like version 2.1"),
|
||||||
|
Some("LGPL-2.1+")
|
||||||
|
);
|
||||||
|
// Multi-license texts: the GPL/Apache reference is the stronger
|
||||||
|
// one, so MIT does not steal them.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("mit license\nunder the gnu general public license, version 2"),
|
||||||
|
Some("GPL-2.0+")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text(
|
||||||
|
"permission is hereby granted, free of charge\ndual-licensed under the apache license version 2"
|
||||||
|
),
|
||||||
|
Some("Apache-2.0")
|
||||||
|
);
|
||||||
|
// BSD-2 vs BSD-3: the endorsement clause is the discriminator.
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text("redistribution and use in source and binary forms"),
|
||||||
|
Some("BSD-2-Clause")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_from_text(
|
||||||
|
"redistribution and use in source and binary forms\nmay be used to endorse or promote"
|
||||||
|
),
|
||||||
|
Some("BSD-3-Clause")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
pub mod debian;
|
pub mod debian;
|
||||||
pub mod detect;
|
pub mod detect;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
|
/// License reference data for the scaffolder (bundled `data/licenses.yml`)
|
||||||
|
pub(crate) mod licenses;
|
||||||
pub mod options;
|
pub mod options;
|
||||||
pub mod orig;
|
pub mod orig;
|
||||||
pub mod origin;
|
pub mod origin;
|
||||||
|
|||||||
+86
-19
@@ -15,6 +15,7 @@ use crate::debian::DebianVersion;
|
|||||||
use crate::debian::deps::{Deps, ParseOpts};
|
use crate::debian::deps::{Deps, ParseOpts};
|
||||||
use crate::distro_info;
|
use crate::distro_info;
|
||||||
use crate::new::detect::{self, Detection};
|
use crate::new::detect::{self, Detection};
|
||||||
|
use crate::new::licenses;
|
||||||
use crate::new::origin::{Forge, GitOrigin};
|
use crate::new::origin::{Forge, GitOrigin};
|
||||||
use crate::new::templates;
|
use crate::new::templates;
|
||||||
|
|
||||||
@@ -213,6 +214,10 @@ impl OrigOrigin {
|
|||||||
|
|
||||||
/// Upstream license of the package: a curated SPDX list plus a free-text
|
/// Upstream license of the package: a curated SPDX list plus a free-text
|
||||||
/// fallback for anything else (including "unknown" until the user picks one).
|
/// fallback for anything else (including "unknown" until the user picks one).
|
||||||
|
/// The curated identifiers, their accepted spellings and the menu labels
|
||||||
|
/// come from the bundled license table (`data/licenses.yml`); this enum
|
||||||
|
/// stays code because [`NewOptions`] and the template rendering match on
|
||||||
|
/// its variants — a consistency test keeps the two in lockstep.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum License {
|
pub enum License {
|
||||||
/// MIT
|
/// MIT
|
||||||
@@ -239,21 +244,34 @@ pub enum License {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl License {
|
impl License {
|
||||||
/// Map a license string to a [`License`]: curated SPDX identifiers are
|
/// Map a license string to a [`License`]: the curated SPDX identifiers
|
||||||
/// matched case-insensitively, anything else becomes
|
/// of the bundled license table (`data/licenses.yml`, matched through
|
||||||
/// [`License::Custom`] verbatim.
|
/// their spellings, case-insensitively) become their variant,
|
||||||
|
/// anything else becomes [`License::Custom`] verbatim (lowercased).
|
||||||
pub fn parse(s: &str) -> License {
|
pub fn parse(s: &str) -> License {
|
||||||
match s.to_ascii_lowercase().as_str() {
|
match licenses::entry_for_spelling(s) {
|
||||||
"mit" => License::Mit,
|
Some(entry) => Self::from_spdx(&entry.id)
|
||||||
"apache-2.0" => License::Apache2,
|
.expect("licenses.yml ids and License variants are kept in sync by a test"),
|
||||||
"gpl-2.0+" => License::Gpl2Plus,
|
None => License::Custom(s.to_ascii_lowercase()),
|
||||||
"gpl-3.0+" => License::Gpl3Plus,
|
}
|
||||||
"lgpl-2.1+" => License::Lgpl21Plus,
|
}
|
||||||
"lgpl-3.0+" => License::Lgpl3Plus,
|
|
||||||
"bsd-2-clause" => License::Bsd2Clause,
|
/// The variant of a curated license's SPDX identifier — the inverse of
|
||||||
"bsd-3-clause" => License::Bsd3Clause,
|
/// [`License::spdx`] for the non-`Custom` variants, `None` for any
|
||||||
"isc" => License::Isc,
|
/// other string. The ids accepted here are locked to the bundled
|
||||||
other => License::Custom(other.to_string()),
|
/// license table by a consistency test.
|
||||||
|
fn from_spdx(id: &str) -> Option<License> {
|
||||||
|
match id {
|
||||||
|
"MIT" => Some(License::Mit),
|
||||||
|
"Apache-2.0" => Some(License::Apache2),
|
||||||
|
"GPL-2.0+" => Some(License::Gpl2Plus),
|
||||||
|
"GPL-3.0+" => Some(License::Gpl3Plus),
|
||||||
|
"LGPL-2.1+" => Some(License::Lgpl21Plus),
|
||||||
|
"LGPL-3.0+" => Some(License::Lgpl3Plus),
|
||||||
|
"BSD-2-Clause" => Some(License::Bsd2Clause),
|
||||||
|
"BSD-3-Clause" => Some(License::Bsd3Clause),
|
||||||
|
"ISC" => Some(License::Isc),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,12 +292,11 @@ impl License {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// SPDX license data page URL (without the trailing `+` of the
|
/// SPDX license data page URL (without the trailing `+` of the
|
||||||
/// "or later" spellings), for the copyright reference paragraph.
|
/// "or later" spellings), for the copyright reference paragraph: the
|
||||||
|
/// identifier substituted into the URL template of the bundled
|
||||||
|
/// license table.
|
||||||
pub fn spdx_url(&self) -> String {
|
pub fn spdx_url(&self) -> String {
|
||||||
format!(
|
licenses::url_template().replace("{id}", self.spdx().trim_end_matches('+'))
|
||||||
"https://spdx.org/licenses/{}.html",
|
|
||||||
self.spdx().trim_end_matches('+')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1177,6 +1194,56 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bundled license table and the `License` enum are two halves of
|
||||||
|
/// one list and must never drift apart: every entry id must map to a
|
||||||
|
/// variant (and the entry must accept its own id as a spelling, so
|
||||||
|
/// the menu answers parse back), every variant must have an entry,
|
||||||
|
/// and the counts must agree. The hardcoded variant list here is the
|
||||||
|
/// lock: adding a variant or a YAML entry without the other half
|
||||||
|
/// fails this test.
|
||||||
|
#[test]
|
||||||
|
fn licenses_table_and_enum_are_in_sync() {
|
||||||
|
let variants = [
|
||||||
|
License::Mit,
|
||||||
|
License::Apache2,
|
||||||
|
License::Gpl2Plus,
|
||||||
|
License::Gpl3Plus,
|
||||||
|
License::Lgpl21Plus,
|
||||||
|
License::Lgpl3Plus,
|
||||||
|
License::Bsd2Clause,
|
||||||
|
License::Bsd3Clause,
|
||||||
|
License::Isc,
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
licenses::entries().len(),
|
||||||
|
variants.len(),
|
||||||
|
"licenses.yml and the License enum carry a different number of licenses"
|
||||||
|
);
|
||||||
|
for variant in variants {
|
||||||
|
let entry = licenses::entries()
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.id == variant.spdx())
|
||||||
|
.unwrap_or_else(|| panic!("no licenses.yml entry for '{}'", variant.spdx()));
|
||||||
|
assert_eq!(entry.id, variant.spdx());
|
||||||
|
assert!(
|
||||||
|
entry
|
||||||
|
.spellings
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.eq_ignore_ascii_case(&entry.id)),
|
||||||
|
"entry '{}' must accept its own id as a spelling",
|
||||||
|
entry.id
|
||||||
|
);
|
||||||
|
// The menu label parses back into the same variant.
|
||||||
|
assert_eq!(License::parse(&entry.menu), variant);
|
||||||
|
}
|
||||||
|
for entry in licenses::entries() {
|
||||||
|
let variant = License::from_spdx(&entry.id).unwrap_or_else(|| {
|
||||||
|
panic!("licenses.yml entry '{}' has no License variant", entry.id)
|
||||||
|
});
|
||||||
|
assert_eq!(variant.spdx(), entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collision_detection() {
|
fn collision_detection() {
|
||||||
assert!(check_file_collisions(&["a".into(), "b".into()]).is_ok());
|
assert!(check_file_collisions(&["a".into(), "b".into()]).is_ok());
|
||||||
|
|||||||
+16
-26
@@ -24,6 +24,7 @@ use indicatif::MultiProgress;
|
|||||||
|
|
||||||
use crate::new::detect::{self, Detection};
|
use crate::new::detect::{self, Detection};
|
||||||
use crate::new::git;
|
use crate::new::git;
|
||||||
|
use crate::new::licenses;
|
||||||
use crate::new::options::{self, NewCli, NewOptions, SourceDir, SourceFormat, TemplateId};
|
use crate::new::options::{self, NewCli, NewOptions, SourceDir, SourceFormat, TemplateId};
|
||||||
use crate::new::origin::GitOrigin;
|
use crate::new::origin::GitOrigin;
|
||||||
use crate::new::templates::{self, ProbeResult, ScaffoldOutcome};
|
use crate::new::templates::{self, ProbeResult, ScaffoldOutcome};
|
||||||
@@ -39,20 +40,6 @@ const SOURCE_PATH: &str = "Package the sources in another directory…";
|
|||||||
/// The "everything else" entry of the license menu.
|
/// The "everything else" entry of the license menu.
|
||||||
const LICENSE_OTHER: &str = "Other (enter a SPDX identifier)";
|
const LICENSE_OTHER: &str = "Other (enter a SPDX identifier)";
|
||||||
|
|
||||||
/// The curated SPDX identifiers of the license menu (without the free-text
|
|
||||||
/// entry), matching [`options::License::parse`]'s known spellings.
|
|
||||||
pub const KNOWN_LICENSES: [&str; 9] = [
|
|
||||||
"MIT",
|
|
||||||
"Apache-2.0",
|
|
||||||
"GPL-2.0+",
|
|
||||||
"GPL-3.0+",
|
|
||||||
"LGPL-2.1+",
|
|
||||||
"LGPL-3.0+",
|
|
||||||
"BSD-2-Clause",
|
|
||||||
"BSD-3-Clause",
|
|
||||||
"ISC",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Labels of the interactive `select` questions. `prompt::select` renders
|
/// Labels of the interactive `select` questions. `prompt::select` renders
|
||||||
/// `> <label><answer>` verbatim — unlike [`prompt::text`], it appends no
|
/// `> <label><answer>` verbatim — unlike [`prompt::text`], it appends no
|
||||||
/// formatting of its own — so each label carries its own separator:
|
/// formatting of its own — so each label carries its own separator:
|
||||||
@@ -783,29 +770,32 @@ fn language_choice(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The license menu: the curated SPDX list plus the free-text entry.
|
/// The license menu: the `menu` labels of the bundled license table
|
||||||
|
/// (`data/licenses.yml`) plus the free-text entry.
|
||||||
fn license_menu() -> Vec<String> {
|
fn license_menu() -> Vec<String> {
|
||||||
KNOWN_LICENSES
|
licenses::entries()
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.map(|entry| entry.menu.clone())
|
||||||
.map(str::to_string)
|
|
||||||
.chain(std::iter::once(LICENSE_OTHER.to_string()))
|
.chain(std::iter::once(LICENSE_OTHER.to_string()))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The defaults of the license question for a probed SPDX identifier: the
|
/// The defaults of the license question for a probed SPDX identifier: the
|
||||||
/// matching curated entry (case-insensitive) is preselected; anything else
|
/// matching curated entry (case-insensitive over the menu labels) is
|
||||||
/// preselects the free-text entry prefilled with the probe. Without a probe
|
/// preselected; anything else preselects the free-text entry prefilled with
|
||||||
/// the curated list defaults to MIT.
|
/// the probe. Without a probe the curated list defaults to MIT (the first
|
||||||
|
/// menu entry of the bundled table).
|
||||||
fn license_question_default(probe_license: Option<&str>) -> (String, String) {
|
fn license_question_default(probe_license: Option<&str>) -> (String, String) {
|
||||||
match probe_license {
|
match probe_license {
|
||||||
Some(license) => match KNOWN_LICENSES
|
Some(license) => {
|
||||||
|
match licenses::entries()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|k| k.eq_ignore_ascii_case(license))
|
.find(|entry| entry.menu.eq_ignore_ascii_case(license))
|
||||||
{
|
{
|
||||||
Some(known) => ((*known).to_string(), String::new()),
|
Some(known) => (known.menu.clone(), String::new()),
|
||||||
None => (LICENSE_OTHER.to_string(), license.to_string()),
|
None => (LICENSE_OTHER.to_string(), license.to_string()),
|
||||||
},
|
}
|
||||||
|
}
|
||||||
None => ("MIT".to_string(), String::new()),
|
None => ("MIT".to_string(), String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1237,7 +1227,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn license_menu_and_defaults() {
|
fn license_menu_and_defaults() {
|
||||||
let menu = license_menu();
|
let menu = license_menu();
|
||||||
assert_eq!(menu.len(), KNOWN_LICENSES.len() + 1);
|
assert_eq!(menu.len(), licenses::entries().len() + 1);
|
||||||
assert_eq!(menu[0], "MIT");
|
assert_eq!(menu[0], "MIT");
|
||||||
assert_eq!(menu.last().unwrap(), LICENSE_OTHER);
|
assert_eq!(menu.last().unwrap(), LICENSE_OTHER);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user