use crate::fs_util::{home_dir, Os};
use aws_types::os_shim_internal;
use std::borrow::Cow;
use std::io::ErrorKind;
use std::path::{Component, Path, PathBuf};
use tracing::{warn, Instrument};
const HOME_EXPANSION_FAILURE_WARNING: &str =
"home directory expansion was requested (via `~` character) for the profile \
config file path, but no home directory could be determined";
pub(super) struct Source {
pub(super) config_file: File,
pub(super) credentials_file: File,
pub(super) profile: Cow<'static, str>,
}
pub(super) struct File {
pub(super) path: String,
pub(super) contents: String,
}
#[derive(Clone, Copy)]
pub(super) enum FileKind {
Config,
Credentials,
}
impl FileKind {
fn default_path(&self) -> &'static str {
match &self {
FileKind::Credentials => "~/.aws/credentials",
FileKind::Config => "~/.aws/config",
}
}
fn override_environment_variable(&self) -> &'static str {
match &self {
FileKind::Config => "AWS_CONFIG_FILE",
FileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE",
}
}
}
pub(super) async fn load(proc_env: &os_shim_internal::Env, fs: &os_shim_internal::Fs) -> Source {
let home = home_dir(proc_env, Os::real());
let config = load_config_file(FileKind::Config, &home, fs, proc_env)
.instrument(tracing::debug_span!("load_config_file"))
.await;
let credentials = load_config_file(FileKind::Credentials, &home, fs, proc_env)
.instrument(tracing::debug_span!("load_credentials_file"))
.await;
Source {
config_file: config,
credentials_file: credentials,
profile: proc_env
.get("AWS_PROFILE")
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed("default")),
}
}
async fn load_config_file(
kind: FileKind,
home_directory: &Option<String>,
fs: &os_shim_internal::Fs,
environment: &os_shim_internal::Env,
) -> File {
let (path_is_default, path) = environment
.get(kind.override_environment_variable())
.map(|p| (false, Cow::Owned(p)))
.ok()
.unwrap_or_else(|| (true, kind.default_path().into()));
let expanded = expand_home(path.as_ref(), path_is_default, home_directory);
if path != expanded.to_string_lossy() {
tracing::debug!(before = ?path, after = ?expanded, "home directory expanded");
}
let data = match fs.read_to_end(&expanded).await {
Ok(data) => data,
Err(e) => {
match e.kind() {
ErrorKind::NotFound if path == kind.default_path() => {
tracing::debug!(path = %path, "config file not found")
}
ErrorKind::NotFound if path != kind.default_path() => {
tracing::warn!(path = %path, env = %kind.override_environment_variable(), "config file overridden via environment variable not found")
}
_other => tracing::warn!(path = %path, error = %e, "failed to read config file"),
};
Default::default()
}
};
let data = match String::from_utf8(data) {
Ok(data) => data,
Err(e) => {
tracing::warn!(path = %path, error = %e, "config file did not contain utf-8 encoded data");
Default::default()
}
};
tracing::debug!(path = %path, size = ?data.len(), "config file loaded");
File {
path: expanded.to_string_lossy().into(),
contents: data,
}
}
fn expand_home(
path: impl AsRef<Path>,
path_is_default: bool,
home_dir: &Option<String>,
) -> PathBuf {
let path = path.as_ref();
let mut components = path.components();
let start = components.next();
match start {
None => path.into(), Some(Component::Normal(s)) if s == "~" => {
let path = match home_dir {
Some(dir) => {
tracing::debug!(home = ?dir, path = ?path, "performing home directory substitution");
dir.clone()
}
None => {
if !path_is_default {
warn!(HOME_EXPANSION_FAILURE_WARNING);
}
"~".into()
}
};
let mut path: PathBuf = path.into();
for component in components {
path.push(component);
}
path
}
_other => path.into(),
}
}
#[cfg(test)]
mod tests {
use crate::profile::parser::source::{
expand_home, load, load_config_file, FileKind, HOME_EXPANSION_FAILURE_WARNING,
};
use aws_types::os_shim_internal::{Env, Fs};
use futures_util::FutureExt;
use serde::Deserialize;
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use tracing_test::traced_test;
#[test]
fn only_expand_home_prefix() {
let path = "~aws/config";
assert_eq!(
expand_home(&path, false, &None).to_str().unwrap(),
"~aws/config"
);
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct SourceTests {
tests: Vec<TestCase>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct TestCase {
name: String,
environment: HashMap<String, String>,
platform: String,
profile: Option<String>,
config_location: String,
credentials_location: String,
}
#[test]
fn run_tests() -> Result<(), Box<dyn Error>> {
let tests = fs::read_to_string("test-data/file-location-tests.json")?;
let tests: SourceTests = serde_json::from_str(&tests)?;
for (i, test) in tests.tests.into_iter().enumerate() {
eprintln!("test: {}", i);
check(test)
.now_or_never()
.expect("these futures should never poll");
}
Ok(())
}
#[traced_test]
#[test]
fn logs_produced_default() {
let env = Env::from_slice(&[("HOME", "/user/name")]);
let mut fs = HashMap::new();
fs.insert(
"/user/name/.aws/config".to_string(),
"[default]\nregion = us-east-1",
);
let fs = Fs::from_map(fs);
let _src = load(&env, &fs).now_or_never();
assert!(logs_contain("config file loaded"));
assert!(logs_contain("performing home directory substitution"));
}
#[traced_test]
#[test]
fn load_config_file_should_not_emit_warning_when_path_not_explicitly_set() {
let env = Env::from_slice(&[]);
let fs = Fs::from_slice(&[]);
let _src = load_config_file(FileKind::Config, &None, &fs, &env).now_or_never();
assert!(!logs_contain(HOME_EXPANSION_FAILURE_WARNING));
}
#[traced_test]
#[test]
fn load_config_file_should_emit_warning_when_path_explicitly_set() {
let env = Env::from_slice(&[("AWS_CONFIG_FILE", "~/some/path")]);
let fs = Fs::from_slice(&[]);
let _src = load_config_file(FileKind::Config, &None, &fs, &env).now_or_never();
assert!(logs_contain(HOME_EXPANSION_FAILURE_WARNING));
}
async fn check(test_case: TestCase) {
let fs = Fs::real();
let env = Env::from(test_case.environment);
let platform_matches = (cfg!(windows) && test_case.platform == "windows")
|| (!cfg!(windows) && test_case.platform != "windows");
if platform_matches {
let source = load(&env, &fs).await;
if let Some(expected_profile) = test_case.profile {
assert_eq!(source.profile, expected_profile, "{}", &test_case.name);
}
assert_eq!(
source.config_file.path, test_case.config_location,
"{}",
&test_case.name
);
assert_eq!(
source.credentials_file.path, test_case.credentials_location,
"{}",
&test_case.name
)
} else {
println!(
"NOTE: ignoring test case for {} which does not apply to our platform: \n {}",
&test_case.platform, &test_case.name
)
}
}
#[test]
#[cfg_attr(windows, ignore)]
fn test_expand_home() {
let path = "~/.aws/config";
assert_eq!(
expand_home(&path, false, &Some("/user/foo".to_string()))
.to_str()
.unwrap(),
"/user/foo/.aws/config"
);
}
#[test]
fn expand_home_no_home() {
if !cfg!(windows) {
assert_eq!(
expand_home("~/config", false, &None).to_str().unwrap(),
"~/config"
)
} else {
assert_eq!(
expand_home("~/config", false, &None).to_str().unwrap(),
"~\\config"
)
}
}
#[test]
#[cfg_attr(not(windows), ignore)]
fn test_expand_home_windows() {
let path = "~/.aws/config";
assert_eq!(
expand_home(&path, true, &Some("C:\\Users\\name".to_string()),)
.to_str()
.unwrap(),
"C:\\Users\\name\\.aws\\config"
);
}
}