Unverified Commit 0ea576fc authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Endpoints 2.0 Standard Library functions (#1667)

* Endpoints 2.0 Standard Library functions

* Endpoints Standard Library Cleanups
parent 63afb410
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ repository = "https://github.com/awslabs/smithy-rs"
"pin-project-lite" = "0.2"
"tower" = { version = "0.4.11", default_features = false }
"async-trait" = "0.1"
"url" = "2.2.2"

[dev-dependencies]
proptest = "1"
+10 −0
Original line number Diff line number Diff line
/*
 *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *  SPDX-License-Identifier: Apache-2.0
 */

mod arn;
mod diagnostic;
mod host;
mod parse_url;
mod substring;
+153 −0
Original line number Diff line number Diff line
/*
 *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *  SPDX-License-Identifier: Apache-2.0
 */

use crate::endpoint_lib::diagnostic::DiagnosticCollector;
use std::borrow::Cow;
use std::error::Error;
use std::fmt::{Display, Formatter};

#[derive(Debug, Eq, PartialEq)]
pub(crate) struct Arn<'a> {
    partition: &'a str,
    service: &'a str,
    region: &'a str,
    account_id: &'a str,
    resource_id: Vec<&'a str>,
}

#[allow(unused)]
impl<'a> Arn<'a> {
    pub(crate) fn partition(&self) -> &'a str {
        self.partition
    }
    pub(crate) fn service(&self) -> &'a str {
        self.service
    }
    pub(crate) fn region(&self) -> &'a str {
        self.region
    }
    pub(crate) fn account_id(&self) -> &'a str {
        self.account_id
    }
    pub(crate) fn resource_id(&self) -> &Vec<&'a str> {
        &self.resource_id
    }
}

#[derive(Debug, PartialEq)]
pub(crate) struct InvalidArn {
    message: Cow<'static, str>,
}

impl InvalidArn {
    fn from_static(message: &'static str) -> InvalidArn {
        Self {
            message: Cow::Borrowed(message),
        }
    }
}
impl Display for InvalidArn {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message)
    }
}
impl Error for InvalidArn {}

impl<'a> Arn<'a> {
    pub(crate) fn parse(arn: &'a str) -> Result<Self, InvalidArn> {
        let mut split = arn.splitn(6, ':');
        let invalid_format =
            || InvalidArn::from_static("ARN must have 6 components delimited by `:`");
        let arn = split.next().ok_or_else(invalid_format)?;
        let partition = split.next().ok_or_else(invalid_format)?;
        let service = split.next().ok_or_else(invalid_format)?;
        let region = split.next().ok_or_else(invalid_format)?;
        let account_id = split.next().ok_or_else(invalid_format)?;
        let resource_id = split.next().ok_or_else(invalid_format)?;

        if arn != "arn" {
            return Err(InvalidArn::from_static(
                "first component of the ARN must be `arn`",
            ));
        }
        if partition.is_empty() || service.is_empty() || resource_id.is_empty() {
            return Err(InvalidArn::from_static(
                "partition, service, and resource id must all be non-empty",
            ));
        }

        let resource_id = resource_id.split([':', '/']).collect::<Vec<_>>();
        Ok(Self {
            partition,
            service,
            region,
            account_id,
            resource_id,
        })
    }
}

pub(crate) fn parse_arn<'a, 'b>(input: &'a str, e: &'b mut DiagnosticCollector) -> Option<Arn<'a>> {
    e.capture(Arn::parse(input))
}

#[cfg(test)]
mod test {
    use super::Arn;
    use crate::endpoint_lib::diagnostic::DiagnosticCollector;

    #[test]
    fn arn_parser() {
        let arn = "arn:aws:s3:us-east-2:012345678:outpost:op-1234";
        let parsed = Arn::parse(arn).expect("valid ARN");
        assert_eq!(
            parsed,
            Arn {
                partition: "aws",
                service: "s3",
                region: "us-east-2",
                account_id: "012345678",
                resource_id: vec!["outpost", "op-1234"]
            }
        );
    }

    #[test]
    fn allow_slash_arns() {
        let arn = "arn:aws:s3:us-east-2:012345678:outpost/op-1234";
        let parsed = Arn::parse(arn).expect("valid ARN");
        assert_eq!(
            parsed,
            Arn {
                partition: "aws",
                service: "s3",
                region: "us-east-2",
                account_id: "012345678",
                resource_id: vec!["outpost", "op-1234"]
            }
        );
    }

