new: drive the license menu, parsing and sniffing from data/licenses.yml

This commit is contained in:
2026-09-18 13:52:24 +02:00
parent 12407c8eac
commit dd1c70c91a
6 changed files with 496 additions and 100 deletions
+26 -52
View File
@@ -23,6 +23,7 @@ use std::path::Path;
use regex::Regex;
use super::licenses;
use super::options::TemplateId;
/// Outcome of the detection.
@@ -109,23 +110,14 @@ fn has_shebang(path: &Path) -> bool {
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`
/// file: an `SPDX-License-Identifier:` line wins, otherwise the text is
/// matched against a short list of recognizable licenses (MIT, BSD-2/3,
/// Apache-2.0, GPL-2/3, LGPL-2.1/3, ISC). `None` when no license file
/// exists or nothing recognizable is found.
/// matched against the marker sets of the bundled license table
/// (`data/licenses.yml`: MIT, BSD-2/3, Apache-2.0, GPL-2/3, LGPL-2.1/3,
/// ISC). `None` when no license file exists or nothing recognizable is
/// found.
pub fn sniff_license(dir: &Path) -> Option<String> {
let content = LICENSE_FILES
.iter()
let content = licenses::detect_files()
.find_map(|name| std::fs::read_to_string(dir.join(name)).ok())
// Case variants and suffixes (LICENSE-MIT, LICENCE, cpYING…): the
// 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);
}
// The recognizable-license markers live in the bundled table; the
// LICENSE_TEXTS test below is their behavioral lock.
let text = content.to_ascii_lowercase();
if text.contains("apache license") && text.contains("version 2") {
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
licenses::detect_from_text(&text).map(str::to_string)
}
#[cfg(test)]
@@ -302,8 +261,10 @@ mod tests {
assert_eq!(detect(dir.path()), Detection::Empty);
}
/// Distinctive (shortened) excerpts of the recognizable license texts.
const LICENSE_TEXTS: [(&str, &str); 9] = [
/// Distinctive (shortened) excerpts of the recognizable license texts:
/// 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 License\n\nPermission is hereby granted, free of charge, to any person",
@@ -337,6 +298,19 @@ mod tests {
"ISC",
"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]