1use super::error::{CouldNotReadConfigFile, EnvConfigFileLoadError};
9use crate::env_config::file::{EnvConfigFile, EnvConfigFileKind, EnvConfigFiles};
10use crate::fs_util::{home_dir, Os};
11use aws_smithy_types::error::display::DisplayErrorContext;
12use aws_types::os_shim_internal;
13use std::borrow::Cow;
14use std::io::ErrorKind;
15use std::path::{Component, Path, PathBuf};
16use std::sync::Arc;
17use tracing::{warn, Instrument};
18const HOME_EXPANSION_FAILURE_WARNING: &str =
19 "home directory expansion was requested (via `~` character) for the profile \
20 config file path, but no home directory could be determined";
21
22#[derive(Debug)]
23pub struct Source {
25 pub(crate) files: Vec<File>,
27
28 pub profile: Cow<'static, str>,
32}
33
34#[derive(Debug)]
35pub struct File {
37 pub(crate) kind: EnvConfigFileKind,
38 pub(crate) path: Option<String>,
39 pub(crate) contents: String,
40}
41
42pub async fn load(
44 proc_env: &os_shim_internal::Env,
45 fs: &os_shim_internal::Fs,
46 profile_files: &EnvConfigFiles,
47) -> Result<Source, EnvConfigFileLoadError> {
48 let home = home_dir(proc_env, Os::real());
49
50 let mut files = Vec::new();
51 for file in &profile_files.files {
52 let file = load_config_file(file, &home, fs, proc_env)
53 .instrument(tracing::debug_span!("load_config_file", file = ?file))
54 .await?;
55 files.push(file);
56 }
57
58 Ok(Source {
59 files,
60 profile: proc_env
61 .get("AWS_PROFILE")
62 .map(Cow::Owned)
63 .unwrap_or(Cow::Borrowed("default")),
64 })
65}
66
67fn file_contents_to_string(path: &Path, contents: Vec<u8>) -> String {
68 match String::from_utf8(contents) {
70 Ok(contents) => contents,
71 Err(e) => {
72 tracing::warn!(path = ?path, error = %DisplayErrorContext(&e), "config file did not contain utf-8 encoded data");
73 Default::default()
74 }
75 }
76}
77
78async fn load_config_file(
89 source: &EnvConfigFile,
90 home_directory: &Option<String>,
91 fs: &os_shim_internal::Fs,
92 environment: &os_shim_internal::Env,
93) -> Result<File, EnvConfigFileLoadError> {
94 let (path, kind, contents) = match source {
95 EnvConfigFile::Default(kind) => {
96 let (path_is_default, path) = environment
97 .get(kind.override_environment_variable())
98 .map(|p| (false, Cow::Owned(p)))
99 .ok()
100 .unwrap_or_else(|| (true, kind.default_path().into()));
101 let expanded = expand_home(path.as_ref(), path_is_default, home_directory);
102 if path != expanded.to_string_lossy() {
103 tracing::debug!(before = ?path, after = ?expanded, "home directory expanded");
104 }
105 let data = match fs.read_to_end(&expanded).await {
108 Ok(data) => data,
109 Err(e) => {
110 match e.kind() {
112 ErrorKind::NotFound if path == kind.default_path() => {
113 tracing::debug!(path = %path, "config file not found")
114 }
115 ErrorKind::NotFound if path != kind.default_path() => {
116 tracing::warn!(path = %path, env = %kind.override_environment_variable(), "config file overridden via environment variable not found")
119 }
120 _other => {
121 tracing::warn!(path = %path, error = %DisplayErrorContext(&e), "failed to read config file")
122 }
123 };
124 Default::default()
125 }
126 };
127 let contents = file_contents_to_string(&expanded, data);
128 (Some(Cow::Owned(expanded)), kind, contents)
129 }
130 EnvConfigFile::FilePath { kind, path } => {
131 let data = match fs.read_to_end(&path).await {
132 Ok(data) => data,
133 Err(e) => {
134 return Err(EnvConfigFileLoadError::CouldNotReadFile(
135 CouldNotReadConfigFile {
136 path: path.clone(),
137 cause: Arc::new(e),
138 },
139 ))
140 }
141 };
142 (
143 Some(Cow::Borrowed(path)),
144 kind,
145 file_contents_to_string(path, data),
146 )
147 }
148 EnvConfigFile::FileContents { kind, contents } => (None, kind, contents.clone()),
149 };
150 tracing::debug!(path = ?path, size = ?contents.len(), "config file loaded");
151 Ok(File {
152 kind: *kind,
153 path: path.map(|p| p.to_string_lossy().into()),
155 contents,
156 })
157}
158
159fn expand_home(
160 path: impl AsRef<Path>,
161 path_is_default: bool,
162 home_dir: &Option<String>,
163) -> PathBuf {
164 let path = path.as_ref();
165 let mut components = path.components();
166 let start = components.next();
167 match start {
168 None => path.into(), Some(Component::Normal(s)) if s == "~" => {
170 let path = match home_dir {
172 Some(dir) => {
173 tracing::debug!(home = ?dir, path = ?path, "performing home directory substitution");
174 dir.clone()
175 }
176 None => {
177 if !path_is_default {
179 warn!(HOME_EXPANSION_FAILURE_WARNING);
180 }
181 "~".into()
183 }
184 };
185 let mut path: PathBuf = path.into();
186 for component in components {
188 path.push(component);
189 }
190 path
191 }
192 _other => path.into(),
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use crate::env_config::error::EnvConfigFileLoadError;
203 use crate::env_config::file::{EnvConfigFile, EnvConfigFileKind, EnvConfigFiles};
204 use crate::env_config::source::{
205 expand_home, load, load_config_file, HOME_EXPANSION_FAILURE_WARNING,
206 };
207 use aws_types::os_shim_internal::{Env, Fs};
208 use futures_util::future::FutureExt;
209 use serde::Deserialize;
210 use std::collections::HashMap;
211 use std::error::Error;
212 use std::fs;
213 use tracing_test::traced_test;
214
215 #[test]
216 fn only_expand_home_prefix() {
217 let path = "~aws/config";
219 assert_eq!(
220 expand_home(path, false, &None).to_str().unwrap(),
221 "~aws/config"
222 );
223 }
224
225 #[derive(Deserialize, Debug)]
226 #[serde(rename_all = "camelCase")]
227 struct SourceTests {
228 tests: Vec<TestCase>,
229 }
230
231 #[derive(Deserialize, Debug)]
232 #[serde(rename_all = "camelCase")]
233 struct TestCase {
234 name: String,
235 environment: HashMap<String, String>,
236 platform: String,
237 profile: Option<String>,
238 config_location: String,
239 credentials_location: String,
240 }
241
242 #[test]
244 fn run_tests() -> Result<(), Box<dyn Error>> {
245 let tests = fs::read_to_string("test-data/file-location-tests.json")?;
246 let tests: SourceTests = serde_json::from_str(&tests)?;
247 for (i, test) in tests.tests.into_iter().enumerate() {
248 eprintln!("test: {}", i);
249 check(test)
250 .now_or_never()
251 .expect("these futures should never poll");
252 }
253 Ok(())
254 }
255
256 #[traced_test]
257 #[test]
258 fn logs_produced_default() {
259 let env = Env::from_slice(&[("HOME", "/user/name")]);
260 let mut fs = HashMap::new();
261 fs.insert(
262 "/user/name/.aws/config".to_string(),
263 "[default]\nregion = us-east-1",
264 );
265
266 let fs = Fs::from_map(fs);
267
268 let _src = load(&env, &fs, &Default::default()).now_or_never();
269 assert!(logs_contain("config file loaded"));
270 assert!(logs_contain("performing home directory substitution"));
271 }
272
273 #[traced_test]
274 #[test]
275 fn load_config_file_should_not_emit_warning_when_path_not_explicitly_set() {
276 let env = Env::from_slice(&[]);
277 let fs = Fs::from_slice(&[]);
278
279 let _src = load_config_file(
280 &EnvConfigFile::Default(EnvConfigFileKind::Config),
281 &None,
282 &fs,
283 &env,
284 )
285 .now_or_never();
286 assert!(!logs_contain(HOME_EXPANSION_FAILURE_WARNING));
287 }
288
289 #[traced_test]
290 #[test]
291 fn load_config_file_should_emit_warning_when_path_explicitly_set() {
292 let env = Env::from_slice(&[("AWS_CONFIG_FILE", "~/some/path")]);
293 let fs = Fs::from_slice(&[]);
294
295 let _src = load_config_file(
296 &EnvConfigFile::Default(EnvConfigFileKind::Config),
297 &None,
298 &fs,
299 &env,
300 )
301 .now_or_never();
302 assert!(logs_contain(HOME_EXPANSION_FAILURE_WARNING));
303 }
304
305 async fn check(test_case: TestCase) {
306 let fs = Fs::real();
307 let env = Env::from(test_case.environment);
308 let platform_matches = (cfg!(windows) && test_case.platform == "windows")
309 || (!cfg!(windows) && test_case.platform != "windows");
310 if platform_matches {
311 let source = load(&env, &fs, &Default::default()).await.unwrap();
312 if let Some(expected_profile) = test_case.profile {
313 assert_eq!(source.profile, expected_profile, "{}", &test_case.name);
314 }
315 assert_eq!(
316 source.files[0].path,
317 Some(test_case.config_location),
318 "{}",
319 &test_case.name
320 );
321 assert_eq!(
322 source.files[1].path,
323 Some(test_case.credentials_location),
324 "{}",
325 &test_case.name
326 )
327 } else {
328 println!(
329 "NOTE: ignoring test case for {} which does not apply to our platform: \n {}",
330 &test_case.platform, &test_case.name
331 )
332 }
333 }
334
335 #[test]
336 #[cfg_attr(windows, ignore)]
337 fn test_expand_home() {
338 let path = "~/.aws/config";
339 assert_eq!(
340 expand_home(path, false, &Some("/user/foo".to_string()))
341 .to_str()
342 .unwrap(),
343 "/user/foo/.aws/config"
344 );
345 }
346
347 #[test]
348 fn expand_home_no_home() {
349 if !cfg!(windows) {
352 assert_eq!(
353 expand_home("~/config", false, &None).to_str().unwrap(),
354 "~/config"
355 )
356 } else {
357 assert_eq!(
358 expand_home("~/config", false, &None).to_str().unwrap(),
359 "~\\config"
360 )
361 }
362 }
363
364 #[test]
366 #[cfg_attr(not(windows), ignore)]
367 fn test_expand_home_windows() {
368 let path = "~/.aws/config";
369 assert_eq!(
370 expand_home(path, true, &Some("C:\\Users\\name".to_string()),)
371 .to_str()
372 .unwrap(),
373 "C:\\Users\\name\\.aws\\config"
374 );
375 }
376
377 #[tokio::test]
378 async fn programmatically_set_credentials_file_contents() {
379 let contents = "[default]\n\
380 aws_access_key_id = AKIAFAKE\n\
381 aws_secret_access_key = FAKE\n\
382 ";
383 let env = Env::from_slice(&[]);
384 let fs = Fs::from_slice(&[]);
385 let profile_files = EnvConfigFiles::builder()
386 .with_contents(EnvConfigFileKind::Credentials, contents)
387 .build();
388 let source = load(&env, &fs, &profile_files).await.unwrap();
389 assert_eq!(1, source.files.len());
390 assert_eq!("default", source.profile);
391 assert_eq!(contents, source.files[0].contents);
392 }
393
394 #[tokio::test]
395 async fn programmatically_set_credentials_file_path() {
396 let contents = "[default]\n\
397 aws_access_key_id = AKIAFAKE\n\
398 aws_secret_access_key = FAKE\n\
399 ";
400 let mut fs = HashMap::new();
401 fs.insert(
402 "/custom/path/to/credentials".to_string(),
403 contents.to_string(),
404 );
405
406 let fs = Fs::from_map(fs);
407 let env = Env::from_slice(&[]);
408 let profile_files = EnvConfigFiles::builder()
409 .with_file(
410 EnvConfigFileKind::Credentials,
411 "/custom/path/to/credentials",
412 )
413 .build();
414 let source = load(&env, &fs, &profile_files).await.unwrap();
415 assert_eq!(1, source.files.len());
416 assert_eq!("default", source.profile);
417 assert_eq!(contents, source.files[0].contents);
418 }
419
420 #[tokio::test]
422 #[cfg_attr(windows, ignore)]
423 async fn programmatically_include_default_files() {
424 let config_contents = "[default]\nregion = us-east-1";
425 let credentials_contents = "[default]\n\
426 aws_access_key_id = AKIAFAKE\n\
427 aws_secret_access_key = FAKE\n\
428 ";
429 let custom_contents = "[profile some-profile]\n\
430 aws_access_key_id = AKIAFAKEOTHER\n\
431 aws_secret_access_key = FAKEOTHER\n\
432 ";
433 let mut fs = HashMap::new();
434 fs.insert(
435 "/user/name/.aws/config".to_string(),
436 config_contents.to_string(),
437 );
438 fs.insert(
439 "/user/name/.aws/credentials".to_string(),
440 credentials_contents.to_string(),
441 );
442
443 let fs = Fs::from_map(fs);
444 let env = Env::from_slice(&[("HOME", "/user/name")]);
445 let profile_files = EnvConfigFiles::builder()
446 .with_contents(EnvConfigFileKind::Config, custom_contents)
447 .include_default_credentials_file(true)
448 .include_default_config_file(true)
449 .build();
450 let source = load(&env, &fs, &profile_files).await.unwrap();
451 assert_eq!(3, source.files.len());
452 assert_eq!("default", source.profile);
453 assert_eq!(config_contents, source.files[0].contents);
454 assert_eq!(credentials_contents, source.files[1].contents);
455 assert_eq!(custom_contents, source.files[2].contents);
456 }
457
458 #[tokio::test]
459 async fn default_files_must_not_error() {
460 let custom_contents = "[profile some-profile]\n\
461 aws_access_key_id = AKIAFAKEOTHER\n\
462 aws_secret_access_key = FAKEOTHER\n\
463 ";
464
465 let fs = Fs::from_slice(&[]);
466 let env = Env::from_slice(&[("HOME", "/user/name")]);
467 let profile_files = EnvConfigFiles::builder()
468 .with_contents(EnvConfigFileKind::Config, custom_contents)
469 .include_default_credentials_file(true)
470 .include_default_config_file(true)
471 .build();
472 let source = load(&env, &fs, &profile_files).await.unwrap();
473 assert_eq!(3, source.files.len());
474 assert_eq!("default", source.profile);
475 assert_eq!("", source.files[0].contents);
476 assert_eq!("", source.files[1].contents);
477 assert_eq!(custom_contents, source.files[2].contents);
478 }
479
480 #[tokio::test]
481 async fn misconfigured_programmatic_custom_profile_path_must_error() {
482 let fs = Fs::from_slice(&[]);
483 let env = Env::from_slice(&[]);
484 let profile_files = EnvConfigFiles::builder()
485 .with_file(EnvConfigFileKind::Config, "definitely-doesnt-exist")
486 .build();
487 assert!(matches!(
488 load(&env, &fs, &profile_files).await,
489 Err(EnvConfigFileLoadError::CouldNotReadFile(_))
490 ));
491 }
492}