1
use std::path::{
2
  Component,
3
  Path,
4
  PathBuf,
5
};
6
use std::{
7
  fmt,
8
  fs,
9
};
10

            
11
use anyhow::Context as _;
12
use hashbrown::HashMap;
13
use serde::de::{
14
  self,
15
  MapAccess,
16
  Visitor,
17
};
18
use serde::{
19
  Deserialize,
20
  Deserializer,
21
};
22
use serde_json::Value as JsonValue;
23

            
24
use crate::file::ToUtf8 as _;
25

            
26
#[allow(dead_code)]
27
#[derive(Debug)]
28
enum AnyValue {
29
  String(String),
30
  Number(serde_json::Number),
31
  Bool(bool),
32
}
33

            
34
impl fmt::Display for AnyValue {
35
312
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36
312
    match self {
37
308
      AnyValue::String(s) => write!(f, "{}", s),
38
4
      AnyValue::Number(n) => write!(f, "{}", n),
39
      AnyValue::Bool(b) => write!(f, "{}", b),
40
    }
41
312
  }
42
}
43

            
44
impl<'de> Deserialize<'de> for AnyValue {
45
312
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46
312
  where
47
312
    D: Deserializer<'de>,
48
  {
49
312
    let value: JsonValue = Deserialize::deserialize(deserializer)?;
50
312
    match value {
51
308
      JsonValue::String(s) => Ok(AnyValue::String(s)),
52
4
      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
312
  }
57
}
58

            
59
136
pub(crate) fn deserialize_environment<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
60
136
where
61
136
  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
136
    fn visit_map<M>(self, mut access: M) -> Result<HashMap<String, String>, M::Error>
73
136
    where
74
136
      M: MapAccess<'de>,
75
    {
76
136
      let mut map = HashMap::new();
77
448
      while let Some((key, value)) = access.next_entry::<String, AnyValue>()? {
78
312
        map.insert(key, value.to_string());
79
312
      }
80
136
      Ok(map)
81
136
    }
82
  }
83

            
84
136
  deserializer.deserialize_map(EnvironmentVisitor)
85
136
}
86

            
87
196
pub(crate) fn resolve_path(base_dir: &Path, value: &str) -> PathBuf {
88
196
  let expanded = expand_home_path(value);
89
196
  let path = expanded.as_deref().unwrap_or_else(|| Path::new(value));
90
196
  let joined = if path.is_absolute() {
91
96
    path.to_path_buf()
92
  } else {
93
100
    base_dir.join(path)
94
  };
95

            
96
196
  normalize_path(&joined)
97
196
}
98

            
99
566
pub(crate) fn expand_home_path(value: &str) -> Option<PathBuf> {
100
566
  if value == "~" {
101
    return home_dir();
102
566
  }
103

            
104
566
  if let Some(rest) = value.strip_prefix("~/") {
105
2
    return home_dir().map(|home| home.join(rest));
106
564
  }
107

            
108
564
  None
109
566
}
110

            
111
2
fn 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
2
    std::env::var_os("HOME").map(PathBuf::from)
122
  }
123
2
}
124

            
125
196
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
126
196
  let mut normalized = PathBuf::new();
127

            
128
1046
  for component in path.components() {
129
1046
    match component {
130
16
      Component::CurDir => {},
131
      Component::ParentDir => {
132
        normalized.pop();
133
      },
134
1030
      other => normalized.push(other.as_os_str()),
135
    }
136
  }
137

            
138
196
  normalized
139
196
}
140

            
141
70
pub(crate) fn load_env_files_in_dir(
142
70
  env_files: &[String],
143
70
  base_dir: &Path,
144
70
) -> anyhow::Result<HashMap<String, String>> {
145
70
  let mut local_env: HashMap<String, String> = HashMap::new();
146
70
  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
70
  Ok(local_env)
159
70
}
160

            
161
pub(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)]
179
mod tests {
180
  use super::*;
181

            
182
  #[test]
183
2
  fn resolve_path_expands_home_directory() {
184
2
    let home = std::env::temp_dir().join("mk-utils-home");
185
2
    unsafe {
186
2
      std::env::set_var("HOME", &home);
187
2
    }
188
2
    let resolved = resolve_path(Path::new("/tmp/project"), "~/.mk-test-env");
189
2
    assert_eq!(resolved, home.join(".mk-test-env"));
190
2
    unsafe {
191
2
      std::env::remove_var("HOME");
192
2
    }
193
2
  }
194
}