#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{borrow::Cow, string::String};
#[cfg(feature = "std")]
use std::borrow::Cow;
use core::fmt;
#[macro_use]
mod macros;
pub mod builders;
pub mod error;
pub mod spec;
#[cfg(test)]
pub mod specs;
pub use builders::{UrnBuilder, UrnSpecBuilder};
pub use error::UrnValidationError;
pub use spec::{UrnComponents, UrnSpec};
use crate::der::{Decode, DecodeValue, EncodeValue, Tag, Tagged};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Urn<'a> {
pub nid: Cow<'a, str>,
pub nss: Cow<'a, str>,
}
impl<'a> Urn<'a> {
#[inline]
pub const fn new(nid: &'static str, nss: &'static str) -> Urn<'static> {
Urn { nid: Cow::Borrowed(nid), nss: Cow::Borrowed(nss) }
}
pub fn verify<S: UrnSpec>(&self) -> Result<(), UrnValidationError> {
if self.nid.as_ref() != S::NID {
return Err(UrnValidationError::NidMismatch);
}
Self::validate_nid(self.nid.as_ref())?;
let builder = UrnBuilder::default().with_nid(self.nid.as_ref()).with_nss(self.nss.as_ref());
S::validate(&builder as &dyn UrnComponents)
}
pub fn validate_nid(nid: &str) -> Result<(), UrnValidationError> {
let len = nid.len();
if !(2..=32).contains(&len) {
return Err(UrnValidationError::InvalidNidLength);
}
let mut chars = nid.chars();
if let Some(first) = chars.next() {
if !first.is_ascii_alphabetic() {
return Err(UrnValidationError::InvalidNidStart);
}
}
for ch in chars {
if !ch.is_ascii_alphanumeric() && ch != '-' {
return Err(UrnValidationError::InvalidNidCharacters);
}
}
Ok(())
}
#[inline]
pub fn into_owned(self) -> Urn<'static> {
Urn { nid: Cow::Owned(self.nid.into_owned()), nss: Cow::Owned(self.nss.into_owned()) }
}
}
impl<'a> fmt::Display for Urn<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "urn:{}:{}", self.nid, self.nss)
}
}
impl<'a> Tagged for Urn<'a> {
fn tag(&self) -> Tag {
Tag::Utf8String
}
}
impl<'a> EncodeValue for Urn<'a> {
fn value_len(&self) -> crate::der::Result<crate::der::Length> {
let total_len = 4 + self.nid.len() + 1 + self.nss.len();
crate::der::Length::try_from(total_len)
}
fn encode_value(&self, encoder: &mut impl crate::der::Writer) -> crate::der::Result<()> {
encoder.write(b"urn:")?;
encoder.write(self.nid.as_bytes())?;
encoder.write(b":")?;
encoder.write(self.nss.as_bytes())?;
Ok(())
}
}
impl<'a> DecodeValue<'a> for Urn<'a> {
fn decode_value<R: crate::der::Reader<'a>>(
reader: &mut R,
_header: crate::der::Header,
) -> crate::der::Result<Self> {
let utf8_str = String::decode_value(reader, _header)?;
let urn_str = utf8_str.as_str();
if !urn_str.starts_with("urn:") {
return Err(crate::der::ErrorKind::Value { tag: Tag::Utf8String }.into());
}
let rest = &urn_str[4..]; let colon_pos = rest.find(':').ok_or(crate::der::ErrorKind::Value { tag: Tag::Utf8String })?;
let nid = &rest[..colon_pos];
let nss = &rest[colon_pos + 1..];
Ok(Urn { nid: Cow::Owned(nid.to_string()), nss: Cow::Owned(nss.to_string()) })
}
}
impl<'a> Decode<'a> for Urn<'a> {
fn decode<R: crate::der::Reader<'a>>(reader: &mut R) -> crate::der::Result<Self> {
let header = reader.peek_header()?;
Self::decode_value(reader, header)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_urn_nid_validation() {
let valid_cases: &[&str] = &["ab", "tightbeam", "isbn", "a123", "my-namespace"];
for nid in valid_cases {
assert!(Urn::validate_nid(nid).is_ok());
}
assert!(Urn::validate_nid("a").is_err());
let too_long = "a".repeat(33);
assert!(Urn::validate_nid(&too_long).is_err());
let invalid_start: &[&str] = &["1abc", "-abc"];
for nid in invalid_start {
assert!(Urn::validate_nid(nid).is_err());
}
let invalid_chars: &[&str] = &["ab_cd", "ab.cd"];
for nid in invalid_chars {
assert!(Urn::validate_nid(nid).is_err());
}
}
#[test]
fn test_urn_to_owned() {
let test_cases: &[(&str, &str)] = &[
("tightbeam", "test:resource"),
("example", "path/to/resource"),
("test", "simple"),
];
for (nid, nss) in test_cases {
let urn = Urn { nid: Cow::Borrowed(*nid), nss: Cow::Borrowed(*nss) };
let owned_urn = urn.into_owned();
assert!(matches!(owned_urn.nid, Cow::Owned(_)));
assert!(matches!(owned_urn.nss, Cow::Owned(_)));
assert_eq!(owned_urn.nid, *nid);
assert_eq!(owned_urn.nss, *nss);
}
}
}