Files
uv/crates/uv-normalize/src/lib.rs
T

196 lines
5.6 KiB
Rust
Raw Normal View History

2023-11-06 11:04:31 +01:00
use std::error::Error;
use std::fmt::{Display, Formatter};
pub use extra_name::ExtraName;
pub use group_name::{GroupName, DEV_DEPENDENCIES};
pub use package_name::PackageName;
mod extra_name;
mod group_name;
mod package_name;
2023-11-06 11:04:31 +01: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
/// 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> {
let mut normalized = String::with_capacity(name.as_ref().len());
let mut last = None;
for char in name.as_ref().bytes() {
match char {
b'A'..=b'Z' => {
normalized.push(char.to_ascii_lowercase() as char);
}
b'a'..=b'z' | b'0'..=b'9' => {
normalized.push(char as char);
}
b'-' | b'_' | b'.' => {
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'.') => {}
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-11-06 11:04:31 +01:00
Ok(normalized)
}
/// Returns `true` if the name is already normalized.
fn is_normalized(name: impl AsRef<str>) -> Result<bool, InvalidNameError> {
let mut last = None;
for char in name.as_ref().bytes() {
match char {
b'A'..=b'Z' => {
// Uppercase characters need to be converted to lowercase.
return Ok(false);
}
b'a'..=b'z' | b'0'..=b'9' => {}
b'_' | b'.' => {
// `_` and `.` are normalized to `-`.
return Ok(false);
}
b'-' => {
match last {
// Names can't start with punctuation.
None => return Err(InvalidNameError(name.as_ref().to_string())),
Some(b'-') => {
// 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
}
// Names can't end with punctuation.
2024-02-25 19:04:05 +00:00
if matches!(last, Some(b'-' | b'_' | b'.')) {
return Err(InvalidNameError(name.as_ref().to_string()));
2023-11-06 11:04:31 +01:00
}
Ok(true)
2023-11-06 11:04:31 +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 \
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",
"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"
);
}
}
#[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:?}");
}
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-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
);
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",
"alpha-α",
2023-11-06 11:04:31 +01:00
];
for input in failures {
assert!(validate_and_normalize_ref(input).is_err());
assert!(validate_and_normalize_owned(input.to_string()).is_err());
assert!(is_normalized(input).is_err());
2023-11-06 11:04:31 +01:00
}
}
}