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