fastly 0.7.3

Fastly Compute@Edge API
Documentation
//! Backend server.

use std::str::FromStr;

/// The maximum length in characters of a backend name.
const MAX_BACKEND_NAME_LEN: usize = 255;

/// A named backend.
///
/// This represents a backend associated with a service that we can send requests to, potentially
/// caching the responses received.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Backend {
    name: String,
}

impl Backend {
    /// Get a backend by its name.
    ///
    /// This function will return a [`BackendError`] if an invalid name was given.
    ///
    /// Backend names:
    ///   * cannot be empty
    ///   * cannot be longer than 255 characters
    ///   * cannot ASCII control characters such as `'\n'` or `DELETE`.
    ///   * cannot contain special Unicode characters
    ///   * should only contain visible ASCII characters or spaces
    ///
    /// Future versions of this function may return an error if your service does not have a backend
    /// with this name.
    pub fn from_name(s: &str) -> Result<Self, BackendError> {
        s.parse()
    }

    /// Get the name of this backend.
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// Turn the backend into its name as a string.
    pub fn into_string(self) -> String {
        self.name
    }
}

/// [`Backend`]-related errors.
#[derive(Copy, Clone, Debug, thiserror::Error, PartialEq, Eq)]
pub enum BackendError {
    /// The backend name was empty.
    #[error("an empty string is not a valid backend")]
    EmptyName,
    /// The backend name was too long.
    #[error("backend names must be <= 255 characters")]
    TooLong,
    /// The backend name contained invalid characters.
    #[error("backend names must only contain visible ASCII characters or spaces")]
    InvalidName,
}

impl FromStr for Backend {
    type Err = BackendError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        validate_backend(s)?;
        Ok(Self { name: s.to_owned() })
    }
}

impl std::fmt::Display for Backend {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.name.as_str())
    }
}

/// Validate that a string looks like an acceptable [`Backend`] value.
///
/// Note that this is *not* meant to be a filter for things that could cause security issues, it is
/// only meant to catch errors before the hostcalls do in order to yield friendlier error messages.
///
/// This function will return a [`BackendError`] if an invalid name was given.
///
/// Backend names:
///   * cannot be empty
///   * cannot be longer than 255 characters
///   * cannot ASCII control characters such as `'\n'` or `DELETE`.
///   * cannot contain special Unicode characters
///   * should only contain visible ASCII characters or spaces
//
// TODO KTM 2020-03-10: We should not allow VCL keywords like `if`, `now`, `true`, `urldecode`, and
// so on. Once we have better errors, let's make sure that these are caught in a pleasant manner.
pub fn validate_backend(backend: &str) -> Result<(), BackendError> {
    if backend.is_empty() {
        Err(BackendError::EmptyName)
    } else if backend.len() > MAX_BACKEND_NAME_LEN {
        Err(BackendError::TooLong)
    } else if backend.chars().any(is_invalid_char) {
        Err(BackendError::InvalidName)
    } else {
        Ok(())
    }
}

/// Return true if a character is not allowed in a [`Backend`] name.
///
/// This is used to enforce the rules described in the documentation of [`Backend::from_name()`]. A
/// backend name should only contain visible ASCII characters, or spaces.
#[inline]
fn is_invalid_char(c: char) -> bool {
    c != ' ' && !c.is_ascii_graphic()
}

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

    #[test]
    fn valid_backend_names_are_accepted() {
        let valid_backend_names = [
            "valid_backend_1",
            "1_backend_with_leading_integer",
            "backend-with-kebab-case",
            "backend with spaces",
            "backend.with.periods",
            "123_456_789_000",
            "123.456.789.000",
            "tilde~backend",
            "(parens-backend)",
        ];
        for backend in valid_backend_names.iter() {
            match validate_backend(backend) {
                Ok(_) => {}
                x => panic!(
                    "backend string \"{}\" yielded unexpected result: {:?}",
                    backend, x
                ),
            }
        }
    }

    #[test]
    fn empty_str_is_not_accepted() {
        let invalid_backend = "";
        match validate_backend(invalid_backend) {
            Err(BackendError::EmptyName) => {}
            x => panic!("unexpected result: {:?}", x),
        }
    }

    #[test]
    fn name_equal_to_character_limit_is_accepted() {
        use std::iter::FromIterator;
        let invalid_backend: String = String::from_iter(vec!['a'; 255]);
        match validate_backend(&invalid_backend) {
            Ok(_) => {}
            x => panic!("unexpected result: {:?}", x),
        }
    }

    #[test]
    fn name_longer_than_character_limit_are_not_accepted() {
        use std::iter::FromIterator;
        let invalid_backend: String = String::from_iter(vec!['a'; 256]);
        match validate_backend(&invalid_backend) {
            Err(BackendError::TooLong) => {}
            x => panic!("unexpected result: {:?}", x),
        }
    }

    #[test]
    fn unprintable_characters_are_not_accepted() {
        let invalid_backend = "\n";
        match validate_backend(invalid_backend) {
            Err(BackendError::InvalidName) => {}
            x => panic!("unexpected result: {:?}", x),
        }
    }

    #[test]
    fn unicode_is_not_accepted() {
        let invalid_backend = "";
        match validate_backend(invalid_backend) {
            Err(BackendError::InvalidName) => {}
            x => panic!("unexpected result: {:?}", x),
        }
    }
}