use aws_types::os_shim_internal;
use std::borrow::Cow;
use std::io::ErrorKind;
use std::path::{Component, Path, PathBuf};
use tracing::Instrument;
pub struct Source {
pub config_file: File,
pub credentials_file: File,
pub profile: Cow<'static, str>,
}
pub struct File {
pub path: String,
pub contents: String,
}
#[derive(Clone, Copy)]
pub 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 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::info_span!("load_config_file"))
.await;
let credentials = load_config_file(FileKind::Credentials, &home, fs, proc_env)
.instrument(tracing::info_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 = environment
.get(kind.override_environment_variable())
.map(Cow::Owned)
.ok()
.unwrap_or_else(|| kind.default_path().into());
let expanded = expand_home(path.as_ref(), home_directory, environment);
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::info!(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::info!(path = %path, size = ?data.len(), "config file loaded");
File {
path: expanded.to_string_lossy().into(),
contents: data,
}
}
fn expand_home(
path: impl AsRef<Path>,
home_dir: &Option<String>,
environment: &os_shim_internal::Env,
) -> 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 => {
let is_likely_running_on_a_lambda =
check_is_likely_running_on_a_lambda(environment);
if !is_likely_running_on_a_lambda {
tracing::warn!(
"could not determine home directory but home expansion was requested"
);
}
"~".into()
}
};
let mut path: PathBuf = path.into();
for component in components {
path.push(component);
}
path
}
_other => path.into(),
}
}
fn check_is_likely_running_on_a_lambda(environment: &os_shim_internal::Env) -> bool {
environment.get("LAMBDA_TASK_ROOT").is_ok()
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Os {
Windows,
NotWindows,
}
impl Os {
pub fn real() -> Self {
match std::env::consts::OS {
"windows" => Os::Windows,
_ => Os::NotWindows,
}
}
}
fn home_dir(env_var: &os_shim_internal::Env, os: Os) -> Option<String> {
if let Ok(home) = env_var.get("HOME") {
tracing::debug!(src = "HOME", "loaded home directory");
return Some(home);
}
if os == Os::Windows {
if let Ok(home) = env_var.get("USERPROFILE") {
tracing::debug!(src = "USERPROFILE", "loaded home directory");
return Some(home);
}
let home_drive = env_var.get("HOMEDRIVE");
let home_path = env_var.get("HOMEPATH");
tracing::debug!(src = "HOMEDRIVE/HOMEPATH", "loaded home directory");
if let (Ok(mut drive), Ok(path)) = (home_drive, home_path) {
drive.push_str(&path);
return Some(drive);
}
}
None
}
#[cfg(test)]
mod tests {
use crate::profile::parser::source::{
expand_home, home_dir, load, load_config_file, FileKind, Os,
};
use aws_types::os_shim_internal::{Env, Fs};
use serde::Deserialize;
use std::collections::HashMap;
use std::error::Error;
use std::fs;
#[test]
fn only_expand_home_prefix() {
let path = "~aws/config";
let environment = Env::from_slice(&[]);
assert_eq!(
expand_home(&path, &None, &environment).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(())
}
use futures_util::FutureExt;
use tracing_test::traced_test;
#[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_on_lambda() {
let env = Env::from_slice(&[("LAMBDA_TASK_ROOT", "/")]);
let fs = Fs::from_slice(&[]);
let _src = load_config_file(FileKind::Config, &None, &fs, &env).now_or_never();
assert!(!logs_contain(
"could not determine home directory but home expansion was requested"
));
}
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";
let environment = Env::from_slice(&[]);
assert_eq!(
expand_home(&path, &Some("/user/foo".to_string()), &environment)
.to_str()
.unwrap(),
"/user/foo/.aws/config"
);
}
#[test]
fn homedir_profile_only_windows() {
let env = Env::from_slice(&[("USERPROFILE", "C:\\Users\\name")]);
assert_eq!(
home_dir(&env, Os::Windows),
Some("C:\\Users\\name".to_string())
);
assert_eq!(home_dir(&env, Os::NotWindows), None);
}
#[test]
fn expand_home_no_home() {
let environment = Env::from_slice(&[]);
if !cfg!(windows) {
assert_eq!(
expand_home("~/config", &None, &environment)
.to_str()
.unwrap(),
"~/config"
)
} else {
assert_eq!(
expand_home("~/config", &None, &environment)
.to_str()
.unwrap(),
"~\\config"
)
}
}
#[test]
#[cfg_attr(not(windows), ignore)]
fn test_expand_home_windows() {
let path = "~/.aws/config";
let environment = Env::from_slice(&[]);
assert_eq!(
expand_home(&path, &Some("C:\\Users\\name".to_string()), &environment)
.to_str()
.unwrap(),
"C:\\Users\\name\\.aws\\config"
);
}
}