zerokms-protocol 0.12.17

Library to manage the CipherStash ZeroKMS communication protocol
Documentation
use std::{
    fmt::{self, Display, Formatter},
    ops::Deref,
};

use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;

/// A UUID or textual name that can uniquely identify a resource. Whereas a UUID is a global identifier, `name` is not
/// implied to be *globally* unique, but unique within scope implied scope: e.g. a workspace.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Clone, ToSchema)]
pub enum IdentifiedBy {
    /// A UUID that uniquely identifies a resource.
    Uuid(Uuid),
    /// A name that uniquely identifies a resource within an implied scope, e.g. a workspace.
    Name(Name),
}

impl<'de> Deserialize<'de> for IdentifiedBy {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        // This is the same as `IdentifiedBy` but is required to avoid a recursive Deserialize stack overflow.
        #[derive(Deserialize)]
        enum Stub {
            Uuid(Uuid),
            Name(Name),
        }

        // This representation is how backwards compatibility is dealt with on the wire.
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum Compat {
            Uuid(Uuid),         // Old style (UUID only)
            IdentifiedBy(Stub), // New style (UUID or Name)
        }

        if let Ok(id_compat) = <Compat as Deserialize>::deserialize(deserializer) {
            match id_compat {
                Compat::Uuid(uuid) => Ok(IdentifiedBy::Uuid(uuid)),
                Compat::IdentifiedBy(stub) => match stub {
                    Stub::Uuid(uuid) => Ok(IdentifiedBy::Uuid(uuid)),
                    Stub::Name(name) => Ok(IdentifiedBy::Name(name)),
                },
            }
        } else {
            Err(serde::de::Error::custom(
                "expected one of: a UUID, IdentifiedBy::Uuid or IdentifiedBy::Name",
            ))
        }
    }
}

impl From<Uuid> for IdentifiedBy {
    fn from(value: Uuid) -> Self {
        Self::Uuid(value)
    }
}

impl From<Name> for IdentifiedBy {
    fn from(value: Name) -> Self {
        Self::Name(value)
    }
}

impl From<String> for Name {
    fn from(value: String) -> Self {
        Self { inner: value }
    }
}

impl Display for IdentifiedBy {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            IdentifiedBy::Uuid(uuid) => Display::fmt(uuid, f),
            IdentifiedBy::Name(name) => Display::fmt(name, f),
        }
    }
}

/// The unique name of a resource (within some scope: e.g. a workspace).
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
#[serde(transparent)]
pub struct Name {
    inner: String,
}

// `Name` serializes transparently as its inner string (`#[serde(transparent)]`), so its OpenAPI
// schema is a plain string. A manual impl avoids the `{ inner }` object utoipa's derive produces
// for a named-field newtype.
impl utoipa::PartialSchema for Name {
    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        String::schema()
    }
}

impl utoipa::ToSchema for Name {}

impl Name {
    /// Crates a new `Name` from an already persisted name. Does not run validations.
    pub fn new_untrusted(name: &str) -> Self {
        Self {
            inner: name.to_owned(),
        }
    }
}

impl Deref for Name {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

const NAME_MAX_LEN: usize = 64;

pub struct InvalidNameError(pub String);

impl TryFrom<&str> for Name {
    type Error = InvalidNameError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        if !value.len() <= NAME_MAX_LEN {
            return Err(InvalidNameError(format!(
                "name length must be <= 64, got {}",
                value.len()
            )));
        }

        if value.is_empty() {
            return Err(InvalidNameError(
                "name must not be an empty string".to_string(),
            ));
        }

        if !value
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '/')
        {
            return Err(InvalidNameError(
                "name must consist of only these allowed characters: A-Z a-z 0-9 _ - /".to_string(),
            ));
        }

        Ok(Name {
            inner: value.to_string(),
        })
    }
}

impl Display for Name {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        Display::fmt(&self.inner, f)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn name_validation() {
        assert!(Name::try_from("alias").is_ok());
        assert!(Name::try_from("foo/bar").is_ok());
        assert!(Name::try_from(
            "abcdefghijklmnopqrstuvqwxyzABCDEFGHIJKLMNOPQRSTUVQWXYZ0123456789-_/"
        )
        .is_ok());
        assert!(Name::try_from(" leading-ws").is_err());
        assert!(Name::try_from("trailing-ws ").is_err());
        assert!(Name::try_from("punctuation%").is_err());
    }
}