use crate::spiffeid::charset::is_backcompat_trust_domain_char;
use crate::spiffeid::path::{format_path, join_path_segments, validate_path};
use crate::spiffeid::{Error, Result, TrustDomain};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use url::Url;
const SCHEME_PREFIX: &str = "spiffe://";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ID {
id: String,
path_idx: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpiffeUrl {
scheme: String,
host: String,
path: String,
}
impl SpiffeUrl {
pub fn new(scheme: &str, host: &str, path: &str) -> Self {
Self {
scheme: scheme.to_string(),
host: host.to_string(),
path: path.to_string(),
}
}
pub fn empty() -> Self {
Self {
scheme: String::new(),
host: String::new(),
path: String::new(),
}
}
pub fn scheme(&self) -> &str {
&self.scheme
}
pub fn host(&self) -> &str {
&self.host
}
pub fn path(&self) -> &str {
&self.path
}
pub fn is_empty(&self) -> bool {
self.scheme.is_empty() && self.host.is_empty() && self.path.is_empty()
}
pub fn as_url(&self) -> Option<Url> {
if self.is_empty() {
return None;
}
Url::parse(&self.to_string()).ok()
}
}
impl std::fmt::Display for SpiffeUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_empty() {
return Ok(());
}
write!(f, "{}://{}{}", self.scheme, self.host, self.path)
}
}
impl ID {
pub fn from_path(td: TrustDomain, path: &str) -> Result<ID> {
validate_path(path)?;
make_id(&td, path)
}
pub fn from_pathf(td: TrustDomain, args: std::fmt::Arguments<'_>) -> Result<ID> {
let path = format_path(args)?;
make_id(&td, &path)
}
pub fn from_segments(td: TrustDomain, segments: &[&str]) -> Result<ID> {
let path = join_path_segments(segments)?;
make_id(&td, &path)
}
pub fn from_string(id: &str) -> Result<ID> {
if id.is_empty() {
return Err(Error::Empty);
}
if !id.starts_with(SCHEME_PREFIX) {
return Err(Error::WrongScheme);
}
let mut path_idx = SCHEME_PREFIX.len();
let bytes = id.as_bytes();
while path_idx < bytes.len() {
let c = bytes[path_idx];
if c == b'/' {
break;
}
if !is_valid_trust_domain_char(c) {
return Err(Error::BadTrustDomainChar);
}
path_idx += 1;
}
if path_idx == SCHEME_PREFIX.len() {
return Err(Error::MissingTrustDomain);
}
validate_path(&id[path_idx..])?;
Ok(ID {
id: id.to_string(),
path_idx,
})
}
pub fn from_stringf(args: std::fmt::Arguments<'_>) -> Result<ID> {
ID::from_string(&format!("{}", args))
}
pub fn from_uri(uri: &Url) -> Result<ID> {
ID::from_string(uri.as_str())
}
pub fn trust_domain(&self) -> TrustDomain {
if self.is_zero() {
return TrustDomain { name: String::new() };
}
TrustDomain {
name: self.id[SCHEME_PREFIX.len()..self.path_idx].to_string(),
}
}
pub fn member_of(&self, td: &TrustDomain) -> bool {
self.trust_domain() == *td
}
pub fn path(&self) -> &str {
&self.id[self.path_idx..]
}
pub fn url(&self) -> SpiffeUrl {
if self.is_zero() {
return SpiffeUrl::empty();
}
SpiffeUrl::new("spiffe", self.trust_domain().name(), self.path())
}
pub fn is_zero(&self) -> bool {
self.id.is_empty()
}
pub fn append_path(&self, path: &str) -> Result<ID> {
if self.is_zero() {
return Err(Error::Other(
"cannot append path on a zero ID value".to_string(),
));
}
validate_path(path)?;
let mut id = self.clone();
id.id.push_str(path);
Ok(id)
}
pub fn append_pathf(&self, args: std::fmt::Arguments<'_>) -> Result<ID> {
if self.is_zero() {
return Err(Error::Other(
"cannot append path on a zero ID value".to_string(),
));
}
let path = format_path(args)?;
let mut id = self.clone();
id.id.push_str(&path);
Ok(id)
}
pub fn append_segments(&self, segments: &[&str]) -> Result<ID> {
if self.is_zero() {
return Err(Error::Other(
"cannot append path segments on a zero ID value".to_string(),
));
}
let path = join_path_segments(segments)?;
let mut id = self.clone();
id.id.push_str(&path);
Ok(id)
}
pub fn replace_path(&self, path: &str) -> Result<ID> {
if self.is_zero() {
return Err(Error::Other(
"cannot replace path on a zero ID value".to_string(),
));
}
ID::from_path(self.trust_domain(), path)
}
pub fn replace_pathf(&self, args: std::fmt::Arguments<'_>) -> Result<ID> {
if self.is_zero() {
return Err(Error::Other(
"cannot replace path on a zero ID value".to_string(),
));
}
let path = format_path(args)?;
ID::from_path(self.trust_domain(), &path)
}
pub fn replace_segments(&self, segments: &[&str]) -> Result<ID> {
if self.is_zero() {
return Err(Error::Other(
"cannot replace path segments on a zero ID value".to_string(),
));
}
let path = join_path_segments(segments)?;
ID::from_path(self.trust_domain(), &path)
}
pub fn zero() -> ID {
ID {
id: String::new(),
path_idx: 0,
}
}
}
impl std::fmt::Display for ID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id.fmt(f)
}
}
impl Serialize for ID {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.is_zero() {
serializer.serialize_str("")
} else {
serializer.serialize_str(&self.id)
}
}
}
impl Default for ID {
fn default() -> Self {
ID::zero()
}
}
impl<'de> Deserialize<'de> for ID {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s.is_empty() {
Ok(ID::zero())
} else {
ID::from_string(&s).map_err(serde::de::Error::custom)
}
}
}
pub(crate) fn make_id(td: &TrustDomain, path: &str) -> Result<ID> {
if td.is_zero() {
return Err(Error::MissingTrustDomain);
}
let mut id = String::with_capacity(SCHEME_PREFIX.len() + td.name.len() + path.len());
id.push_str(SCHEME_PREFIX);
id.push_str(td.name());
let path_idx = id.len();
id.push_str(path);
Ok(ID { id, path_idx })
}
fn is_valid_trust_domain_char(c: u8) -> bool {
matches!(c, b'a'..=b'z')
|| matches!(c, b'0'..=b'9')
|| matches!(c, b'-' | b'.' | b'_')
|| is_backcompat_trust_domain_char(c)
}