new: validate the command name before generating files
The command/binary name was accepted verbatim and interpolated into debian/install, debian/rules, debian/tests/smoke, automake variables, meson.build and [project.scripts]: a value with a space or quote broke the install lines and shell snippets, 'my.tool' parsed as a nested TOML table (silently dropping the console script) and produced non-canonical automake variable names. Both --command and the wizard answer now go through a shared validator (lowercase identifier: letters, digits, + - . _).
This commit is contained in:
@@ -473,6 +473,29 @@ pub fn sanitize_name(input: &str) -> Option<String> {
|
|||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate the installed command (binary) name: a non-empty, ASCII-only,
|
||||||
|
/// lowercase identifier (`^[a-z0-9][a-z0-9+.\-_]*$`), matching what
|
||||||
|
/// dpkg/devscripts accept for executable names in practice. The name is
|
||||||
|
/// interpolated verbatim into `debian/install`, `debian/rules`, the
|
||||||
|
/// autopkgtest smoke test, automake variables (`{command}_SOURCES`),
|
||||||
|
/// `meson.build` and the `[project.scripts]` table, so uppercase letters,
|
||||||
|
/// whitespace, quotes and shell metacharacters are rejected here instead of
|
||||||
|
/// generating broken install lines and build files.
|
||||||
|
pub fn validate_command(command: &str) -> Result<(), String> {
|
||||||
|
static COMMAND_REGEX: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
|
||||||
|
let regex = COMMAND_REGEX.get_or_init(|| Regex::new(r"^[a-z0-9][a-z0-9+.\-_]*$").unwrap());
|
||||||
|
if command.is_empty() {
|
||||||
|
return Err("the command name must not be empty".to_string());
|
||||||
|
}
|
||||||
|
if !regex.is_match(command) {
|
||||||
|
return Err(format!(
|
||||||
|
"'{command}' is not a valid command name: commands must be a \
|
||||||
|
lowercase identifier (letters, digits, + - . _), e.g. 'mytool'"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate an upstream version: it must start with a digit (dpkg
|
/// Validate an upstream version: it must start with a digit (dpkg
|
||||||
/// recommendation, enforced here) and survive [`DebianVersion::parse`] once
|
/// recommendation, enforced here) and survive [`DebianVersion::parse`] once
|
||||||
/// composed with the Debian revision. It must not contain `-` (the revision
|
/// composed with the Debian revision. It must not contain `-` (the revision
|
||||||
@@ -763,6 +786,9 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
|
|||||||
let license = License::parse(cli.license.as_deref().unwrap_or("unknown"));
|
let license = License::parse(cli.license.as_deref().unwrap_or("unknown"));
|
||||||
|
|
||||||
let command = cli.command.unwrap_or_else(|| name.clone());
|
let command = cli.command.unwrap_or_else(|| name.clone());
|
||||||
|
// The default is the already-validated package name (a subset of the
|
||||||
|
// command charset), so only an explicit --command/wizard answer can fail.
|
||||||
|
validate_command(&command)?;
|
||||||
|
|
||||||
let maintainer = match &cli.maintainer {
|
let maintainer = match &cli.maintainer {
|
||||||
Some(m) => parse_maintainer(m)?,
|
Some(m) => parse_maintainer(m)?,
|
||||||
@@ -988,6 +1014,39 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The command name is interpolated verbatim into debian/install,
|
||||||
|
/// debian/rules, the smoke test, automake variables, meson.build and
|
||||||
|
/// [project.scripts], so only the safe identifier charset passes.
|
||||||
|
#[test]
|
||||||
|
fn command_validator() {
|
||||||
|
for valid in [
|
||||||
|
"mytool",
|
||||||
|
"my.tool", // automake/TOML-friendly spellings in use in Debian
|
||||||
|
"my+tool",
|
||||||
|
"my_tool",
|
||||||
|
"my-tool",
|
||||||
|
"2ping", // leading digit
|
||||||
|
"a", // single character
|
||||||
|
"a1.b+c-d_e",
|
||||||
|
] {
|
||||||
|
assert!(validate_command(valid).is_ok(), "{valid} must pass");
|
||||||
|
}
|
||||||
|
for invalid in [
|
||||||
|
"my tool", // whitespace breaks install lines
|
||||||
|
"a\"b", // quotes break shell snippets
|
||||||
|
"MyTool", // uppercase
|
||||||
|
"-lead", // bad first char
|
||||||
|
"_lead", // bad first char
|
||||||
|
".dot", // bad first char
|
||||||
|
"café", // non-ASCII
|
||||||
|
"a:b", // shell metacharacter
|
||||||
|
"a/b", // path separator
|
||||||
|
"", // empty
|
||||||
|
] {
|
||||||
|
assert!(validate_command(invalid).is_err(), "{invalid} must fail");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sanitize_name_derives_valid_names() {
|
fn sanitize_name_derives_valid_names() {
|
||||||
assert_eq!(sanitize_name("My Tool"), Some("my-tool".to_string()));
|
assert_eq!(sanitize_name("My Tool"), Some("my-tool".to_string()));
|
||||||
|
|||||||
@@ -383,15 +383,16 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 9. Command name (skipped for the empty template, where nothing is
|
// 9. Command name (skipped for the empty template, where nothing is
|
||||||
// installed). The probed default must pass the required-answer check
|
// installed). Typed answers and the offered default go through the
|
||||||
// too, or the question is asked without one.
|
// same `validate_command` bar as `resolve` applies (which also
|
||||||
|
// requires a non-empty answer), so an unusable probe is withheld and
|
||||||
|
// invalid input re-asks here instead of failing late in `resolve`.
|
||||||
if cli.command.is_none() && template != TemplateId::Empty {
|
if cli.command.is_none() && template != TemplateId::Empty {
|
||||||
let default = probe
|
let default = probe
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| p.command.clone())
|
.and_then(|p| p.command.clone())
|
||||||
.unwrap_or_else(|| cli.name.clone().unwrap_or_default());
|
.unwrap_or_else(|| cli.name.clone().unwrap_or_default());
|
||||||
let validate = required_answer("the command name");
|
let command = ask_text("Command name", &default, options::validate_command)?;
|
||||||
let command = ask_text("Command name", &default, validate)?;
|
|
||||||
cli.command = Some(command);
|
cli.command = Some(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user