2023-11-06 11:04:31 +01:00
|
|
|
|
use std::error::Error;
|
|
|
|
|
|
use std::fmt::{Display, Formatter};
|
|
|
|
|
|
|
2023-11-02 12:15:27 +01:00
|
|
|
|
pub use extra_name::ExtraName;
|
2024-08-01 17:04:30 -04:00
|
|
|
|
pub use group_name::{GroupName, DEV_DEPENDENCIES};
|
2023-11-02 12:15:27 +01:00
|
|
|
|
pub use package_name::PackageName;
|
|
|
|
|
|
|
|
|
|
|
|
mod extra_name;
|
2024-06-05 21:40:17 -04:00
|
|
|
|
mod group_name;
|
2023-11-02 12:15:27 +01:00
|
|
|
|
mod package_name;
|
2023-11-06 11:04:31 +01:00
|
|
|
|
|
2023-12-12 00:50:48 -05:00
|
|
|
|
/// Validate and normalize an owned package or extra name.
|
|
|
|
|
|
pub(crate) fn validate_and_normalize_owned(name: String) -> Result<String, InvalidNameError> {
|
|
|
|
|
|
if is_normalized(&name)? {
|
|
|
|
|
|
Ok(name)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
validate_and_normalize_ref(name)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-11-06 11:04:31 +01:00
|
|
|
|
|
2023-12-12 00:50:48 -05:00
|
|
|
|
/// Validate and normalize an unowned package or extra name.
|
2023-11-06 11:04:31 +01:00
|
|
|
|
pub(crate) fn validate_and_normalize_ref(
|
|
|
|
|
|
name: impl AsRef<str>,
|
|
|
|
|
|
) -> Result<String, InvalidNameError> {
|
2023-12-12 00:50:48 -05:00
|
|
|
|
let mut normalized = String::with_capacity(name.as_ref().len());
|
|
|
|
|
|
|
|
|
|
|
|
let mut last = None;
|
2023-12-12 10:01:02 -05:00
|
|
|
|
for char in name.as_ref().bytes() {
|
2023-12-12 00:50:48 -05:00
|
|
|
|
match char {
|
2023-12-12 10:01:02 -05:00
|
|
|
|
b'A'..=b'Z' => {
|
|
|
|
|
|
normalized.push(char.to_ascii_lowercase() as char);
|
2023-12-12 00:50:48 -05:00
|
|
|
|
}
|
2023-12-12 10:01:02 -05:00
|
|
|
|
b'a'..=b'z' | b'0'..=b'9' => {
|
|
|
|
|
|
normalized.push(char as char);
|
2023-12-12 00:50:48 -05:00
|
|
|
|
}
|
2023-12-12 10:01:02 -05:00
|
|
|
|
b'-' | b'_' | b'.' => {
|
2023-12-12 00:50:48 -05:00
|
|
|
|
match last {
|
|
|
|
|
|
// Names can't start with punctuation.
|
|
|
|
|
|
None => return Err(InvalidNameError(name.as_ref().to_string())),
|
2024-02-25 19:04:05 +00:00
|
|
|
|
Some(b'-' | b'_' | b'.') => {}
|
2023-12-12 00:50:48 -05:00
|
|
|
|
Some(_) => normalized.push('-'),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => return Err(InvalidNameError(name.as_ref().to_string())),
|
|
|
|
|
|
}
|
|
|
|
|
|
last = Some(char);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Names can't end with punctuation.
|
2024-02-25 19:04:05 +00:00
|
|
|
|
if matches!(last, Some(b'-' | b'_' | b'.')) {
|
2023-11-06 11:04:31 +01:00
|
|
|
|
return Err(InvalidNameError(name.as_ref().to_string()));
|
|
|
|
|
|
}
|
2023-12-12 00:50:48 -05:00
|
|
|
|
|
2023-11-06 11:04:31 +01:00
|
|
|
|
Ok(normalized)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-12 00:50:48 -05:00
|
|
|
|
/// Returns `true` if the name is already normalized.
|
|
|
|
|
|
fn is_normalized(name: impl AsRef<str>) -> Result<bool, InvalidNameError> {
|
|
|
|
|
|
let mut last = None;
|
2023-12-12 10:01:02 -05:00
|
|
|
|
for char in name.as_ref().bytes() {
|
2023-12-12 00:50:48 -05:00
|
|
|
|
match char {
|
2023-12-12 10:01:02 -05:00
|
|
|
|
b'A'..=b'Z' => {
|
2023-12-12 00:50:48 -05:00
|
|
|
|
// Uppercase characters need to be converted to lowercase.
|
|
|
|
|
|
return Ok(false);
|
|
|
|
|
|
}
|
2023-12-12 10:01:02 -05:00
|
|
|
|
b'a'..=b'z' | b'0'..=b'9' => {}
|
|
|
|
|
|
b'_' | b'.' => {
|
2023-12-12 00:50:48 -05:00
|
|
|
|
// `_` and `.` are normalized to `-`.
|
|
|
|
|
|
return Ok(false);
|
|
|
|
|
|
}
|
2023-12-12 10:01:02 -05:00
|
|
|
|
b'-' => {
|
2023-12-12 00:50:48 -05:00
|
|
|
|
match last {
|
|
|
|
|
|
// Names can't start with punctuation.
|
|
|
|
|
|
None => return Err(InvalidNameError(name.as_ref().to_string())),
|
2023-12-12 10:01:02 -05:00
|
|
|
|
Some(b'-') => {
|
2023-12-12 00:50:48 -05:00
|
|
|
|
// Runs of `-` are normalized to a single `-`.
|
|
|
|
|
|
return Ok(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(_) => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => return Err(InvalidNameError(name.as_ref().to_string())),
|
|
|
|
|
|
}
|
|
|
|
|
|
last = Some(char);
|
2023-11-06 11:04:31 +01:00
|
|
|
|
}
|
2023-12-12 00:50:48 -05:00
|
|
|
|
|
|
|
|
|
|
// Names can't end with punctuation.
|
2024-02-25 19:04:05 +00:00
|
|
|
|
if matches!(last, Some(b'-' | b'_' | b'.')) {
|
2023-12-12 00:50:48 -05:00
|
|
|
|
return Err(InvalidNameError(name.as_ref().to_string()));
|
2023-11-06 11:04:31 +01:00
|
|
|
|
}
|
2023-12-12 00:50:48 -05:00
|
|
|
|
|
|
|
|
|
|
Ok(true)
|
2023-11-06 11:04:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-20 19:33:27 +01:00
|
|
|
|
/// Invalid [`crate::PackageName`] or [`crate::ExtraName`].
|
2023-11-06 11:04:31 +01:00
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
|
pub struct InvalidNameError(String);
|
|
|
|
|
|
|
2024-02-16 00:01:21 -05:00
|
|
|
|
impl InvalidNameError {
|
|
|
|
|
|
/// Returns the invalid name.
|
|
|
|
|
|
pub fn as_str(&self) -> &str {
|
|
|
|
|
|
&self.0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-11-06 11:04:31 +01:00
|
|
|
|
impl Display for InvalidNameError {
|
|
|
|
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
|
write!(
|
|
|
|
|
|
f,
|
|
|
|
|
|
"Not a valid package or extra name: \"{}\". Names must start and end with a letter or \
|
2023-12-12 00:50:48 -05:00
|
|
|
|
digit and may only contain -, _, ., and alphanumeric characters.",
|
2023-11-06 11:04:31 +01:00
|
|
|
|
self.0
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Error for InvalidNameError {}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn normalize() {
|
|
|
|
|
|
let inputs = [
|
|
|
|
|
|
"friendly-bard",
|
|
|
|
|
|
"Friendly-Bard",
|
|
|
|
|
|
"FRIENDLY-BARD",
|
|
|
|
|
|
"friendly.bard",
|
|
|
|
|
|
"friendly_bard",
|
|
|
|
|
|
"friendly--bard",
|
2023-12-12 00:50:48 -05:00
|
|
|
|
"friendly-.bard",
|
2023-11-06 11:04:31 +01:00
|
|
|
|
"FrIeNdLy-._.-bArD",
|
|
|
|
|
|
];
|
|
|
|
|
|
for input in inputs {
|
|
|
|
|
|
assert_eq!(validate_and_normalize_ref(input).unwrap(), "friendly-bard");
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
validate_and_normalize_owned(input.to_string()).unwrap(),
|
|
|
|
|
|
"friendly-bard"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-12 00:50:48 -05:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn check() {
|
|
|
|
|
|
let inputs = ["friendly-bard", "friendlybard"];
|
|
|
|
|
|
for input in inputs {
|
2024-02-25 19:04:05 +00:00
|
|
|
|
assert!(is_normalized(input).unwrap(), "{input:?}");
|
2023-12-12 00:50:48 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let inputs = [
|
|
|
|
|
|
"friendly.bard",
|
|
|
|
|
|
"friendly.BARD",
|
|
|
|
|
|
"friendly_bard",
|
|
|
|
|
|
"friendly--bard",
|
|
|
|
|
|
"friendly-.bard",
|
|
|
|
|
|
"FrIeNdLy-._.-bArD",
|
|
|
|
|
|
];
|
|
|
|
|
|
for input in inputs {
|
2024-02-25 19:04:05 +00:00
|
|
|
|
assert!(!is_normalized(input).unwrap(), "{input:?}");
|
2023-12-12 00:50:48 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-11-06 11:04:31 +01:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn unchanged() {
|
|
|
|
|
|
// Unchanged
|
|
|
|
|
|
let unchanged = ["friendly-bard", "1okay", "okay2"];
|
|
|
|
|
|
for input in unchanged {
|
|
|
|
|
|
assert_eq!(validate_and_normalize_ref(input).unwrap(), input);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
validate_and_normalize_owned(input.to_string()).unwrap(),
|
|
|
|
|
|
input
|
|
|
|
|
|
);
|
2023-12-12 00:50:48 -05:00
|
|
|
|
assert!(is_normalized(input).unwrap());
|
2023-11-06 11:04:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn failures() {
|
|
|
|
|
|
let failures = [
|
|
|
|
|
|
" starts-with-space",
|
|
|
|
|
|
"-starts-with-dash",
|
|
|
|
|
|
"ends-with-dash-",
|
|
|
|
|
|
"ends-with-space ",
|
|
|
|
|
|
"includes!invalid-char",
|
|
|
|
|
|
"space in middle",
|
2023-12-12 10:01:02 -05:00
|
|
|
|
"alpha-α",
|
2023-11-06 11:04:31 +01:00
|
|
|
|
];
|
|
|
|
|
|
for input in failures {
|
|
|
|
|
|
assert!(validate_and_normalize_ref(input).is_err());
|
2023-12-12 00:50:48 -05:00
|
|
|
|
assert!(validate_and_normalize_owned(input.to_string()).is_err());
|
|
|
|
|
|
assert!(is_normalized(input).is_err());
|
2023-11-06 11:04:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|