225 lines
8.7 KiB
Rust
225 lines
8.7 KiB
Rust
//! 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")
|
|
);
|
|
}
|
|
}
|