use anyhow::{Result, bail};
use std::ffi::OsStr;
use std::path::{Component, Path};
pub fn validate_name(name: &str) -> Result<()> {
let valid = !name.is_empty()
&& name.len() <= 64
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
&& name.chars().next().is_some_and(|c| c.is_ascii_alphabetic());
if !valid {
bail!("`{name}` is not a valid crates.io package name");
}
Ok(())
}
pub fn validate_bin_name(name: &str) -> Result<()> {
let mut components = Path::new(name).components();
match (components.next(), components.next()) {
(Some(Component::Normal(component)), None) if component == OsStr::new(name) => Ok(()),
_ => bail!("`{name}` is not a valid binary name"),
}
}
pub fn validate_bin_list(bins: &[String]) -> Result<()> {
if bins.is_empty() {
bail!("crate provides no binaries");
}
let mut seen = std::collections::BTreeSet::new();
for bin in bins {
validate_bin_name(bin)?;
if !seen.insert(bin.as_str()) {
bail!("binary `{bin}` is listed twice");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_validation_matches_crates_io_rules() {
assert!(validate_name("ripgrep").is_ok());
assert!(validate_name("scx-tools").is_ok());
assert!(validate_name("serde_json").is_ok());
assert!(validate_name("b2").is_ok());
assert!(validate_name("1foo").is_err());
assert!(validate_name("_foo").is_err());
assert!(validate_name("-foo").is_err());
assert!(validate_name("../../etc").is_err());
assert!(validate_name("a/b").is_err());
assert!(validate_name("żółć").is_err());
assert!(validate_name("").is_err());
}
#[test]
fn bin_names_are_single_filenames() {
assert!(validate_bin_name("scxtop").is_ok());
assert!(validate_bin_name("cargo-watch").is_ok());
assert!(validate_bin_name("foo_bar").is_ok());
assert!(validate_bin_name("foo.exe").is_ok());
assert!(validate_bin_name("../foo").is_err());
assert!(validate_bin_name("foo/bar").is_err());
assert!(validate_bin_name("/var/tmp/foo").is_err());
assert!(validate_bin_name("/etc/something").is_err());
assert!(validate_bin_name(".").is_err());
assert!(validate_bin_name("..").is_err());
assert!(validate_bin_name("").is_err());
}
#[test]
fn bin_lists_reject_duplicates() {
let ok = vec!["foo".to_owned(), "fooctl".to_owned()];
assert!(validate_bin_list(&ok).is_ok());
let dup = vec!["foo".to_owned(), "foo".to_owned()];
let err = validate_bin_list(&dup).unwrap_err().to_string();
assert!(err.contains("listed twice"), "{err}");
let bad = vec!["../foo".to_owned()];
assert!(validate_bin_list(&bad).is_err());
assert!(validate_bin_list(&[]).is_err());
}
}