mod normalize;
mod parse;
mod source;
use crate::profile::parser::parse::parse_profile_file;
use crate::profile::parser::source::{FileKind, Source};
use aws_types::os_shim_internal::{Env, Fs};
use std::borrow::Cow;
use std::collections::HashMap;
pub use self::parse::ProfileParseError;
pub async fn load(fs: &Fs, env: &Env) -> Result<ProfileSet, ProfileParseError> {
let source = source::load(env, fs).await;
ProfileSet::parse(source)
}
#[derive(Debug, Eq, Clone, PartialEq)]
pub struct ProfileSet {
profiles: HashMap<String, Profile>,
selected_profile: Cow<'static, str>,
}
impl ProfileSet {
#[doc(hidden)]
pub fn new(
profiles: HashMap<String, HashMap<String, String>>,
selected_profile: impl Into<Cow<'static, str>>,
) -> Self {
let mut base = ProfileSet::empty();
base.selected_profile = selected_profile.into();
for (name, profile) in profiles {
base.profiles.insert(
name.clone(),
Profile::new(
name,
profile
.into_iter()
.map(|(k, v)| (k.clone(), Property::new(k, v)))
.collect(),
),
);
}
base
}
pub fn get(&self, key: &str) -> Option<&str> {
self.profiles
.get(self.selected_profile.as_ref())
.and_then(|profile| profile.get(key))
}
pub fn get_profile(&self, profile_name: &str) -> Option<&Profile> {
self.profiles.get(profile_name)
}
pub fn selected_profile(&self) -> &str {
self.selected_profile.as_ref()
}
pub fn is_empty(&self) -> bool {
self.profiles.is_empty()
}
fn parse(source: Source) -> Result<Self, ProfileParseError> {
let mut base = ProfileSet::empty();
base.selected_profile = source.profile;
normalize::merge_in(
&mut base,
parse_profile_file(&source.config_file)?,
FileKind::Config,
);
normalize::merge_in(
&mut base,
parse_profile_file(&source.credentials_file)?,
FileKind::Credentials,
);
Ok(base)
}
fn empty() -> Self {
Self {
profiles: Default::default(),
selected_profile: "default".into(),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Profile {
name: String,
properties: HashMap<String, Property>,
}
impl Profile {
pub fn new(name: String, properties: HashMap<String, Property>) -> Self {
Self { name, properties }
}
pub fn name(&self) -> &str {
&self.name
}
pub fn get(&self, name: &str) -> Option<&str> {
self.properties.get(name).map(|prop| prop.value())
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Property {
key: String,
value: String,
}
impl Property {
pub fn value(&self) -> &str {
&self.value
}
pub fn key(&self) -> &str {
&self.key
}
pub fn new(key: String, value: String) -> Self {
Property { key, value }
}
}
#[cfg(test)]
mod test {
use crate::profile::parser::source::{File, Source};
use crate::profile::ProfileSet;
use arbitrary::{Arbitrary, Unstructured};
use serde::Deserialize;
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use tracing_test::traced_test;
#[test]
#[traced_test]
fn run_tests() -> Result<(), Box<dyn Error>> {
let tests = fs::read_to_string("test-data/profile-parser-tests.json")?;
let tests: ParserTests = serde_json::from_str(&tests)?;
for (i, test) in tests.tests.into_iter().enumerate() {
eprintln!("test: {}", i);
check(test);
}
Ok(())
}
#[test]
fn empty_source_empty_profile() {
let source = Source {
config_file: File {
path: "~/.aws/config".to_string(),
contents: "".into(),
},
credentials_file: File {
path: "~/.aws/credentials".to_string(),
contents: "".into(),
},
profile: "default".into(),
};
let profile_set = ProfileSet::parse(source).expect("empty profiles are valid");
assert!(profile_set.is_empty());
}
#[test]
#[ignore]
fn run_fuzz_tests() -> Result<(), Box<dyn Error>> {
let fuzz_corpus = fs::read_dir("fuzz/corpus/profile-parser")?
.map(|res| res.map(|entry| entry.path()))
.collect::<Result<Vec<_>, _>>()?;
for file in fuzz_corpus {
let raw = fs::read(file)?;
let mut unstructured = Unstructured::new(&raw);
let (conf, creds): (Option<&str>, Option<&str>) =
Arbitrary::arbitrary(&mut unstructured)?;
let profile_source = Source {
config_file: File {
path: "~/.aws/config".to_string(),
contents: conf.unwrap_or_default().to_string(),
},
credentials_file: File {
path: "~/.aws/config".to_string(),
contents: creds.unwrap_or_default().to_string(),
},
profile: "default".into(),
};
let _ = ProfileSet::parse(profile_source);
}
Ok(())
}
fn flatten(profile: ProfileSet) -> HashMap<String, HashMap<String, String>> {
profile
.profiles
.into_iter()
.map(|(_name, profile)| {
(
profile.name,
profile
.properties
.into_iter()
.map(|(_, prop)| (prop.key, prop.value))
.collect(),
)
})
.collect()
}
fn make_source(input: ParserInput) -> Source {
Source {
config_file: File {
path: "~/.aws/config".to_string(),
contents: input.config_file.unwrap_or_default(),
},
credentials_file: File {
path: "~/.aws/credentials".to_string(),
contents: input.credentials_file.unwrap_or_default(),
},
profile: "default".into(),
}
}
fn check(test_case: ParserTest) {
let copy = test_case.clone();
let parsed = ProfileSet::parse(make_source(test_case.input));
let res = match (parsed.map(flatten), &test_case.output) {
(Ok(actual), ParserOutput::Profiles(expected)) if &actual != expected => Err(format!(
"mismatch:\nExpected: {:#?}\nActual: {:#?}",
expected, actual
)),
(Ok(_), ParserOutput::Profiles(_)) => Ok(()),
(Err(msg), ParserOutput::ErrorContaining(substr)) => {
if format!("{}", msg).contains(substr) {
Ok(())
} else {
Err(format!("Expected {} to contain {}", msg, substr))
}
}
(Ok(output), ParserOutput::ErrorContaining(err)) => Err(format!(
"expected an error: {} but parse succeeded:\n{:#?}",
err, output
)),
(Err(err), ParserOutput::Profiles(_expected)) => {
Err(format!("Expected to succeed but got: {}", err))
}
};
if let Err(e) = res {
eprintln!("Test case failed: {:#?}", copy);
eprintln!("failure: {}", e);
panic!("test failed")
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct ParserTests {
tests: Vec<ParserTest>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct ParserTest {
name: String,
input: ParserInput,
output: ParserOutput,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
enum ParserOutput {
Profiles(HashMap<String, HashMap<String, String>>),
ErrorContaining(String),
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct ParserInput {
config_file: Option<String>,
credentials_file: Option<String>,
}
}