refactor(util-schemas): error type for restricted_names

This commit is contained in:
Weihang Lo 2023-12-19 03:40:38 -05:00
parent 90017c0747
commit a3267bfa29
No known key found for this signature in database
GPG key ID: D7DBF189825E82E7
5 changed files with 103 additions and 84 deletions

View file

@ -10,7 +10,6 @@ use std::fmt::{self, Display, Write};
use std::path::PathBuf;
use std::str;
use anyhow::Result;
use serde::de::{self, IntoDeserializer as _, Unexpected};
use serde::ser;
use serde::{Deserialize, Serialize};
@ -21,6 +20,8 @@ use crate::core::PartialVersion;
use crate::core::PartialVersionError;
use crate::restricted_names;
pub use crate::restricted_names::NameValidationError;
/// This type is used to deserialize `Cargo.toml` files.
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
@ -1145,7 +1146,7 @@ macro_rules! str_newtype {
}
impl<'a> std::str::FromStr for $name<String> {
type Err = anyhow::Error;
type Err = restricted_names::NameValidationError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value.to_owned())
@ -1174,7 +1175,7 @@ str_newtype!(PackageName);
impl<T: AsRef<str>> PackageName<T> {
/// Validated package name
pub fn new(name: T) -> Result<Self> {
pub fn new(name: T) -> Result<Self, NameValidationError> {
restricted_names::validate_package_name(name.as_ref(), "package name", "")?;
Ok(Self(name))
}
@ -1196,7 +1197,7 @@ str_newtype!(RegistryName);
impl<T: AsRef<str>> RegistryName<T> {
/// Validated registry name
pub fn new(name: T) -> Result<Self> {
pub fn new(name: T) -> Result<Self, NameValidationError> {
restricted_names::validate_package_name(name.as_ref(), "registry name", "")?;
Ok(Self(name))
}
@ -1206,7 +1207,7 @@ str_newtype!(ProfileName);
impl<T: AsRef<str>> ProfileName<T> {
/// Validated profile name
pub fn new(name: T) -> Result<Self> {
pub fn new(name: T) -> Result<Self, NameValidationError> {
restricted_names::validate_profile_name(name.as_ref())?;
Ok(Self(name))
}
@ -1216,7 +1217,7 @@ str_newtype!(FeatureName);
impl<T: AsRef<str>> FeatureName<T> {
/// Validated feature name
pub fn new(name: T) -> Result<Self> {
pub fn new(name: T) -> Result<Self, NameValidationError> {
restricted_names::validate_feature_name(name.as_ref())?;
Ok(Self(name))
}

View file

@ -1,7 +1,32 @@
//! Helpers for validating and checking names like package and crate names.
use anyhow::bail;
use anyhow::Result;
type Result<T> = std::result::Result<T, NameValidationError>;
/// Error validating names in Cargo.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum NameValidationError {
#[error("{0} cannot be empty")]
Empty(&'static str),
#[error("invalid character `{ch}` in {what}: `{name}`, {reason}")]
InvalidCharacter {
ch: char,
what: &'static str,
name: String,
reason: &'static str,
},
#[error(
"profile name `{name}` is reserved\n{help}\n\
See https://doc.rust-lang.org/cargo/reference/profiles.html \
for more on configuring profiles."
)]
ProfileNameReservedKeyword { name: String, help: &'static str },
#[error("feature named `{0}` is not allowed to start with `dep:`")]
FeatureNameStartsWithDepColon(String),
}
/// Check the base requirements for a package name.
///
@ -9,46 +34,41 @@ use anyhow::Result;
/// level of sanity. Note that package names have other restrictions
/// elsewhere. `cargo new` has a few restrictions, such as checking for
/// reserved names. crates.io has even more restrictions.
pub fn validate_package_name(name: &str, what: &str, help: &str) -> Result<()> {
pub fn validate_package_name(name: &str, what: &'static str, help: &str) -> Result<()> {
if name.is_empty() {
bail!("{what} cannot be empty");
return Err(NameValidationError::Empty(what));
}
let mut chars = name.chars();
if let Some(ch) = chars.next() {
if ch.is_digit(10) {
// A specific error for a potentially common case.
bail!(
"the name `{}` cannot be used as a {}, \
the name cannot start with a digit{}",
name,
what,
help
);
}
if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_') {
bail!(
"invalid character `{}` in {}: `{}`, \
the first character must be a Unicode XID start character \
(most letters or `_`){}",
return Err(NameValidationError::InvalidCharacter {
ch,
what,
name,
help
);
name: name.into(),
reason: "the name cannot start with a digit",
});
}
if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_') {
return Err(NameValidationError::InvalidCharacter {
ch,
what,
name: name.into(),
reason: "the first character must be a Unicode XID start character \
(most letters or `_`)",
});
}
}
for ch in chars {
if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-') {
bail!(
"invalid character `{}` in {}: `{}`, \
characters must be Unicode XID characters \
(numbers, `-`, `_`, or most letters){}",
return Err(NameValidationError::InvalidCharacter {
ch,
what,
name,
help
);
name: name.into(),
reason: "characters must be Unicode XID characters \
(numbers, `-`, `_`, or most letters)",
});
}
}
Ok(())
@ -83,37 +103,28 @@ pub fn validate_profile_name(name: &str) -> Result<()> {
.chars()
.find(|ch| !ch.is_alphanumeric() && *ch != '_' && *ch != '-')
{
bail!(
"invalid character `{}` in profile name `{}`\n\
Allowed characters are letters, numbers, underscore, and hyphen.",
return Err(NameValidationError::InvalidCharacter {
ch,
name
);
what: "profile name",
name: name.into(),
reason: "allowed characters are letters, numbers, underscore, and hyphen",
});
}
const SEE_DOCS: &str = "See https://doc.rust-lang.org/cargo/reference/profiles.html \
for more on configuring profiles.";
let lower_name = name.to_lowercase();
if lower_name == "debug" {
bail!(
"profile name `{}` is reserved\n\
To configure the default development profile, use the name `dev` \
as in [profile.dev]\n\
{}",
name,
SEE_DOCS
);
return Err(NameValidationError::ProfileNameReservedKeyword {
name: name.into(),
help: "To configure the default development profile, \
use the name `dev` as in [profile.dev]",
});
}
if lower_name == "build-override" {
bail!(
"profile name `{}` is reserved\n\
To configure build dependency settings, use [profile.dev.build-override] \
and [profile.release.build-override]\n\
{}",
name,
SEE_DOCS
);
return Err(NameValidationError::ProfileNameReservedKeyword {
name: name.into(),
help: "To configure build dependency settings, use [profile.dev.build-override] \
and [profile.release.build-override]",
});
}
// These are some arbitrary reservations. We have no plans to use
@ -144,46 +155,55 @@ pub fn validate_profile_name(name: &str) -> Result<()> {
| "uninstall"
) || lower_name.starts_with("cargo")
{
bail!(
"profile name `{}` is reserved\n\
Please choose a different name.\n\
{}",
name,
SEE_DOCS
);
return Err(NameValidationError::ProfileNameReservedKeyword {
name: name.into(),
help: "Please choose a different name.",
});
}
Ok(())
}
pub fn validate_feature_name(name: &str) -> Result<()> {
let what = "feature name";
if name.is_empty() {
bail!("feature name cannot be empty");
return Err(NameValidationError::Empty(what));
}
if name.starts_with("dep:") {
bail!("feature named `{name}` is not allowed to start with `dep:`",);
return Err(NameValidationError::FeatureNameStartsWithDepColon(
name.into(),
));
}
if name.contains('/') {
bail!("feature named `{name}` is not allowed to contain slashes",);
return Err(NameValidationError::InvalidCharacter {
ch: '/',
what,
name: name.into(),
reason: "feature name is not allowed to contain slashes",
});
}
let mut chars = name.chars();
if let Some(ch) = chars.next() {
if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_' || ch.is_digit(10)) {
bail!(
"invalid character `{ch}` in feature `{name}`, \
the first character must be a Unicode XID start character or digit \
(most letters or `_` or `0` to `9`)",
);
return Err(NameValidationError::InvalidCharacter {
ch,
what,
name: name.into(),
reason: "the first character must be a Unicode XID start character or digit \
(most letters or `_` or `0` to `9`)",
});
}
}
for ch in chars {
if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-' || ch == '+' || ch == '.') {
bail!(
"invalid character `{ch}` in feature `{name}`, \
characters must be Unicode XID characters, '-', `+`, or `.` \
(numbers, `+`, `-`, `_`, `.`, or most letters)",
);
return Err(NameValidationError::InvalidCharacter {
ch,
what,
name: name.into(),
reason: "characters must be Unicode XID characters, '-', `+`, or `.` \
(numbers, `+`, `-`, `_`, `.`, or most letters)",
});
}
}
Ok(())

View file

@ -2063,7 +2063,7 @@ Caused by:
|
8 | \"+foo\" = []
| ^^^^^^
invalid character `+` in feature `+foo`, \
invalid character `+` in feature name: `+foo`, \
the first character must be a Unicode XID start character or digit \
(most letters or `_` or `0` to `9`)
",
@ -2094,7 +2094,7 @@ Caused by:
|
8 | \"a&b\" = []
| ^^^^^
invalid character `&` in feature `a&b`, \
invalid character `&` in feature name: `a&b`, \
characters must be Unicode XID characters, '-', `+`, or `.` \
(numbers, `+`, `-`, `_`, `.`, or most letters)
",
@ -2131,7 +2131,7 @@ Caused by:
|
7 | \"foo/bar\" = []
| ^^^^^^^^^
feature named `foo/bar` is not allowed to contain slashes
invalid character `/` in feature name: `foo/bar`, feature name is not allowed to contain slashes
",
)
.run();

View file

@ -348,8 +348,7 @@ fn explicit_invalid_name_not_suggested() {
.with_status(101)
.with_stderr(
"\
[ERROR] the name `10-invalid` cannot be used as a package name, \
the name cannot start with a digit\n\
[ERROR] invalid character `1` in package name: `10-invalid`, the name cannot start with a digit
If you need a binary with the name \"10-invalid\", use a valid package name, \
and set the binary name to be different from the package. \
This can be done by setting the binary filename to `src/bin/10-invalid.rs` \

View file

@ -90,8 +90,7 @@ Caused by:
|
7 | [profile.'.release-lto']
| ^^^^^^^^^^^^^^
invalid character `.` in profile name `.release-lto`
Allowed characters are letters, numbers, underscore, and hyphen.
invalid character `.` in profile name: `.release-lto`, allowed characters are letters, numbers, underscore, and hyphen
",
)
.run();