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 expanded = expand_home_path(value);
89 let path = expanded.as_deref().unwrap_or_else(|| Path::new(value));
90 let joined = if path.is_absolute() {
91 path.to_path_buf()
92 } else {
93 base_dir.join(path)
94 };
95
96 normalize_path(&joined)
97}
98
99pub(crate) fn expand_home_path(value: &str) -> Option<PathBuf> {
100 if value == "~" {
101 return home_dir();
102 }
103
104 if let Some(rest) = value.strip_prefix("~/") {
105 return home_dir().map(|home| home.join(rest));
106 }
107
108 None
109}
110
111fn home_dir() -> Option<PathBuf> {
112 #[cfg(windows)]
113 {
114 std::env::var_os("HOME")
115 .or_else(|| std::env::var_os("USERPROFILE"))
116 .map(PathBuf::from)
117 }
118
119 #[cfg(not(windows))]
120 {
121 std::env::var_os("HOME").map(PathBuf::from)
122 }
123}
124
125pub(crate) fn normalize_path(path: &Path) -> PathBuf {
126 let mut normalized = PathBuf::new();
127
128 for component in path.components() {
129 match component {
130 Component::CurDir => {},
131 Component::ParentDir => {
132 normalized.pop();
133 },
134 other => normalized.push(other.as_os_str()),
135 }
136 }
137
138 normalized
139}
140
141pub(crate) fn load_env_files_in_dir(
142 env_files: &[String],
143 base_dir: &Path,
144) -> anyhow::Result<HashMap<String, String>> {
145 let mut local_env: HashMap<String, String> = HashMap::new();
146 for env_file in env_files {
147 let path = resolve_path(base_dir, env_file);
148 let contents = fs::read_to_string(&path).with_context(|| {
149 format!(
150 "Failed to read env file - {}",
151 path.to_utf8().unwrap_or("<non-utf8-path>")
152 )
153 })?;
154
155 local_env.extend(parse_env_contents(&contents));
156 }
157
158 Ok(local_env)
159}
160
161pub(crate) fn parse_env_contents(contents: &str) -> HashMap<String, String> {
162 let mut env_vars = HashMap::new();
163
164 for line in contents.lines() {
165 let line = line.trim();
166 if line.is_empty() || line.starts_with('#') {
167 continue;
168 }
169
170 if let Some((key, value)) = line.split_once('=') {
171 env_vars.insert(key.trim().to_string(), value.trim().to_string());
172 }
173 }
174
175 env_vars
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn resolve_path_expands_home_directory() {
184 let home = std::env::temp_dir().join("mk-utils-home");
185 unsafe {
186 std::env::set_var("HOME", &home);
187 }
188 let resolved = resolve_path(Path::new("/tmp/project"), "~/.mk-test-env");
189 assert_eq!(resolved, home.join(".mk-test-env"));
190 unsafe {
191 std::env::remove_var("HOME");
192 }
193 }
194}