1use crate::env_config::normalize;
9use crate::env_config::parse::{parse_profile_file, EnvConfigParseError};
10use crate::env_config::property::{Properties, Property};
11use crate::env_config::source::Source;
12use std::borrow::Cow;
13use std::collections::HashMap;
14
15pub(crate) trait Section {
17 fn name(&self) -> &str;
19
20 #[allow(dead_code)]
22 fn properties(&self) -> &HashMap<String, Property>;
23
24 fn get(&self, name: &str) -> Option<&str>;
26
27 #[allow(dead_code)]
29 fn is_empty(&self) -> bool;
30
31 fn insert(&mut self, name: String, value: Property);
33}
34
35#[derive(Debug, Clone, Eq, PartialEq)]
36pub(super) struct SectionInner {
37 pub(super) name: String,
38 pub(super) properties: HashMap<String, Property>,
39}
40
41impl Section for SectionInner {
42 fn name(&self) -> &str {
43 &self.name
44 }
45
46 fn properties(&self) -> &HashMap<String, Property> {
47 &self.properties
48 }
49
50 fn get(&self, name: &str) -> Option<&str> {
51 self.properties
52 .get(name.to_ascii_lowercase().as_str())
53 .map(|prop| prop.value())
54 }
55
56 fn is_empty(&self) -> bool {
57 self.properties.is_empty()
58 }
59
60 fn insert(&mut self, name: String, value: Property) {
61 self.properties.insert(name.to_ascii_lowercase(), value);
62 }
63}
64
65#[derive(Debug, Clone, Eq, PartialEq)]
69pub struct Profile(SectionInner);
70
71impl Profile {
72 pub fn new(name: impl Into<String>, properties: HashMap<String, Property>) -> Self {
74 Self(SectionInner {
75 name: name.into(),
76 properties,
77 })
78 }
79
80 pub fn name(&self) -> &str {
82 self.0.name()
83 }
84
85 pub fn get(&self, name: &str) -> Option<&str> {
87 self.0.get(name)
88 }
89}
90
91impl Section for Profile {
92 fn name(&self) -> &str {
93 self.0.name()
94 }
95
96 fn properties(&self) -> &HashMap<String, Property> {
97 self.0.properties()
98 }
99
100 fn get(&self, name: &str) -> Option<&str> {
101 self.0.get(name)
102 }
103
104 fn is_empty(&self) -> bool {
105 self.0.is_empty()
106 }
107
108 fn insert(&mut self, name: String, value: Property) {
109 self.0.insert(name, value)
110 }
111}
112
113#[derive(Debug, Clone, Eq, PartialEq)]
115pub struct SsoSession(SectionInner);
116
117impl SsoSession {
118 pub(super) fn new(name: impl Into<String>, properties: HashMap<String, Property>) -> Self {
120 Self(SectionInner {
121 name: name.into(),
122 properties,
123 })
124 }
125
126 pub fn get(&self, name: &str) -> Option<&str> {
128 self.0.get(name)
129 }
130}
131
132impl Section for SsoSession {
133 fn name(&self) -> &str {
134 self.0.name()
135 }
136
137 fn properties(&self) -> &HashMap<String, Property> {
138 self.0.properties()
139 }
140
141 fn get(&self, name: &str) -> Option<&str> {
142 self.0.get(name)
143 }
144
145 fn is_empty(&self) -> bool {
146 self.0.is_empty()
147 }
148
149 fn insert(&mut self, name: String, value: Property) {
150 self.0.insert(name, value)
151 }
152}
153
154#[derive(Debug, Eq, Clone, PartialEq)]
156pub struct EnvConfigSections {
157 pub(crate) profiles: HashMap<String, Profile>,
158 pub(crate) selected_profile: Cow<'static, str>,
159 pub(crate) sso_sessions: HashMap<String, SsoSession>,
160 pub(crate) other_sections: Properties,
161}
162
163impl Default for EnvConfigSections {
164 fn default() -> Self {
165 Self {
166 profiles: Default::default(),
167 selected_profile: "default".into(),
168 sso_sessions: Default::default(),
169 other_sections: Default::default(),
170 }
171 }
172}
173
174impl EnvConfigSections {
175 #[cfg(test)]
179 pub fn new(
180 profiles: HashMap<String, HashMap<String, String>>,
181 selected_profile: impl Into<Cow<'static, str>>,
182 sso_sessions: HashMap<String, HashMap<String, String>>,
183 other_sections: Properties,
184 ) -> Self {
185 let mut base = EnvConfigSections {
186 selected_profile: selected_profile.into(),
187 ..Default::default()
188 };
189 for (name, profile) in profiles {
190 base.profiles.insert(
191 name.clone(),
192 Profile::new(
193 name,
194 profile
195 .into_iter()
196 .map(|(k, v)| (k.clone(), Property::new(k, v)))
197 .collect(),
198 ),
199 );
200 }
201 for (name, session) in sso_sessions {
202 base.sso_sessions.insert(
203 name.clone(),
204 SsoSession::new(
205 name,
206 session
207 .into_iter()
208 .map(|(k, v)| (k.clone(), Property::new(k, v)))
209 .collect(),
210 ),
211 );
212 }
213 base.other_sections = other_sections;
214 base
215 }
216
217 pub fn get(&self, key: &str) -> Option<&str> {
219 self.profiles
220 .get(self.selected_profile.as_ref())
221 .and_then(|profile| profile.get(key))
222 }
223
224 pub fn get_profile(&self, profile_name: &str) -> Option<&Profile> {
226 self.profiles.get(profile_name)
227 }
228
229 pub fn selected_profile(&self) -> &str {
231 self.selected_profile.as_ref()
232 }
233
234 pub fn is_empty(&self) -> bool {
236 self.profiles.is_empty()
237 }
238
239 pub fn profiles(&self) -> impl Iterator<Item = &str> {
241 self.profiles.keys().map(String::as_ref)
242 }
243
244 pub fn sso_sessions(&self) -> impl Iterator<Item = &str> {
246 self.sso_sessions.keys().map(String::as_ref)
247 }
248
249 pub fn sso_session(&self, name: &str) -> Option<&SsoSession> {
251 self.sso_sessions.get(name)
252 }
253
254 pub fn other_sections(&self) -> &Properties {
256 &self.other_sections
257 }
258
259 pub fn parse(source: Source) -> Result<Self, EnvConfigParseError> {
261 let mut base = EnvConfigSections {
262 selected_profile: source.profile,
263 ..Default::default()
264 };
265
266 for file in source.files {
267 normalize::merge_in(&mut base, parse_profile_file(&file)?, file.kind);
268 }
269 Ok(base)
270 }
271}
272
273#[cfg(test)]
274mod test {
275 use super::EnvConfigSections;
276 use crate::env_config::file::EnvConfigFileKind;
277 use crate::env_config::section::Section;
278 use crate::env_config::source::{File, Source};
279 use arbitrary::{Arbitrary, Unstructured};
280 use serde::Deserialize;
281 use std::collections::HashMap;
282 use std::error::Error;
283 use std::fs;
284 use tracing_test::traced_test;
285
286 #[test]
290 #[traced_test]
291 fn run_tests() -> Result<(), Box<dyn Error>> {
292 let tests = fs::read_to_string("test-data/profile-parser-tests.json")?;
293 let tests: ParserTests = serde_json::from_str(&tests)?;
294 for (i, test) in tests.tests.into_iter().enumerate() {
295 eprintln!("test: {}", i);
296 check(test);
297 }
298 Ok(())
299 }
300
301 #[test]
302 fn empty_source_empty_profile() {
303 let source = make_source(ParserInput {
304 config_file: Some("".to_string()),
305 credentials_file: Some("".to_string()),
306 });
307
308 let profile_set = EnvConfigSections::parse(source).expect("empty profiles are valid");
309 assert!(profile_set.is_empty());
310 }
311
312 #[test]
313 fn profile_names_are_exposed() {
314 let source = make_source(ParserInput {
315 config_file: Some("[profile foo]\n[profile bar]".to_string()),
316 credentials_file: Some("".to_string()),
317 });
318
319 let profile_set = EnvConfigSections::parse(source).expect("profiles loaded");
320
321 let mut profile_names: Vec<_> = profile_set.profiles().collect();
322 profile_names.sort();
323 assert_eq!(profile_names, vec!["bar", "foo"]);
324 }
325
326 #[test]
328 #[ignore]
329 fn run_fuzz_tests() -> Result<(), Box<dyn Error>> {
330 let fuzz_corpus = fs::read_dir("fuzz/corpus/profile-parser")?
331 .map(|res| res.map(|entry| entry.path()))
332 .collect::<Result<Vec<_>, _>>()?;
333 for file in fuzz_corpus {
334 let raw = fs::read(file)?;
335 let mut unstructured = Unstructured::new(&raw);
336 let (conf, creds): (Option<&str>, Option<&str>) =
337 Arbitrary::arbitrary(&mut unstructured)?;
338 let profile_source = Source {
339 files: vec![
340 File {
341 kind: EnvConfigFileKind::Config,
342 path: Some("~/.aws/config".to_string()),
343 contents: conf.unwrap_or_default().to_string(),
344 },
345 File {
346 kind: EnvConfigFileKind::Credentials,
347 path: Some("~/.aws/credentials".to_string()),
348 contents: creds.unwrap_or_default().to_string(),
349 },
350 ],
351 profile: "default".into(),
352 };
353 let _ = EnvConfigSections::parse(profile_source);
355 }
356
357 Ok(())
358 }
359
360 #[derive(Debug)]
362 struct FlattenedProfileSet {
363 profiles: HashMap<String, HashMap<String, String>>,
364 sso_sessions: HashMap<String, HashMap<String, String>>,
365 }
366 fn flatten(config: EnvConfigSections) -> FlattenedProfileSet {
367 FlattenedProfileSet {
368 profiles: flatten_sections(config.profiles.values().map(|p| p as _)),
369 sso_sessions: flatten_sections(config.sso_sessions.values().map(|s| s as _)),
370 }
371 }
372 fn flatten_sections<'a>(
373 sections: impl Iterator<Item = &'a dyn Section>,
374 ) -> HashMap<String, HashMap<String, String>> {
375 sections
376 .map(|section| {
377 (
378 section.name().to_string(),
379 section
380 .properties()
381 .values()
382 .map(|prop| (prop.key().to_owned(), prop.value().to_owned()))
383 .collect(),
384 )
385 })
386 .collect()
387 }
388
389 fn make_source(input: ParserInput) -> Source {
390 Source {
391 files: vec![
392 File {
393 kind: EnvConfigFileKind::Config,
394 path: Some("~/.aws/config".to_string()),
395 contents: input.config_file.unwrap_or_default(),
396 },
397 File {
398 kind: EnvConfigFileKind::Credentials,
399 path: Some("~/.aws/credentials".to_string()),
400 contents: input.credentials_file.unwrap_or_default(),
401 },
402 ],
403 profile: "default".into(),
404 }
405 }
406
407 fn check(test_case: ParserTest) {
409 let copy = test_case.clone();
410 let parsed = EnvConfigSections::parse(make_source(test_case.input));
411 let res = match (parsed.map(flatten), &test_case.output) {
412 (
413 Ok(FlattenedProfileSet {
414 profiles: actual_profiles,
415 sso_sessions: actual_sso_sessions,
416 }),
417 ParserOutput::Config {
418 profiles,
419 sso_sessions,
420 },
421 ) => {
422 if profiles != &actual_profiles {
423 Err(format!(
424 "mismatched profiles:\nExpected: {profiles:#?}\nActual: {actual_profiles:#?}",
425 ))
426 } else if sso_sessions != &actual_sso_sessions {
427 Err(format!(
428 "mismatched sso_sessions:\nExpected: {sso_sessions:#?}\nActual: {actual_sso_sessions:#?}",
429 ))
430 } else {
431 Ok(())
432 }
433 }
434 (Err(msg), ParserOutput::ErrorContaining(substr)) => {
435 if format!("{}", msg).contains(substr) {
436 Ok(())
437 } else {
438 Err(format!("Expected {} to contain {}", msg, substr))
439 }
440 }
441 (Ok(output), ParserOutput::ErrorContaining(err)) => Err(format!(
442 "expected an error: {err} but parse succeeded:\n{output:#?}",
443 )),
444 (Err(err), ParserOutput::Config { .. }) => {
445 Err(format!("Expected to succeed but got: {}", err))
446 }
447 };
448 if let Err(e) = res {
449 eprintln!("Test case failed: {:#?}", copy);
450 eprintln!("failure: {}", e);
451 panic!("test failed")
452 }
453 }
454
455 #[derive(Deserialize, Debug)]
456 #[serde(rename_all = "camelCase")]
457 struct ParserTests {
458 tests: Vec<ParserTest>,
459 }
460
461 #[derive(Deserialize, Debug, Clone)]
462 #[serde(rename_all = "camelCase")]
463 struct ParserTest {
464 _name: String,
465 input: ParserInput,
466 output: ParserOutput,
467 }
468
469 #[derive(Deserialize, Debug, Clone)]
470 #[serde(rename_all = "camelCase")]
471 enum ParserOutput {
472 Config {
473 profiles: HashMap<String, HashMap<String, String>>,
474 #[serde(default)]
475 sso_sessions: HashMap<String, HashMap<String, String>>,
476 },
477 ErrorContaining(String),
478 }
479
480 #[derive(Deserialize, Debug, Clone)]
481 #[serde(rename_all = "camelCase")]
482 struct ParserInput {
483 config_file: Option<String>,
484 credentials_file: Option<String>,
485 }
486}