zerokms-protocol 0.12.9

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

use serde::{Deserialize, Serialize};
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)]
pub enum IdentifiedBy {
    // A UUID that uniquely identifies a resource.
    Uuid(Uuid),
    // A name that uniquely identifies a resource when used in combination with 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,
}

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());
    }
}