use std::{error, fmt, mem};
use std::borrow::Cow;
use std::fmt::Write as _;
use std::str::FromStr;
use rpki::ca::idexchange::Handle;
use rpki::crypto::keys::KeyIdentifier;
#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[repr(transparent)]
pub struct Ident(str);
impl Ident {
pub const fn from_bytes(bytes: &[u8]) -> Result<&Self, IdentError> {
if let Err(err) = Self::check_bytes(bytes) {
return Err(err)
}
Ok(unsafe { Self::from_bytes_unchecked(bytes) })
}
pub const fn from_str(s: &str) -> Result<&Self, IdentError> {
Self::from_bytes(s.as_bytes())
}
pub const fn make(s: &str) -> &Self {
match Self::from_str(s) {
Ok(some) => some,
Err(_) => panic!("invalid storage identifier")
}
}
pub const unsafe fn from_bytes_unchecked(s: &[u8]) -> &Self {
unsafe { mem::transmute(s) }
}
pub fn from_box(bytes: Box<[u8]>) -> Result<Box<Self>, IdentError> {
Self::check_bytes(&bytes)?;
Ok(unsafe { Self::from_box_unchecked(bytes) })
}
pub const unsafe fn from_box_unchecked(s: Box<[u8]>) -> Box<Self> {
unsafe { mem::transmute(s) }
}
pub fn boxed_from_string(s: String) -> Result<Box<Self>, IdentError> {
Self::check_bytes(s.as_bytes())?;
Ok(unsafe { Self::boxed_from_string_unchecked(s) })
}
pub unsafe fn boxed_from_string_unchecked(s: String) -> Box<Self> {
unsafe { Self::from_box_unchecked(s.into_boxed_str().into()) }
}
const fn check_bytes(mut bytes: &[u8]) -> Result<(), IdentError> {
let Some(first) = bytes.first() else {
return Err(IdentError(IdentErrorEnum::Empty))
};
if *first == b'.' {
return Err(IdentError(IdentErrorEnum::LeadingDot))
}
while let Some((head, tail)) = bytes.split_first() {
if !head.is_ascii_alphanumeric()
&& *head != b'+' && *head != b'-'
&& *head != b'_' && *head != b'.'
{
return Err(
IdentError(IdentErrorEnum::IllegalCharacter(*head))
)
}
bytes = tail;
}
Ok(())
}
pub fn builder(start: impl Into<Box<Ident>>) -> IdentBuilder {
IdentBuilder::new(start)
}
}
impl Ident {
pub const fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub const fn as_str(&self) -> &str {
&self.0
}
pub fn to_boxed(&self) -> Box<Self> {
unsafe { Ident::from_box_unchecked(Box::from(self.as_bytes())) }
}
}
impl Ident {
pub fn _from_parts<const N: usize>(
parts: [&Self; N], extension: Option<&Self>
) -> Box<Ident> {
const { assert!(N > 0) };
let mut s: String = parts.into_iter().map(Ident::as_str).collect();
if let Some(extension) = extension {
s.push('.');
s.push_str(extension.as_str())
}
unsafe { Ident::boxed_from_string_unchecked(s) }
}
}
impl Ident {
pub fn from_handle<T>(src: &Handle<T>) -> Cow<'_, Self> {
let res = if src.as_str().contains(['/', '\\']) {
Cow::Owned(unsafe {
Ident::boxed_from_string_unchecked(
src.as_str().replace(['/', '\\'], "+")
)
})
}
else {
Cow::Borrowed(unsafe {
Ident::from_bytes_unchecked(src.as_ref())
})
};
debug_assert!(Ident::check_bytes(res.as_bytes()).is_ok());
res
}
pub fn to_handle<T>(&self) -> Option<Handle<T>> {
Handle::from_str(self.as_str()).ok()
}
pub fn from_key_identifier(src: KeyIdentifier) -> Box<Ident> {
unsafe {
Ident::boxed_from_string_unchecked(format!("{src}"))
}
}
pub fn from_u64(src: u64) -> Box<Ident> {
unsafe {
Ident::boxed_from_string_unchecked(src.to_string())
}
}
pub fn from_i64(src: i64) -> Box<Ident> {
unsafe {
Ident::boxed_from_string_unchecked(src.to_string())
}
}
pub fn from_str_or_replace(src: &str) -> Cow<'_, Self> {
if src.is_empty() {
return Cow::Borrowed(const { Ident::make("_") })
}
if !src.starts_with('_') && let Ok(ident) = Self::from_str(src) {
return Cow::Borrowed(ident)
}
let mut res = Vec::with_capacity(src.len() + 1);
res.push(b'_');
for ch in src.as_bytes() {
res.extend_from_slice(&rpki::util::hex::encode_u8(*ch));
}
Cow::Owned(unsafe {
Ident::from_box_unchecked(
res.into_boxed_slice()
)
})
}
}
impl Clone for Box<Ident> {
fn clone(&self) -> Self {
unsafe {
Ident::from_box_unchecked(Box::from(self.as_bytes()))
}
}
}
impl<'a> From<&'a Ident> for Box<Ident> {
fn from(src: &'a Ident) -> Self {
unsafe {
Ident::from_box_unchecked(Box::from(src.as_bytes()))
}
}
}
impl From<Box<Ident>> for Box<str> {
fn from(src: Box<Ident>) -> Self {
unsafe { mem::transmute(src) }
}
}
impl AsRef<[u8]> for Ident {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl AsRef<str> for Ident {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl ToOwned for Ident {
type Owned = Box<Ident>;
fn to_owned(&self) -> Self::Owned {
self.to_boxed()
}
}
impl fmt::Display for Ident {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Clone, Debug)]
pub struct IdentBuilder {
content: String,
}
impl IdentBuilder {
pub fn new(start: impl Into<Box<Ident>>) -> Self {
IdentBuilder {
content: Box::<str>::from(start.into()).into()
}
}
pub fn push_ident(mut self, ident: &Ident) -> Self {
self.content.push_str(ident.as_str());
self
}
pub fn push_dot(mut self) -> Self {
self.content.push('.');
self
}
pub fn push_handle<T>(mut self, src: &Handle<T>) -> Self {
let mut parts = src.as_str().split(['/', '\\']);
match parts.next() {
Some(part) => self.content.push_str(part),
None => return self,
}
for part in parts {
self.content.push('+');
self.content.push_str(part);
}
self
}
pub fn push_key_identifier(mut self, key: KeyIdentifier) -> Self {
write!(self.content, "{key}").expect("format to string failed");
self
}
pub fn push_u64(mut self, value: u64) -> Self {
write!(self.content, "{value}").expect("format to string failed");
self
}
pub fn push_i64(mut self, value: i64) -> Self {
write!(self.content, "{value}").expect("format to string failed");
self
}
pub fn push_converted_str(mut self, s: &str) -> Self {
if s.is_empty() {
return self
}
if !s.starts_with("+") && Ident::check_bytes(s.as_bytes()).is_ok() {
self.content.push_str(s);
return self
}
self.content.push('+');
for ch in s.as_bytes() {
let hex = rpki::util::hex::encode_u8(*ch);
self.content.push_str(
unsafe { std::str::from_utf8_unchecked(&hex) }
);
}
self
}
pub fn finish_with_extension(self, ident: &Ident) -> Box<Ident> {
self.push_dot().push_ident(ident).finish()
}
pub fn finish(self) -> Box<Ident> {
unsafe { Ident::boxed_from_string_unchecked(self.content) }
}
}
#[derive(Clone, Copy, Debug)]
pub struct IdentError(IdentErrorEnum);
#[derive(Clone, Copy, Debug)]
enum IdentErrorEnum {
Empty,
LeadingDot,
IllegalCharacter(u8)
}
impl fmt::Display for IdentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::IdentErrorEnum::*;
match self.0 {
Empty => f.write_str("empty storage identifier"),
LeadingDot => f.write_str("leading period"),
IllegalCharacter(n) => {
match char::from_u32(n.into()) {
Some(ch) => {
write!(f,
"storage identifier with illegal character '{ch}'"
)
},
None => {
write!(f,
"storage identifier with illegal character \
0x{n:02x}'"
)
}
}
}
}
}
}
impl error::Error for IdentError { }