1use crate::span::{is_single_line, line_column_from_line};
41use crate::{Parse, Source};
42use cfg_if::cfg_if;
43use tanzim_value::{Error, LocatedValue, Location, Map, Value};
44
45#[derive(Clone, Copy, Default)]
67pub struct Env;
68
69impl Env {
70 pub fn new() -> Self {
72 Self
73 }
74}
75
76impl Parse for Env {
77 fn name(&self) -> &str {
78 "Environment-Variables"
79 }
80
81 fn supported_format_list(&self) -> Vec<String> {
82 vec!["env".into()]
83 }
84
85 fn parse(&self, source: &Source, bytes: &[u8]) -> Result<LocatedValue, Error> {
86 fn insert_nested(map: &mut Map, parts: &[String], value: LocatedValue) {
87 if parts.is_empty() {
88 return;
89 }
90 if parts.len() == 1 {
91 map.insert(parts[0].clone(), value);
92 return;
93 }
94 let head = parts[0].clone();
95 let rest = &parts[1..];
96 match map.get_mut(&head) {
97 Some(existing) => {
98 if let Value::Map(ref mut inner) = existing.value {
99 insert_nested(inner, rest, value);
100 return;
101 }
102 let loc = value.location.clone();
103 let mut inner = Map::new();
104 insert_nested(&mut inner, rest, value);
105 existing.value = Value::Map(inner);
106 existing.location = loc;
107 }
108 None => {
109 let loc = value.location.clone();
110 let mut inner = Map::new();
111 insert_nested(&mut inner, rest, value);
112 map.insert(
113 head,
114 LocatedValue {
115 value: Value::Map(inner),
116 location: loc,
117 },
118 );
119 }
120 }
121 }
122
123 let source_name = source.source();
124 let resource = source.resource();
125 cfg_if! {
126 if #[cfg(feature = "tracing")] {
127 tracing::debug!(msg = "Parsing env-format configuration", source = source_name, resource = resource, bytes = bytes.len());
128 } else if #[cfg(feature = "logging")] {
129 log::debug!("msg=\"Parsing env-format configuration\" source={source_name} resource={resource} bytes={}", bytes.len());
130 }
131 }
132
133 let separator = match source.options().get("separator") {
134 None => None,
135 Some(value) => value.as_string().cloned(),
136 };
137
138 let lowercase = match source.options().get("lowercase") {
139 None => true,
140 Some(value) => value.as_bool().unwrap_or(true),
141 };
142
143 let text = match std::str::from_utf8(bytes) {
144 Ok(value) => value,
145 Err(_) => {
146 return Err(Error::InvalidUtf8 {
147 location: Location::at(source_name, resource, None, None, None),
148 });
149 }
150 };
151 let single_line = is_single_line(bytes);
152 let mut map = Map::new();
153 let mut line_number = 0usize;
154 let mut offset = 0usize;
155 while offset < text.len() {
156 let rest = &text[offset..];
157 let line_end = match rest.find('\n') {
158 Some(index) => index,
159 None => rest.len(),
160 };
161 let line = &rest[..line_end];
162 line_number += 1;
163 let trimmed = line.trim();
164 if !trimmed.is_empty() && !trimmed.starts_with('#') {
165 let mut line_body = trimmed;
166 if line_body.starts_with("export ") {
167 line_body = line_body["export ".len()..].trim_start();
168 }
169 if let Some(equal_index) = line_body.find('=') {
170 let key = line_body[..equal_index].trim();
171 let value_part = line_body[equal_index + 1..].trim();
172 if !key.is_empty() {
173 let key_start = line.find(key).unwrap_or(0);
174 let column = line_column_from_line(line, 1, key_start);
175 let value = if value_part.starts_with('"')
176 && value_part.ends_with('"')
177 && value_part.len() >= 2
178 {
179 let inner = &value_part[1..value_part.len() - 1];
180 let mut out = String::new();
181 let mut index = 0usize;
182 while index < inner.len() {
183 let ch = inner[index..].chars().next().expect("valid utf-8");
184 let ch_len = ch.len_utf8();
185 if ch == '\\' {
186 index += ch_len;
187 if index < inner.len() {
188 let next =
189 inner[index..].chars().next().expect("valid utf-8");
190 let next_len = next.len_utf8();
191 match next {
192 'n' => out.push('\n'),
193 'r' => out.push('\r'),
194 't' => out.push('\t'),
195 '"' => out.push('"'),
196 '\\' => out.push('\\'),
197 other => {
198 out.push('\\');
199 out.push(other);
200 }
201 }
202 index += next_len;
203 } else {
204 out.push('\\');
205 }
206 } else {
207 out.push(ch);
208 index += ch_len;
209 }
210 }
211 out
212 } else if value_part.starts_with('\'')
213 && value_part.ends_with('\'')
214 && value_part.len() >= 2
215 {
216 value_part[1..value_part.len() - 1].to_string()
217 } else {
218 value_part.to_string()
219 };
220 let location = if single_line {
221 Location::at(source_name, resource, None, None, None)
222 } else {
223 Location::at(
224 source_name,
225 resource,
226 Some(line_number),
227 Some(column),
228 None,
229 )
230 };
231 let final_key = if lowercase {
232 key.to_lowercase()
233 } else {
234 key.to_string()
235 };
236 let located_value = LocatedValue {
237 value: Value::String(value),
238 location,
239 };
240 match &separator {
241 None => {
242 map.insert(final_key, located_value);
243 }
244 Some(sep) => {
245 let mut part_list: Vec<String> = Vec::new();
246 let mut remaining = final_key.as_str();
247 loop {
248 if let Some(index) = remaining.find(sep.as_str()) {
249 part_list.push(remaining[..index].to_string());
250 remaining = &remaining[index + sep.len()..];
251 } else {
252 part_list.push(remaining.to_string());
253 break;
254 }
255 }
256 if part_list.len() == 1 {
257 map.insert(part_list[0].clone(), located_value);
258 } else {
259 insert_nested(&mut map, &part_list, located_value);
260 }
261 }
262 }
263 }
264 }
265 }
266 offset += line_end;
267 if offset < text.len() {
268 offset += 1;
269 }
270 }
271 cfg_if! {
272 if #[cfg(feature = "tracing")] {
273 tracing::trace!(msg = "Parsed env-format configuration", source = source_name, resource = resource, key_count = map.len());
274 } else if #[cfg(feature = "logging")] {
275 log::trace!("msg=\"Parsed env-format configuration\" source={source_name} resource={resource} key_count={}", map.len());
276 }
277 }
278 Ok(LocatedValue {
279 value: Value::Map(map),
280 location: Location::at(source_name, resource, None, None, None),
281 })
282 }
283
284 fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
285 let text = std::str::from_utf8(bytes).ok()?;
286 for line in text.split('\n') {
287 let line = line.trim();
288 if !line.is_empty() && !line.starts_with('#') && line.contains('=') {
289 return Some(true);
290 }
291 }
292 Some(false)
293 }
294}
295
296#[cfg(all(test, feature = "env"))]
297mod tests {
298 use super::*;
299 use tanzim_source::{OptionValue, SourceBuilder};
300
301 fn file_source(resource: &str) -> Source {
302 SourceBuilder::new()
303 .with_source("file")
304 .with_resource(resource)
305 .build()
306 .unwrap()
307 }
308
309 #[test]
310 fn parses_dotenv_contents() {
311 let source = file_source(".env");
312 let parsed = Env::new().parse(&source, b"FOO=bar\nBAZ=qux\n").unwrap();
313 let map = parsed.value.as_map().unwrap();
314 assert_eq!(map.get("foo").unwrap().value.as_string().unwrap(), "bar");
315 assert_eq!(map.get("baz").unwrap().value.as_string().unwrap(), "qux");
316 }
317
318 #[test]
319 fn parses_env_with_line_numbers() {
320 let source = file_source(".env");
321 let root = Env::new().parse(&source, b"FOO=bar\nBAZ=qux\n").unwrap();
322 let map = root.value.as_map().unwrap();
323 let foo = map.get("foo").unwrap();
324 assert_eq!(foo.value.as_string().unwrap(), "bar");
325 assert_eq!(foo.location.line, std::num::NonZeroU32::new(1));
326 let baz = map.get("baz").unwrap();
327 assert_eq!(baz.location.line, std::num::NonZeroU32::new(2));
328 }
329
330 #[test]
331 fn parses_nested_keys_with_separator() {
332 let source = SourceBuilder::new()
333 .with_source("env")
334 .with_option("separator", OptionValue::String("__".into()))
335 .build()
336 .unwrap();
337 let parsed = Env::new().parse(&source, b"BAR__BAZ=val\n").unwrap();
338 let map = parsed.value.as_map().unwrap();
339 let bar = map.get("bar").unwrap();
340 let nested = bar.value.as_map().unwrap();
341 assert_eq!(nested.get("baz").unwrap().value.as_string().unwrap(), "val");
342 }
343}