1use std::path::{
2 Component,
3 Path,
4 PathBuf,
5};
6use std::{
7 fmt,
8 fs,
9};
10
11use anyhow::Context as _;
12use hashbrown::HashMap;
13use serde::de::{
14 self,
15 MapAccess,
16 Visitor,
17};
18use serde::{
19 Deserialize,
20 Deserializer,
21};
22use serde_json::Value as JsonValue;
23
24use crate::file::ToUtf8 as _;
25
26#[allow(dead_code)]
27#[derive(Debug)]
28enum AnyValue {
29 String(String),
30 Number(serde_json::Number),
31 Bool(bool),
32}
33
34impl fmt::Display for AnyValue {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 AnyValue::String(s) => write!(f, "{}", s),
38 AnyValue::Number(n) => write!(f, "{}", n),
39 AnyValue::Bool(b) => write!(f, "{}", b),
40 }
41 }
42}
43
44impl<'de> Deserialize<'de> for AnyValue {
45 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46 where
47 D: Deserializer<'de>,
48 {
49 let value: JsonValue = Deserialize::deserialize(deserializer)?;
50 match value {
51 JsonValue::String(s) => Ok(AnyValue::String(s)),
52 JsonValue::Number(n) => Ok(AnyValue::Number(n)),
53 JsonValue::Bool(b) => Ok(AnyValue::Bool(b)),
54 _ => Err(de::Error::custom("expected a string, number, or boolean")),
55 }
56 }
57}
58
59pub(crate) fn deserialize_environment<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
60where
61 D: Deserializer<'de>,
62{
63 struct EnvironmentVisitor;
64
65 impl<'de> Visitor<'de> for EnvironmentVisitor {
66 type Value = HashMap<String, String>;
67
68 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
69 formatter.write_str("a map of strings to any value (string, int, or bool)")
70 }
71
72 fn visit_map<M>(self, mut access: M) -> Result<HashMap<String, String>, M::Error>
73 where
74 M: MapAccess<'de>,
75 {
76 let mut map = HashMap::new();
77 while let Some((key, value)) = access.next_entry::<String, AnyValue>()? {
78 map.insert(key, value.to_string());
79 }
80 Ok(map)
81 }
82 }
83
84 deserializer.deserialize_map(EnvironmentVisitor)
85}
86
87pub(crate) fn resolve_path(base_dir: &Path, value: &str) -> PathBuf {
88 let path = Path::new(value);
89 let joined = if path.is_absolute() {
90 path.to_path_buf()
91 } else {
92 base_dir.join(path)
93 };
94
95 normalize_path(&joined)
96}
97
98pub(crate) fn normalize_path(path: &Path) -> PathBuf {
99 let mut normalized = PathBuf::new();
100
101 for component in path.components() {
102 match component {
103 Component::CurDir => {},
104 Component::ParentDir => {
105 normalized.pop();
106 },
107 other => normalized.push(other.as_os_str()),
108 }
109 }
110
111 normalized
112}
113
114pub(crate) fn load_env_files_in_dir(
115 env_files: &[String],
116 base_dir: &Path,
117) -> anyhow::Result<HashMap<String, String>> {
118 let mut local_env: HashMap<String, String> = HashMap::new();
119 for env_file in env_files {
120 let path = resolve_path(base_dir, env_file);
121 let contents = fs::read_to_string(&path).with_context(|| {
122 format!(
123 "Failed to read env file - {}",
124 path.to_utf8().unwrap_or("<non-utf8-path>")
125 )
126 })?;
127
128 local_env.extend(parse_env_contents(&contents));
129 }
130
131 Ok(local_env)
132}
133
134pub(crate) fn parse_env_contents(contents: &str) -> HashMap<String, String> {
135 let mut env_vars = HashMap::new();
136
137 for line in contents.lines() {
138 let line = line.trim();
139 if line.is_empty() || line.starts_with('#') {
140 continue;
141 }
142
143 if let Some((key, value)) = line.split_once('=') {
144 env_vars.insert(key.trim().to_string(), value.trim().to_string());
145 }
146 }
147
148 env_vars
149}