    #[test]
    fn resource_id_must_be_nonempty() {
        let arn = "arn:aws:s3:us-east-2:012345678:";
        Arn::parse(arn).expect_err("empty resource");
    }

    #[test]
    fn arns_with_empty_parts() {
        let arn = "arn:aws:s3:::my_corporate_bucket/Development/*";
        assert_eq!(
            Arn::parse(arn).expect("valid arn"),
            Arn {
                partition: "aws",
                service: "s3",
                region: "",
                account_id: "",
                resource_id: vec!["my_corporate_bucket", "Development", "*"]
            }
        );
    }
}
+45 −0
Original line number Diff line number Diff line
/*
 *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *  SPDX-License-Identifier: Apache-2.0
 */

use std::error::Error;

/// Diagnostic collector for endpoint resolution
///
/// Endpoint functions return `Option<T>`—to enable diagnostic information to flow, we capture the
/// last error that occurred.
#[derive(Debug, Default)]
pub(crate) struct DiagnosticCollector {
    last_error: Option<Box<dyn Error + Send + Sync>>,
}

impl DiagnosticCollector {
    /// Report an error to the collector
    pub(crate) fn report_error(&mut self, err: impl Into<Box<dyn Error + Send + Sync>>) {
        self.last_error = Some(err.into());
    }

    /// Capture a result, returning Some(t) when the input was `Ok` and `None` otherwise
    pub(crate) fn capture<T, E: Into<Box<dyn Error + Send + Sync>>>(
        &mut self,
        err: Result<T, E>,
    ) -> Option<T> {
        match err {
            Ok(res) => Some(res),
            Err(e) => {
                self.report_error(e);
                None
            }
        }
    }

    pub(crate) fn take_last_error(&mut self) -> Option<Box<dyn Error + Send + Sync>> {
        self.last_error.take()
    }

    /// Create a new diagnostic collector
    pub(crate) fn new() -> Self {
        Self { last_error: None }
    }
}
+76 −0
Original line number Diff line number Diff line
/*
 *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *  SPDX-License-Identifier: Apache-2.0
 */

use crate::endpoint_lib::diagnostic::DiagnosticCollector;

pub(crate) fn is_valid_host_label(
    label: &str,
    allow_dots: bool,
    e: &mut DiagnosticCollector,
) -> bool {
    if allow_dots {
        for part in label.split('.') {
            if !is_valid_host_label(part, false, e) {
                return false;
            }
        }
        true
    } else {
        if label.is_empty() || label.len() > 63 {
            e.report_error("host was too short or too long");
            return false;
        }
        label.chars().enumerate().all(|(idx, ch)| match (ch, idx) {
            ('-', 0) => {
                e.report_error("cannot start with `-`");
                false
            }
            _ => ch.is_alphanumeric() || ch == '-',
        })
    }
}

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

    fn is_valid_host_label(label: &str, allow_dots: bool) -> bool {
        super::is_valid_host_label(label, allow_dots, &mut DiagnosticCollector::new())
    }

    #[test]
    fn basic_cases() {
        assert_eq!(is_valid_host_label("", false), false);
        assert_eq!(is_valid_host_label("", true), false);
        assert_eq!(is_valid_host_label(".", true), false);
        assert_eq!(is_valid_host_label("a.b", true), true);
        assert_eq!(is_valid_host_label("a.b", false), false);
        assert_eq!(is_valid_host_label("a.b.", true), false);
        assert_eq!(is_valid_host_label("a.b.c", true), true);
        assert_eq!(is_valid_host_label("a_b", true), false);
        assert_eq!(is_valid_host_label(&"a".repeat(64), false), false);
        assert_eq!(
            is_valid_host_label(&format!("{}.{}", "a".repeat(63), "a".repeat(63)), true),
            true
        );
    }

    #[test]
    fn start_bounds() {
        assert_eq!(is_valid_host_label("-foo", false), false);
        assert_eq!(is_valid_host_label("-foo", true), false);
        assert_eq!(is_valid_host_label(".foo", true), false);
        assert_eq!(is_valid_host_label("a-b.foo", true), true);
    }

    use crate::endpoint_lib::diagnostic::DiagnosticCollector;
    use proptest::prelude::*;
    proptest! {
        #[test]
        fn no_panics(s in any::<String>(), dots in any::<bool>()) {
            is_valid_host_label(&s, dots);
        }
    }
}
Loading