#![warn(
missing_debug_implementations,
missing_docs,
unused_extern_crates,
rust_2018_idioms
)]
#[macro_use]
extern crate lazy_static;
#[cfg(serde_support)]
use serde::{Deserialize, Serialize};
use regex::Regex;
use std::fmt::{Debug, Display, Error, Formatter};
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(serde_support, derive(Deserialize, Serialize))]
pub enum Resource {
Any,
Id(String),
Path(String),
TypedId {
the_type: String,
id: String,
},
QTypedId {
the_type: String,
id: String,
qualifier: String,
},
}
#[derive(Debug, Clone)]
#[cfg_attr(serde_support, derive(Deserialize, Serialize))]
pub struct ARN {
pub partition: Option<String>,
pub service: String,
pub region: Option<String>,
pub account_id: Option<String>,
pub resource: Resource,
}
#[derive(Debug, PartialEq)]
pub enum ArnError {
TooFewComponents,
MissingPrefix,
MissingPartition,
InvalidPartition,
MissingService,
InvalidService,
MissingRegion,
InvalidRegion,
RegionWildcardNotAllowed,
MissingAccountId,
InvalidAccountId,
AccountIdWildcardNotAllowed,
MissingResource,
InvalidResource,
ResourceWildcardNotAllowed,
}
const ARN_PREFIX: &str = "arn";
const ARN_SEPARATOR: char = ':';
const ARN_SEPARATOR_STR: &str = ":";
const DEFAULT_PARTITION: &str = "aws";
const PATH_SEPARATOR: char = '/';
const WILD: &str = "*";
lazy_static! {
static ref PARTITION: Regex = Regex::new(r"^aws(\-[a-zA-Z][a-zA-Z0-9\-]+)?$").unwrap();
static ref IDENTIFIER: Regex = Regex::new(r"^[a-zA-Z][a-zA-Z0-9\-]+$").unwrap();
}
impl ARN {
pub fn validate(&self) -> Result<(), ArnError> {
if let Some(partition) = &self.partition {
if !PARTITION.is_match(&partition) {
return Err(ArnError::InvalidPartition);
}
}
if !IDENTIFIER.is_match(&self.service) {
return Err(ArnError::InvalidService);
}
if let Some(region) = &self.region {
if !IDENTIFIER.is_match(region) {
return Err(ArnError::InvalidRegion);
}
}
if let Some(account_id) = &self.account_id {
if account_id.len() != 12 || !account_id.chars().all(|c| c.is_ascii_digit()) {
return Err(ArnError::InvalidAccountId);
}
}
self.resource.validate()?;
if validate::is_registered(&self.service, &self.resource) {
validate::validate(self)?
}
Ok(())
}
}
impl Display for ARN {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
write!(
f,
"{}",
vec![
ARN_PREFIX.to_string(),
self.partition
.as_ref()
.unwrap_or(&DEFAULT_PARTITION.to_string())
.clone(),
self.service.clone(),
self.region.as_ref().unwrap_or(&String::new()).clone(),
self.account_id.as_ref().unwrap_or(&String::new()).clone(),
self.resource.clone().to_string()
]
.join(ARN_SEPARATOR_STR)
)
}
}
impl FromStr for ARN {
type Err = ArnError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts: Vec<&str> = s.split(ARN_SEPARATOR).collect();
if parts.len() < 6 {
Err(ArnError::TooFewComponents)
} else if parts[0] != ARN_PREFIX {
Err(ArnError::MissingPrefix)
} else {
let new_arn = ARN {
partition: if parts[1].is_empty() {
None
} else {
Some(parts[1].to_string())
},
service: parts[2].to_string(),
region: if parts[3].is_empty() {
None
} else {
Some(parts[3].to_string())
},
account_id: if parts[4].is_empty() {
None
} else {
Some(parts[4].to_string())
},
resource: {
let resource_parts: Vec<&str> = parts.drain(5..).collect();
Resource::from_str(&resource_parts.join(ARN_SEPARATOR_STR))?
},
};
new_arn.validate().map(|_| new_arn)
}
}
}
impl Resource {
pub fn validate(&self) -> Result<(), ArnError> {
match self {
Resource::Id(id) => must_not_contain(id, &[':', '/', '*']),
Resource::Path(path) => must_not_contain(path, &[':']),
Resource::TypedId { the_type, id } => must_not_contain(the_type, &[':', '/', '*'])
.and_then(|_| must_not_contain(id, &[':', '/'])),
Resource::QTypedId {
the_type,
id,
qualifier,
} => must_not_contain(the_type, &[':', '/', '*']).and_then(|_| {
must_not_contain(id, &[':', '/'])
.and_then(|_| must_not_contain(qualifier, &[':', '/', '*']))
}),
_ => Ok(()),
}
}
}
fn must_not_contain(s: &String, chars: &[char]) -> Result<(), ArnError> {
if s.contains(chars) {
Err(ArnError::InvalidResource)
} else {
Ok(())
}
}
impl Display for Resource {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
match self {
Resource::Any => write!(f, "{}", WILD),
Resource::Id(id) => write!(f, "{}", id),
Resource::Path(path) => write!(f, "{}", path),
Resource::TypedId { the_type, id } => write!(f, "{}{}{}", the_type, ARN_SEPARATOR, id),
Resource::QTypedId {
the_type,
id,
qualifier,
} => write!(
f,
"{}{}{}{}{}",
the_type, ARN_SEPARATOR, id, ARN_SEPARATOR, qualifier
),
}
}
}
impl FromStr for Resource {
type Err = ArnError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
Err(ArnError::MissingResource)
} else if s == WILD {
Ok(Resource::Any)
} else if s.contains(ARN_SEPARATOR) {
let parts: Vec<&str> = s.split(ARN_SEPARATOR).collect();
if parts.len() == 2 {
Ok(Resource::TypedId {
the_type: parts[0].to_string(),
id: parts[1].to_string(),
})
} else if parts.len() == 3 {
Ok(Resource::QTypedId {
the_type: parts[0].to_string(),
id: parts[1].to_string(),
qualifier: parts[2].to_string(),
})
} else {
Err(ArnError::InvalidResource)
}
} else if s.contains(PATH_SEPARATOR) {
Ok(Resource::Path(s.to_string()))
} else {
Ok(Resource::Id(s.to_string()))
}
}
}
pub mod builder;
#[cfg(feature = "ext_validation")]
mod validate;
#[cfg(not(feature = "ext_validation"))]
mod validate {
use crate::{ArnError, Resource, ARN};
pub fn is_registered(_service: &str, _resource: &Resource) -> bool {
false
}
pub fn validate(_arn: &ARN) -> Result<(), ArnError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_to_string() {
assert_eq!(Resource::Id("thing".to_string()).to_string(), "thing");
assert_eq!(
Resource::Path("mythings/athing".to_string()).to_string(),
"mythings/athing"
);
assert_eq!(
Resource::TypedId {
the_type: "things".to_string(),
id: "athing".to_string()
}
.to_string(),
"things:athing"
);
assert_eq!(
Resource::QTypedId {
the_type: "things".to_string(),
id: "athing".to_string(),
qualifier: "v2".to_string()
}
.to_string(),
"things:athing:v2"
);
}
#[test]
fn test_resource_from_str() {
assert_eq!(Resource::from_str(WILD), Ok(Resource::Any));
assert_eq!(
Resource::from_str("mythings/athing"),
Ok(Resource::Path("mythings/athing".to_string()))
);
assert_eq!(
Resource::from_str("things:athing"),
Ok(Resource::TypedId {
the_type: "things".to_string(),
id: "athing".to_string()
})
);
assert_eq!(
Resource::from_str("things:athing:v2"),
Ok(Resource::QTypedId {
the_type: "things".to_string(),
id: "athing".to_string(),
qualifier: "v2".to_string()
})
);
}
#[test]
fn test_arn_to_string() {
let arn = ARN {
partition: None,
service: "s3".to_string(),
region: None,
account_id: None,
resource: Resource::Path("mythings/athing".to_string()),
};
assert_eq!(arn.to_string(), "arn:aws:s3:::mythings/athing");
}
#[test]
fn test_arn_from_str() {
let arn_str = "arn:aws:s3:us-east-1:123456789012:job/23476";
let arn: ARN = arn_str.parse().unwrap();
assert_eq!(arn.partition, Some("aws".to_string()));
assert_eq!(arn.service, "s3".to_string());
assert_eq!(arn.region, Some("us-east-1".to_string()));
assert_eq!(arn.account_id, Some("123456789012".to_string()));
}
}