use crate::error::ParseError;
use core::fmt;
use core::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Uid {
IsoOid(IsoOid),
Uuid(Uuid),
InternetId(InternetId),
}
impl Uid {
#[must_use]
pub fn value(&self) -> &str {
match self {
Self::IsoOid(v) => v.as_str(),
Self::Uuid(v) => v.as_str(),
Self::InternetId(v) => v.as_str(),
}
}
}
impl fmt::Display for Uid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.value())
}
}
impl FromStr for Uid {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(u) = s.parse::<Uuid>() {
return Ok(Self::Uuid(u));
}
if let Ok(o) = s.parse::<IsoOid>() {
return Ok(Self::IsoOid(o));
}
if let Ok(i) = s.parse::<InternetId>() {
return Ok(Self::InternetId(i));
}
Err(ParseError::new(
"UID",
"not a UUID, an ISO OID, or an internet id",
s,
))
}
}
crate::impl_string_serde!(Uid, "UID");
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IsoOid(String);
impl IsoOid {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn arcs(&self) -> impl Iterator<Item = &str> {
self.0.split('.')
}
}
impl fmt::Display for IsoOid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for IsoOid {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(ParseError::new("ISO_OID", "empty", s));
}
for arc in s.split('.') {
if arc.is_empty() {
return Err(ParseError::new("ISO_OID", "empty arc", s));
}
if !arc.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseError::new("ISO_OID", "arc is not a number", s));
}
}
Ok(Self(s.to_owned()))
}
}
#[derive(Debug, Clone, Eq)]
pub struct Uuid(String);
impl Uuid {
const GROUPS: [usize; 5] = [8, 4, 4, 4, 12];
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl PartialEq for Uuid {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl core::hash::Hash for Uuid {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
for b in self.0.bytes() {
state.write_u8(b.to_ascii_lowercase());
}
}
}
impl fmt::Display for Uuid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for Uuid {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut groups = s.split('-');
for want in Self::GROUPS {
let Some(group) = groups.next() else {
return Err(ParseError::new("UUID", "too few groups", s));
};
if group.len() != want {
return Err(ParseError::new("UUID", "group has the wrong length", s));
}
if !group.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(ParseError::new("UUID", "group is not hexadecimal", s));
}
}
if groups.next().is_some() {
return Err(ParseError::new("UUID", "too many groups", s));
}
Ok(Self(s.to_owned()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InternetId(String);
impl InternetId {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn labels(&self) -> impl Iterator<Item = &str> {
self.0.split('.')
}
}
impl fmt::Display for InternetId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for InternetId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(ParseError::new("INTERNET_ID", "empty", s));
}
for label in s.split('.') {
if label.is_empty() {
return Err(ParseError::new("INTERNET_ID", "empty label", s));
}
if !label
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
return Err(ParseError::new(
"INTERNET_ID",
"label has a character outside [A-Za-z0-9_-]",
s,
));
}
let first = label.as_bytes()[0];
let last = label.as_bytes()[label.len() - 1];
if !first.is_ascii_alphanumeric() || !last.is_ascii_alphanumeric() {
return Err(ParseError::new(
"INTERNET_ID",
"label starts or ends with a separator",
s,
));
}
}
Ok(Self(s.to_owned()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn oid_wins_over_internet_id_for_all_digit_text() {
let uid: Uid = "2.16.840".parse().unwrap();
assert!(matches!(uid, Uid::IsoOid(_)));
}
#[test]
fn uuid_case_is_preserved_but_not_significant() {
let a: Uid = "87284370-2D4B-4e3d-A3F3-F303D2F4F34B".parse().unwrap();
let b: Uid = "87284370-2d4b-4e3d-a3f3-f303d2f4f34b".parse().unwrap();
assert_eq!(a, b);
assert_eq!(a.value(), "87284370-2D4B-4e3d-A3F3-F303D2F4F34B");
}
#[test]
fn uuid_hash_agrees_with_eq() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(
"87284370-2D4B-4E3D-A3F3-F303D2F4F34B"
.parse::<Uuid>()
.unwrap(),
);
assert!(
set.contains(
&"87284370-2d4b-4e3d-a3f3-f303d2f4f34b"
.parse::<Uuid>()
.unwrap()
)
);
}
#[test]
fn rejects_near_misses() {
assert!("87284370-2D4B-4E3D-A3F3".parse::<Uuid>().is_err());
assert!(
"87284370-2D4B-4E3D-A3F3-F303D2F4F34B-0"
.parse::<Uuid>()
.is_err()
);
assert!(
"87284370_2D4B_4E3D_A3F3_F303D2F4F34B"
.parse::<Uuid>()
.is_err()
);
assert!("".parse::<Uid>().is_err());
}
}