darkweb_dotenv/
dotenv.rs

1// Copyright (c) 2020 DarkWeb Design
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
11// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
12// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
13// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
15// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
16// SOFTWARE.
17
18use std::{env, fs};
19use std::collections::HashMap;
20
21use regex::Regex;
22
23use crate::Exception;
24
25/// Dotenv file loader
26pub 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    ///
40    /// Creates a new instance of the Dotenv file loader.
41    ///
42    /// # Examples
43    ///
44    /// ```dotenv
45    /// # .env
46    /// DB_USER=root
47    /// DB_PASS=pass
48    /// ```
49    ///
50    /// ```rust
51    /// use darkweb_dotenv::Dotenv;
52    ///
53    /// let mut dotenv = Dotenv::new();
54    /// dotenv.load(".env").unwrap();
55    ///
56    /// let db_user = std::env::var("DB_USER").unwrap();
57    /// ```
58    ///
59    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    ///
71    /// Loads environment variables from file a `.env` file.
72    ///
73    /// # Examples
74    ///
75    /// ```rust
76    /// use darkweb_dotenv::Dotenv;
77    ///
78    /// let mut dotenv = Dotenv::new();
79    /// dotenv.load(".env").unwrap();
80    /// ```
81    ///
82    /// # Exceptions
83    ///
84    /// * `Exception::FormatException`
85    /// * `Exception::PathException`
86    ///
87    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    ///
102    /// Loads environment variables from a `.env` file and overwrites exiting environment variables.
103    ///
104    /// # Examples
105    ///
106    /// ```rust
107    /// use darkweb_dotenv::Dotenv;
108    ///
109    /// let mut dotenv = Dotenv::new();
110    /// dotenv.overload(".env").unwrap();
111    /// ```
112    ///
113    /// # Exceptions
114    ///
115    /// * `Exception::FormatException`
116    /// * `Exception::PathException`
117    ///
118    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    ///
133    /// Loads environment-specific environment variables from multiple `.env` files in an hierarchy.
134    ///
135    /// The following files are loaded if they exist, the latter taking precedence over the former:
136    /// * `.env` --> committed environment defaults
137    /// * `.env.local` --> uncommitted file with local overrides
138    /// * `.env.{APP_ENV}` --> committed environment-specific defaults
139    /// * `.env.{APP_ENV}.local` --> uncommitted environment-specific local overrides
140    ///
141    /// # Examples
142    ///
143    /// ```rust
144    /// use darkweb_dotenv::Dotenv;
145    ///
146    /// let mut dotenv = Dotenv::new();
147    /// dotenv.load_env(".env", "APP_ENV", "dev").unwrap();
148    /// ```
149    ///
150    /// # Exceptions
151    ///
152    /// * `Exception::FormatException`
153    /// * `Exception::PathException`
154    ///
155    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(&regex_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(&regex_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(&regex_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}