1use std::{env, fs};
19use std::collections::HashMap;
20
21use regex::Regex;
22
23use crate::Exception;
24
25pub struct Dotenv {
27 path: String,
28 data: String,
29 line_number: usize,
30 cursor: usize,
31 end: usize,
32 state: usize,
33}
34
35impl Dotenv {
36 const STATE_VARNAME: usize = 0;
37 const STATE_VALUE: usize = 1;
38
39 pub fn new() -> Self {
60 Self {
61 path: "".to_string(),
62 data: "".to_string(),
63 line_number: 0,
64 cursor: 0,
65 end: 0,
66 state: Self::STATE_VARNAME,
67 }
68 }
69
70 pub fn load<Path>(&mut self, path: Path) -> Result<(), Exception>
88 where
89 Path: AsRef<str> {
90
91 let path = path.as_ref().to_string();
92 let data = self.read_file(&path)?;
93
94 let values = self.parse(data, path)?;
95
96 self.populate(&values, false);
97
98 Ok(())
99 }
100
101 pub fn overload<Path>(&mut self, path: Path) -> Result<(), Exception>
119 where
120 Path: AsRef<str> {
121
122 let path = path.as_ref().to_string();
123 let data = self.read_file(&path)?;
124
125 let values = self.parse(data, path)?;
126
127 self.populate(&values, true);
128
129 Ok(())
130 }
131
132 pub fn load_env<Path, EnvKey, DefaultEnv>(&mut self, path: Path, env_key: EnvKey, default_env: DefaultEnv) -> Result<(), Exception>
156 where
157 Path: AsRef<str>,
158 EnvKey: AsRef<str>,
159 DefaultEnv: AsRef<str> {
160
161 let path = path.as_ref().to_string();
162 let env_key = env_key.as_ref().to_string();
163 let default_env = default_env.as_ref().to_string();
164
165 let mut values = HashMap::new();
166
167 if let Ok(data) = self.read_file(&path) {
168 values.extend(self.parse(data, &path)?)
169 }
170
171 let local_path = format!("{}.local", path);
172
173 if let Ok(data) = self.read_file(&local_path) {
174 values.extend(self.parse(data, local_path)?)
175 }
176
177 self.populate(&values, false);
178 values.clear();
179
180 let env = match env::var_os(env_key) {
181 Some(value) => value.to_string_lossy().to_string(),
182 None => default_env,
183 };
184
185 if &env == "local" {
186 return Ok(());
187 }
188
189 let env_path = format!("{}.{}", path, env);
190
191 if let Ok(data) = self.read_file(&env_path) {
192 values.extend(self.parse(data, env_path)?)
193 }
194
195 let env_local_path = format!("{}.{}.local", path, env);
196
197 if let Ok(data) = self.read_file(&env_local_path) {
198 values.extend(self.parse(data, env_local_path)?)
199 }
200
201 self.populate(&values, false);
202
203 Ok(())
204 }
205
206 fn read_file<Path>(&mut self, path: Path) -> Result<String, Exception>
207 where
208 Path: AsRef<str> {
209
210 let path = path.as_ref();
211
212 match fs::read_to_string(path) {
213 Ok(data) => Ok(data),
214 Err(_) => Err(Exception::PathException(path.to_string())),
215 }
216 }
217
218 fn parse<Data, Path>(&mut self, data: Data, path: Path) -> Result<HashMap<String, String>, Exception>
219 where
220 Data: AsRef<str>,
221 Path: AsRef<str> {
222
223 self.path = path.as_ref().to_string();
224 self.data = data.as_ref().replace("\r\n", "\n");
225 self.line_number = 1;
226 self.cursor = 0;
227 self.end = self.data.len();
228 self.state = Self::STATE_VARNAME;
229
230 let mut values = HashMap::new();
231
232 let mut name = "".to_string();
233
234 self.skip_empty_lines();
235
236 while self.cursor < self.end {
237 match self.state {
238 Self::STATE_VARNAME => {
239 name = self.lex_varname()?;
240 self.state = Self::STATE_VALUE;
241 },
242 Self::STATE_VALUE => {
243 let value = self.lex_value()?;
244 values.insert(name.clone(), value);
245 self.state = Self::STATE_VARNAME;
246 },
247 _ => unreachable!("invalid state"),
248 }
249 }
250
251 if self.state == Self::STATE_VALUE {
252 values.insert(name.clone(), "".to_string());
253 }
254
255 Ok(values)
256 }
257
258 fn lex_varname(&mut self) -> Result<String, Exception> {
259 let regex = Regex::new(r"^(export[ \t]++)?((?i:[A-Z][A-Z0-9_]*+))").unwrap();
260 let regex_value = self.data.clone().chars().skip(self.cursor).collect::<String>();
261 let regex_captures = regex.captures(®ex_value);
262
263 if regex_captures.is_none() {
264 return Err(self.create_format_exception("Invalid character in variable name".to_string()));
265 }
266
267 let captures = regex_captures.unwrap();
268
269 self.move_cursor(&captures[0].to_string());
270
271 let token = &self.get_token();
272
273 if self.cursor == self.end || token == "\n" || token == "#" {
274 if captures.get(1).is_some() {
275 return Err(self.create_format_exception("Unable to unset an environment variable".to_string()));
276 }
277
278 return Err(self.create_format_exception("Missing = in the environment variable declaration".to_string()));
279 }
280
281 if token == " " || token == "\t" {
282 return Err(self.create_format_exception("Whitespace characters are not supported after the variable name".to_string()));
283 }
284
285 if token != "=" {
286 return Err(self.create_format_exception("Missing = in the environment variable declaration".to_string()));
287 }
288
289 self.cursor += 1;
290
291 Ok(captures[2].to_string())
292 }
293
294 fn lex_value(&mut self) -> Result<String, Exception> {
295 let regex = Regex::new(r"^[ \t]*+(?:#.*)?$").unwrap();
296 let regex_value = self.data.clone().chars().skip(self.cursor).collect::<String>();
297 let regex_match = regex.find(®ex_value);
298
299 if regex_match.is_some() {
300 self.move_cursor(regex_match.unwrap().as_str());
301 self.skip_empty_lines();
302
303 return Ok("".to_string());
304 }
305
306 if &self.get_token() == " " || &self.get_token() == "\t" {
307 return Err(self.create_format_exception("Whitespace are not supported before the value".to_string()));
308 }
309
310 let mut value = "".to_string();
311
312 loop {
313 if &self.get_token() == "'" {
314 let mut len = 0;
315
316 loop {
317 len += 1;
318 if self.cursor + len == self.end {
319 self.cursor += len;
320
321 return Err(self.create_format_exception("Missing quote to end the value".to_string()));
322 }
323
324 if &self.get_token_at(self.cursor + len) == "'" {
325 break;
326 }
327 }
328
329 value = format!("{}{}", value, self.data.chars().skip(self.cursor + 1).take(len - 1).collect::<String>());
330 self.cursor += 1 + len;
331 } else if &self.get_token() == "\"" {
332 let mut len = 0;
333
334 loop {
335 len += 1;
336 if self.cursor + len == self.end {
337 self.cursor += len;
338
339 return Err(self.create_format_exception("Missing quote to end the value".to_string()));
340 }
341
342 if &self.get_token_at(self.cursor + len) == "\"" && &self.get_token_at(self.cursor + len - 1) != "\\" && &self.get_token_at(self.cursor + len - 2) != "\"" {
343 break;
344 }
345 }
346
347 let mut resolved_value = format!("{}{}", value, self.data.chars().skip(self.cursor + 1).take(len - 1).collect::<String>());
348 resolved_value = resolved_value.replace("\\\"", "\"");
349 resolved_value = resolved_value.replace("\\r", "\r");
350 resolved_value = resolved_value.replace("\\n", "\n");
351 resolved_value = resolved_value.replace("\\\\", "\\");
352
353 value = format!("{}{}", value, resolved_value);
354 self.cursor += 1 + len;
355 } else {
356 let mut resolved_value = "".to_string();
357 let mut previous_character = self.get_token_at(self.cursor - 1);
358
359 loop {
360 if self.cursor == self.end || self.get_token() == "\n" || self.get_token() == "\"" || self.get_token() == "'" || ((previous_character == " " || previous_character == "\t") && self.get_token() == "#") {
361 break;
362 }
363
364 if self.get_token() == "\\" && self.cursor + 1 < self.end && (self.get_token_at(self.cursor + 1) == "\"" || self.get_token_at(self.cursor + 1) == "'") {
365 self.cursor += 1;
366 }
367
368 previous_character = self.get_token();
369 resolved_value = format!("{}{}", resolved_value, previous_character);
370
371 self.cursor += 1;
372 }
373
374 resolved_value = resolved_value.trim_end().to_string();
375 resolved_value = resolved_value.replace("\\\\", "\\");
376
377 if resolved_value.contains(" ") || resolved_value.contains("\t") {
378 return Err(self.create_format_exception("A value containing spaces must be surrounded by quotes".to_string()));
379 }
380
381 value = format!("{}{}", value, resolved_value);
382
383 if self.cursor < self.end && self.get_token() == "#" {
384 break;
385 }
386 }
387
388 if self.cursor == self.end || &self.get_token() == "\n" {
389 break;
390 }
391 }
392
393 self.skip_empty_lines();
394
395 Ok(value.to_string())
396 }
397
398 fn skip_empty_lines(&mut self) {
399 let regex = Regex::new(r"^(?:\s*+(?:#[^\n]*+)?+)++").unwrap();
400 let regex_value = self.data.clone().chars().skip(self.cursor).collect::<String>();
401
402 if let Some(regex_match) = regex.find(®ex_value) {
403 self.move_cursor(regex_match.as_str());
404 }
405 }
406
407 fn move_cursor(&mut self, text: &str) {
408 self.cursor += text.len();
409 self.line_number += text.matches("\n").count();
410 }
411
412 fn get_token(&self) -> String {
413 self.get_token_at(self.cursor)
414 }
415
416 fn get_token_at(&self, position: usize) -> String {
417 self.data.chars().skip(position).take(1).collect::<String>()
418 }
419
420 fn create_format_exception(&self, message: String) -> Exception {
421 Exception::FormatException(message, self.path.clone(), self.line_number)
422 }
423
424 fn populate(&self, values: &HashMap<String, String>, override_existing: bool) {
425 for (key, value) in values.iter() {
426 if override_existing && env::var_os(key).is_some() {
427 continue;
428 }
429 env::set_var(key, value);
430 }
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use crate::Dotenv;
437
438 #[test]
439 fn parse_no_quotes() {
440 let mut dotenv = Dotenv::new();
441 let values = dotenv.parse("FOO=bar", ".env").unwrap();
442 assert_eq!(values.get("FOO").unwrap(), "bar");
443 }
444
445 #[test]
446 fn parse_single_quotes() {
447 let mut dotenv = Dotenv::new();
448 let values = dotenv.parse("FOO='bar'", ".env").unwrap();
449 assert_eq!(values.get("FOO").unwrap(), "bar");
450 }
451
452 #[test]
453 fn parse_single_quotes_concatenation() {
454 let mut dotenv = Dotenv::new();
455 let values = dotenv.parse("FOO='bar'\\''baz'", ".env").unwrap();
456 assert_eq!(values.get("FOO").unwrap(), "bar'baz");
457 }
458
459 #[test]
460 fn parse_double_quotes() {
461 let mut dotenv = Dotenv::new();
462 let values = dotenv.parse("FOO=\"bar\"", ".env").unwrap();
463 assert_eq!(values.get("FOO").unwrap(), "bar");
464 }
465
466 #[test]
467 fn parse_double_quotes_escaped_quotes() {
468 let mut dotenv = Dotenv::new();
469 let values = dotenv.parse("FOO=\"bar\\\"baz\"", ".env").unwrap();
470 assert_eq!(values.get("FOO").unwrap(), "bar\"baz");
471 }
472
473 #[test]
474 fn parse_double_quotes_newlines() {
475 let mut dotenv = Dotenv::new();
476 let values = dotenv.parse("FOO=\"bar\\r\\nbaz\"", ".env").unwrap();
477 assert_eq!(values.get("FOO").unwrap(), "bar\r\nbaz");
478 }
479
480 #[test]
481 fn parse_double_quotes_slashes() {
482 let mut dotenv = Dotenv::new();
483 let values = dotenv.parse("FOO=\"bar\\\\baz\"", ".env").unwrap();
484 assert_eq!(values.get("FOO").unwrap(), "bar\\baz");
485 }
486
487 #[test]
488 fn parse_export() {
489 let mut dotenv = Dotenv::new();
490 let values = dotenv.parse("export FOO=bar", ".env").unwrap();
491 assert_eq!(values.get("FOO").unwrap(), "bar");
492 }
493}