dotenv_core/
lib.rs

1#[derive(Clone, Debug, PartialEq, Eq)]
2pub struct LineEntry {
3    pub number: usize,
4    pub raw_string: String,
5
6    /// Used in ExtraBlankLineFixer
7    pub is_deleted: bool,
8    /// Used in EndingBlankLineChecker
9    pub is_last_line: bool,
10}
11
12impl LineEntry {
13    pub fn new<T>(number: usize, raw_string: T, is_last_line: bool) -> Self
14    where
15        T: Into<String>,
16    {
17        LineEntry {
18            number,
19            raw_string: raw_string.into(),
20            is_deleted: false,
21            is_last_line,
22        }
23    }
24
25    pub fn is_empty_or_comment(&self) -> bool {
26        self.is_empty() || self.is_comment()
27    }
28
29    pub fn is_empty(&self) -> bool {
30        self.trimmed_string().is_empty()
31    }
32
33    pub fn is_comment(&self) -> bool {
34        self.trimmed_string().starts_with('#')
35    }
36
37    pub fn get_key(&self) -> Option<&str> {
38        if self.is_empty_or_comment() {
39            return None;
40        }
41
42        let stripped = self.stripped_export_string();
43        Some(stripped.split('=').next().unwrap_or(stripped))
44    }
45
46    pub fn get_value(&self) -> Option<&str> {
47        if self.is_empty_or_comment() {
48            return None;
49        }
50
51        self.raw_string
52            .find('=')
53            .map(|idx| &self.raw_string[(idx + 1)..])
54    }
55
56    fn trimmed_string(&self) -> &str {
57        self.raw_string.trim()
58    }
59
60    fn stripped_export_string(&self) -> &str {
61        let trimmed = self.trimmed_string();
62        trimmed
63            .strip_prefix("export ")
64            .map(str::trim)
65            .unwrap_or(trimmed)
66    }
67
68    pub fn mark_as_deleted(&mut self) {
69        self.is_deleted = true;
70    }
71
72    // Maybe we should add the comment field to the LineEntry struct (but this requires some
73    // refactoring of the line entries creation)
74    // pub control_comment: Option<Comment<'a>>
75    pub fn get_comment(&self) -> Option<&str> {
76        if !self.is_comment() {
77            return None;
78        }
79
80        Some(self.raw_string.as_str())
81    }
82
83    pub fn get_substitution_keys(&self) -> Vec<&str> {
84        let mut keys = Vec::new();
85
86        let mut value = match self.get_value().map(str::trim) {
87            Some(value) if !value.starts_with('\'') => value,
88            _ => return keys,
89        };
90
91        if value.starts_with('\"') {
92            if value.len() > 1 && value.ends_with('\"') && !is_escaped(&value[..value.len() - 1]) {
93                value = &value[1..value.len() - 1]
94            } else {
95                return keys;
96            }
97        }
98
99        while let Some(index) = value.find('$') {
100            let prefix = &value[..index];
101            let raw_key = &value[index + 1..];
102
103            if is_escaped(prefix) {
104                value = raw_key;
105            } else {
106                let (key, rest) = raw_key
107                    .strip_prefix('{')
108                    .and_then(|raw_key| raw_key.find('}').map(|i| raw_key.split_at(i)))
109                    .or_else(|| {
110                        raw_key
111                            .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
112                            .map(|i| raw_key.split_at(i))
113                    })
114                    .unwrap_or((raw_key, ""));
115                if key.is_empty() {
116                    return keys;
117                }
118
119                keys.push(key);
120                value = rest;
121            }
122        }
123        keys
124    }
125}
126
127pub fn is_escaped(prefix: &str) -> bool {
128    prefix.chars().rev().take_while(|ch| *ch == '\\').count() % 2 == 1
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn is_escaped_value_test() {
137        let escaped = "\\";
138        assert!(is_escaped(escaped));
139
140        let escaped = "\\\\\\";
141        assert!(is_escaped(escaped));
142
143        let non_escaped = "\\\\";
144        assert!(!is_escaped(non_escaped));
145
146        let random_string = "text without escaping";
147        assert!(!is_escaped(random_string));
148    }
149
150    pub fn line_entry(number: usize, total_lines: usize, raw_string: &str) -> LineEntry {
151        LineEntry::new(number, raw_string, total_lines == number)
152    }
153
154    mod is_empty_or_comment {
155        use super::*;
156
157        #[test]
158        fn run_with_empty_line_test() {
159            let input = line_entry(1, 1, "");
160
161            assert!(input.is_empty());
162            assert!(!input.is_comment());
163            assert!(input.is_empty_or_comment());
164        }
165
166        #[test]
167        fn run_with_comment_line_test() {
168            let input = line_entry(1, 1, "# Comment");
169
170            assert!(!input.is_empty());
171            assert!(input.is_comment());
172            assert!(input.is_empty_or_comment());
173        }
174
175        #[test]
176        fn run_with_not_comment_or_empty_line_test() {
177            let input = line_entry(1, 1, "NotComment");
178
179            assert!(!input.is_empty());
180            assert!(!input.is_comment());
181            assert!(!input.is_empty_or_comment());
182        }
183    }
184
185    mod get_key {
186        use super::*;
187
188        #[test]
189        fn empty_line_test() {
190            let input = line_entry(1, 1, "");
191            let expected = None;
192
193            assert_eq!(expected, input.get_key());
194        }
195
196        #[test]
197        fn stripped_export_prefix_test() {
198            let input = line_entry(1, 1, "export FOO=BAR");
199            let expected = Some("FOO");
200
201            assert_eq!(expected, input.get_key());
202        }
203
204        #[test]
205        fn correct_line_test() {
206            let input = line_entry(1, 1, "FOO=BAR");
207            let expected = Some("FOO");
208
209            assert_eq!(expected, input.get_key());
210        }
211
212        #[test]
213        fn line_without_value_test() {
214            let input = line_entry(1, 1, "FOO=");
215            let expected = Some("FOO");
216
217            assert_eq!(expected, input.get_key());
218        }
219
220        #[test]
221        fn missing_value_and_equal_sign_test() {
222            let input = line_entry(1, 1, "FOOBAR");
223            let expected = Some("FOOBAR");
224
225            assert_eq!(expected, input.get_key());
226        }
227    }
228
229    mod get_value {
230        use super::*;
231
232        #[test]
233        fn empty_line_test() {
234            let input = line_entry(1, 1, "");
235            let expected = None;
236
237            assert_eq!(expected, input.get_value());
238        }
239
240        #[test]
241        fn correct_line_test() {
242            let input = line_entry(1, 1, "FOO=BAR");
243            let expected = Some("BAR");
244
245            assert_eq!(expected, input.get_value());
246        }
247
248        #[test]
249        fn correct_line_with_single_quote_test() {
250            let input = line_entry(1, 1, "FOO='BAR'");
251            let expected = Some("'BAR'");
252
253            assert_eq!(expected, input.get_value());
254        }
255
256        #[test]
257        fn correct_line_with_double_quote_test() {
258            let input = line_entry(1, 1, "FOO=\"BAR\"");
259            let expected = Some("\"BAR\"");
260
261            assert_eq!(expected, input.get_value());
262        }
263
264        #[test]
265        fn line_without_key_test() {
266            let input = line_entry(1, 1, "=BAR");
267            let expected = Some("BAR");
268
269            assert_eq!(expected, input.get_value());
270        }
271
272        #[test]
273        fn line_without_value_test() {
274            let input = line_entry(1, 1, "FOO=");
275            let expected = Some("");
276
277            assert_eq!(expected, input.get_value());
278        }
279
280        #[test]
281        fn missing_value_and_equal_sign_test() {
282            let input = line_entry(1, 1, "FOOBAR");
283            let expected = None;
284
285            assert_eq!(expected, input.get_value());
286        }
287    }
288
289    mod trimmed_string {
290        use super::*;
291
292        #[test]
293        fn line_without_blank_chars_test() {
294            let entry = line_entry(1, 1, "FOO=BAR");
295
296            assert_eq!("FOO=BAR", entry.trimmed_string());
297        }
298
299        #[test]
300        fn line_with_spaces_test() {
301            let entry = line_entry(1, 1, "   FOO=BAR  ");
302
303            assert_eq!("FOO=BAR", entry.trimmed_string());
304        }
305
306        #[test]
307        fn line_with_tab_test() {
308            let entry = line_entry(1, 1, "FOO=BAR\t");
309
310            assert_eq!("FOO=BAR", entry.trimmed_string());
311        }
312    }
313
314    mod get_comment {
315        use super::*;
316
317        #[test]
318        fn line_with_comment_test() {
319            let entry = line_entry(1, 1, "# dotenv-linter:off LowercaseKey");
320            let comment = entry.get_comment();
321            assert!(comment.is_some());
322
323            // TODO: Update later
324            // let comment = entry.get_comment().expect("comment");
325            // assert_eq!(comment.is_disabled(), true);
326            // assert_eq!(comment.checks, vec![LintKind::LowercaseKey]);
327        }
328
329        #[test]
330        fn line_with_no_comment_test() {
331            let entry = line_entry(1, 1, "A=B");
332            let comment = entry.get_comment();
333            assert!(comment.is_none());
334        }
335    }
336
337    mod get_substitution_keys {
338        use super::*;
339
340        #[test]
341        fn run_with_empty() {
342            let input = line_entry(1, 1, "");
343            assert!(input.get_substitution_keys().is_empty());
344        }
345
346        #[test]
347        fn run_with_simple() {
348            let input = line_entry(1, 1, "FOO=$BAR");
349            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
350        }
351
352        #[test]
353        fn run_with_simple_comment() {
354            let input = line_entry(1, 1, "FOO=$BAR # comment");
355            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
356        }
357
358        #[test]
359        fn run_with_curly_braces() {
360            let input = line_entry(1, 1, "FOO=${BAR}");
361            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
362
363            let input = line_entry(1, 1, "FOO=$BAR}");
364            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
365
366            let input = line_entry(1, 1, "FOO=${BAR");
367            assert!(input.get_substitution_keys().is_empty());
368        }
369
370        #[test]
371        fn run_with_double_quotes() {
372            let input = line_entry(1, 1, r#"FOO="$BAR""#);
373            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
374
375            let input = line_entry(1, 1, r#"FOO=$BAR""#);
376            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
377
378            let input = line_entry(1, 1, r#"FOO="$BAR"#);
379            assert!(input.get_substitution_keys().is_empty());
380
381            let input = line_entry(1, 1, r#"FOO="$BAR\""#);
382            assert!(input.get_substitution_keys().is_empty());
383
384            let input = line_entry(1, 1, r#"FOO="\""#);
385            assert!(input.get_substitution_keys().is_empty());
386
387            let input = line_entry(1, 1, r#"FOO="${BAR}\\""#);
388            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
389        }
390
391        #[test]
392        fn run_with_single_quotes() {
393            let input = line_entry(1, 1, "FOO='$BAR'");
394            assert!(input.get_substitution_keys().is_empty());
395
396            let input = line_entry(1, 1, r"FOO=TEST_${BAR}_\'");
397            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
398        }
399
400        #[test]
401        fn run_with_escaped_dollar() {
402            let input = line_entry(1, 1, r"FOO=\$BAR");
403            assert!(input.get_substitution_keys().is_empty());
404
405            let input = line_entry(1, 1, r"FOO=\\$BAR");
406            assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
407
408            let input = line_entry(1, 1, r"FOO=\\\$BAR");
409            assert!(input.get_substitution_keys().is_empty());
410        }
411
412        #[test]
413        fn run_with_complicated() {
414            let input = line_entry(1, 1, "DATABASE=postgres://${USER}@localhost/database");
415            assert_eq!(input.get_substitution_keys(), vec!["USER"]);
416        }
417
418        #[test]
419        fn run_with_reused() {
420            let input = line_entry(1, 1, "FOO=$BAR$BAR");
421            assert_eq!(input.get_substitution_keys(), vec!["BAR", "BAR"]);
422
423            let input = line_entry(1, 1, "FOO=${BAR}${BAR}");
424            assert_eq!(input.get_substitution_keys(), vec!["BAR", "BAR"]);
425
426            let input = line_entry(1, 1, "FOO=${BAR}${BAZ}");
427            assert_eq!(input.get_substitution_keys(), vec!["BAR", "BAZ"]);
428        }
429
430        #[test]
431        fn run_with_break() {
432            let input = line_entry(1, 1, "FOO=${BAR $BAZ");
433            assert!(input.get_substitution_keys().is_empty());
434        }
435    }
436}