use std::{
borrow::Borrow,
cmp::Ordering,
fmt::{self, Display, Formatter},
hash::{Hash, Hasher},
num::ParseIntError,
};
use thiserror::Error;
use super::char_is_alnum;
#[derive(Debug, Clone, Copy, Eq)]
pub struct Name<'a> {
inner: &'a str,
registry_end: Option<usize>,
}
impl<'a> Name<'a> {
pub fn new(name: &'a str) -> Result<Self, InvalidNamePartError> {
let mut split = name.split('/');
let mut registry_end = None;
if let Some(mut first) = split.next() {
if first.contains('.') {
registry_end = Some(first.len());
if let Some((host, port)) = first.split_once(':') {
port.parse::<u16>()
.map_err(|source| InvalidNamePartError::RegistryPort {
source,
port: port.to_owned(),
})?;
first = host;
}
}
validate_part(first)?;
}
for part in split {
validate_part(part)?;
}
Ok(Self {
inner: name,
registry_end,
})
}
pub(super) const fn new_unchecked(name: &'a str, registry_end: Option<usize>) -> Self {
Self {
inner: name,
registry_end,
}
}
pub(super) const fn registry_end(&self) -> Option<usize> {
self.registry_end
}
#[must_use]
pub fn registry(&self) -> Option<&str> {
self.registry_end.map(|end| {
#[allow(clippy::indexing_slicing, clippy::string_slice)]
&self.inner[..end]
})
}
#[must_use]
pub const fn into_inner(self) -> &'a str {
self.inner
}
}
fn validate_part(part: &str) -> Result<(), InvalidNamePartError> {
let mut dots: u8 = 0;
let mut underscores: u8 = 0;
let mut prev_char_dash = false;
part.chars().try_for_each(|char| match char {
'a'..='z' | '0'..='9' => {
dots = 0;
underscores = 0;
prev_char_dash = false;
Ok(())
}
'-' => {
if dots == 0 && underscores == 0 {
prev_char_dash = true;
Ok(())
} else {
Err(InvalidNamePartError::MultipleSeparators)
}
}
'.' => {
dots += 1;
if dots == 1 && underscores == 0 && !prev_char_dash {
prev_char_dash = false;
Ok(())
} else {
Err(InvalidNamePartError::MultipleSeparators)
}
}
'_' => {
underscores += 1;
if dots == 0 && underscores <= 2 && !prev_char_dash {
prev_char_dash = false;
Ok(())
} else {
Err(InvalidNamePartError::MultipleSeparators)
}
}
char => Err(InvalidNamePartError::Character(char)),
})?;
if part.is_empty() {
Err(InvalidNamePartError::Empty)
} else if !part.starts_with(char_is_alnum) {
Err(InvalidNamePartError::Start)
} else if !part.ends_with(char_is_alnum) {
Err(InvalidNamePartError::End)
} else {
Ok(())
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidNamePartError {
#[error("image name parts may only have one separator (., _, __, any number of -) in a row")]
MultipleSeparators,
#[error(
"invalid character in image name '{0}', name parts must only contain \
lowercase ASCII letters (a-z), digits (0-9), dashes (-), dots (.), and underscores (_)"
)]
Character(char),
#[error(
"a part of an image name cannot be empty, i.e. there were two slashes (/) in row, \
or the image name was completely empty"
)]
Empty,
#[error("image name parts must start with a lowercase ASCII letter (a-z) or a digit (0-9)")]
Start,
#[error("image name parts must end with a lowercase ASCII letter (a-z) or a digit (0-9)")]
End,
#[error("image registry port `{port}` is not a valid port number")]
RegistryPort {
source: ParseIntError,
port: String,
},
}
impl<'a> AsRef<str> for Name<'a> {
fn as_ref(&self) -> &str {
self.inner
}
}
impl<'a> Borrow<str> for Name<'a> {
fn borrow(&self) -> &str {
self.inner
}
}
impl<'a> TryFrom<&'a str> for Name<'a> {
type Error = InvalidNamePartError;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl<'a> PartialEq for Name<'a> {
fn eq(&self, other: &Self) -> bool {
self.inner.eq(other.inner)
}
}
impl<'a> PartialEq<str> for Name<'a> {
fn eq(&self, other: &str) -> bool {
self.inner == other
}
}
impl<'a> PartialEq<&str> for Name<'a> {
fn eq(&self, other: &&str) -> bool {
self.inner == *other
}
}
impl<'a> PartialOrd for Name<'a> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<'a> Ord for Name<'a> {
fn cmp(&self, other: &Self) -> Ordering {
self.inner.cmp(other.inner)
}
}
impl<'a> Hash for Name<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
impl<'a> Display for Name<'a> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(self.inner)
}
}
#[cfg(test)]
mod tests {
use std::fmt::Write;
use pomsky_macro::pomsky;
use proptest::{prop_assert_eq, proptest};
use super::*;
const NAME: &str = pomsky! {
let end = [ascii_lower ascii_digit]+;
let separator = '.' | '_' | "__" | '-'+;
let part = end (separator end)*;
part ('/' part)*
};
const REGISTRY: &str = pomsky! {
let end = [ascii_lower ascii_digit]+;
let separator = '.' | '_' | "__" | '-'+;
let part = end (separator end)*;
part '.' part
};
proptest! {
#[test]
fn no_panic(name: String) {
let _ = Name::new(&name);
}
#[test]
#[ignore]
fn new(name in NAME) {
Name::new(&name)?;
}
#[test]
#[ignore]
fn registry(mut registry in REGISTRY, port: Option<u16>, rest in NAME) {
if let Some(port) = port {
write!(registry, ":{port}")?;
}
let name = format!("{registry}/{rest}");
let name = Name::new(&name)?;
prop_assert_eq!(name.registry(), Some(registry.as_str()));
}
}
}