Skip to main content

tanzim_parse/
env.rs

1//! Dotenv / env-file parser (`env` feature).
2//!
3//! **Format:** `env`
4//!
5//! # Example
6//!
7//! ```
8//! use tanzim_parse::{Deserialize, Env};
9//!
10//! let value = Env::new().parse("file", ".env", b"SERVER_HOST=\"127.0.0.1\"\n").unwrap();
11//! assert_eq!(
12//!     value.value.as_map().unwrap().get("SERVER_HOST").unwrap().value.as_string().unwrap(),
13//!     "127.0.0.1"
14//! );
15//! ```
16
17use crate::Deserialize;
18use crate::span::{is_single_line, line_column_from_line};
19use cfg_if::cfg_if;
20use tanzim_value::{Error, LocatedValue, Location, Map, Value};
21
22#[derive(Clone, Copy, Default)]
23pub struct Env;
24
25impl Env {
26    pub fn new() -> Self {
27        Self
28    }
29}
30
31impl Deserialize for Env {
32    fn name(&self) -> &str {
33        "Environment-Variables"
34    }
35
36    fn supported_format_list(&self) -> Vec<String> {
37        vec!["env".into()]
38    }
39
40    fn parse(&self, source: &str, resource: &str, bytes: &[u8]) -> Result<LocatedValue, Error> {
41        cfg_if! {
42            if #[cfg(feature = "tracing")] {
43                tracing::debug!(msg = "Parsing env-format configuration", source = source, resource = resource, bytes = bytes.len());
44            } else if #[cfg(feature = "logging")] {
45                log::debug!("msg=\"Parsing env-format configuration\" source={source} resource={resource} bytes={}", bytes.len());
46            }
47        }
48        let text = match std::str::from_utf8(bytes) {
49            Ok(value) => value,
50            Err(_) => {
51                return Err(Error::InvalidUtf8 {
52                    location: Location::at(source, resource, None, None, None),
53                });
54            }
55        };
56        let single_line = is_single_line(bytes);
57        let mut map = Map::new();
58        let mut line_number = 0usize;
59        let mut offset = 0usize;
60        while offset < text.len() {
61            let rest = &text[offset..];
62            let line_end = match rest.find('\n') {
63                Some(index) => index,
64                None => rest.len(),
65            };
66            let line = &rest[..line_end];
67            line_number += 1;
68            let trimmed = line.trim();
69            if !trimmed.is_empty() && !trimmed.starts_with('#') {
70                let mut line_body = trimmed;
71                if line_body.starts_with("export ") {
72                    line_body = line_body["export ".len()..].trim_start();
73                }
74                if let Some(equal_index) = line_body.find('=') {
75                    let key = line_body[..equal_index].trim();
76                    let value_part = line_body[equal_index + 1..].trim();
77                    if !key.is_empty() {
78                        let key_start = line.find(key).unwrap_or(0);
79                        let column = line_column_from_line(line, 1, key_start);
80                        let value = if value_part.starts_with('"')
81                            && value_part.ends_with('"')
82                            && value_part.len() >= 2
83                        {
84                            let inner = &value_part[1..value_part.len() - 1];
85                            let mut out = String::new();
86                            let mut index = 0usize;
87                            while index < inner.len() {
88                                let ch = inner[index..].chars().next().expect("valid utf-8");
89                                let ch_len = ch.len_utf8();
90                                if ch == '\\' {
91                                    index += ch_len;
92                                    if index < inner.len() {
93                                        let next =
94                                            inner[index..].chars().next().expect("valid utf-8");
95                                        let next_len = next.len_utf8();
96                                        match next {
97                                            'n' => out.push('\n'),
98                                            'r' => out.push('\r'),
99                                            't' => out.push('\t'),
100                                            '"' => out.push('"'),
101                                            '\\' => out.push('\\'),
102                                            other => {
103                                                out.push('\\');
104                                                out.push(other);
105                                            }
106                                        }
107                                        index += next_len;
108                                    } else {
109                                        out.push('\\');
110                                    }
111                                } else {
112                                    out.push(ch);
113                                    index += ch_len;
114                                }
115                            }
116                            out
117                        } else if value_part.starts_with('\'')
118                            && value_part.ends_with('\'')
119                            && value_part.len() >= 2
120                        {
121                            value_part[1..value_part.len() - 1].to_string()
122                        } else {
123                            value_part.to_string()
124                        };
125                        let location = if single_line {
126                            Location::at(source, resource, None, None, None)
127                        } else {
128                            Location::at(source, resource, Some(line_number), Some(column), None)
129                        };
130                        map.insert(
131                            key.to_string(),
132                            LocatedValue {
133                                value: Value::String(value),
134                                location,
135                            },
136                        );
137                    }
138                }
139            }
140            offset += line_end;
141            if offset < text.len() {
142                offset += 1;
143            }
144        }
145        cfg_if! {
146            if #[cfg(feature = "tracing")] {
147                tracing::trace!(msg = "Parsed env-format configuration", source = source, resource = resource, key_count = map.len());
148            } else if #[cfg(feature = "logging")] {
149                log::trace!("msg=\"Parsed env-format configuration\" source={source} resource={resource} key_count={}", map.len());
150            }
151        }
152        Ok(LocatedValue {
153            value: Value::Map(map),
154            location: Location::at(source, resource, None, None, None),
155        })
156    }
157
158    fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
159        let text = std::str::from_utf8(bytes).ok()?;
160        for line in text.split('\n') {
161            let line = line.trim();
162            if !line.is_empty() && !line.starts_with('#') && line.contains('=') {
163                return Some(true);
164            }
165        }
166        Some(false)
167    }
168}
169
170#[cfg(all(test, feature = "env"))]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn parses_dotenv_contents() {
176        let parsed = Env::new()
177            .parse("file", ".env", b"FOO=bar\nBAZ=qux\n")
178            .unwrap();
179        let map = parsed.value.as_map().unwrap();
180        assert_eq!(map.get("FOO").unwrap().value.as_string().unwrap(), "bar");
181        assert_eq!(map.get("BAZ").unwrap().value.as_string().unwrap(), "qux");
182    }
183
184    #[test]
185    fn parses_env_with_line_numbers() {
186        let root = Env::new()
187            .parse("file", ".env", b"FOO=bar\nBAZ=qux\n")
188            .unwrap();
189        let map = root.value.as_map().unwrap();
190        let foo = map.get("FOO").unwrap();
191        assert_eq!(foo.value.as_string().unwrap(), "bar");
192        assert_eq!(foo.location.line, std::num::NonZeroU32::new(1));
193        let baz = map.get("BAZ").unwrap();
194        assert_eq!(baz.location.line, std::num::NonZeroU32::new(2));
195    }
196}