Coverage Report

Created: 2026-03-31 07:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/dotenvy-0.15.7/src/parse.rs
Line
Count
Source
1
use std::collections::HashMap;
2
use std::env;
3
4
use crate::errors::*;
5
6
// for readability's sake
7
pub type ParsedLine = Result<Option<(String, String)>>;
8
9
0
pub fn parse_line(
10
0
    line: &str,
11
0
    substitution_data: &mut HashMap<String, Option<String>>,
12
0
) -> ParsedLine {
13
0
    let mut parser = LineParser::new(line, substitution_data);
14
0
    parser.parse_line()
15
0
}
16
17
struct LineParser<'a> {
18
    original_line: &'a str,
19
    substitution_data: &'a mut HashMap<String, Option<String>>,
20
    line: &'a str,
21
    pos: usize,
22
}
23
24
impl<'a> LineParser<'a> {
25
0
    fn new(
26
0
        line: &'a str,
27
0
        substitution_data: &'a mut HashMap<String, Option<String>>,
28
0
    ) -> LineParser<'a> {
29
0
        LineParser {
30
0
            original_line: line,
31
0
            substitution_data,
32
0
            line: line.trim_end(), // we don’t want trailing whitespace
33
0
            pos: 0,
34
0
        }
35
0
    }
36
37
0
    fn err(&self) -> Error {
38
0
        Error::LineParse(self.original_line.into(), self.pos)
39
0
    }
40
41
0
    fn parse_line(&mut self) -> ParsedLine {
42
0
        self.skip_whitespace();
43
        // if its an empty line or a comment, skip it
44
0
        if self.line.is_empty() || self.line.starts_with('#') {
45
0
            return Ok(None);
46
0
        }
47
48
0
        let mut key = self.parse_key()?;
49
0
        self.skip_whitespace();
50
51
        // export can be either an optional prefix or a key itself
52
0
        if key == "export" {
53
            // here we check for an optional `=`, below we throw directly when it’s not found.
54
0
            if self.expect_equal().is_err() {
55
0
                key = self.parse_key()?;
56
0
                self.skip_whitespace();
57
0
                self.expect_equal()?;
58
0
            }
59
        } else {
60
0
            self.expect_equal()?;
61
        }
62
0
        self.skip_whitespace();
63
64
0
        if self.line.is_empty() || self.line.starts_with('#') {
65
0
            self.substitution_data.insert(key.clone(), None);
66
0
            return Ok(Some((key, String::new())));
67
0
        }
68
69
0
        let parsed_value = parse_value(self.line, self.substitution_data)?;
70
0
        self.substitution_data
71
0
            .insert(key.clone(), Some(parsed_value.clone()));
72
73
0
        Ok(Some((key, parsed_value)))
74
0
    }
75
76
0
    fn parse_key(&mut self) -> Result<String> {
77
0
        if !self
78
0
            .line
79
0
            .starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
80
        {
81
0
            return Err(self.err());
82
0
        }
83
0
        let index = match self
84
0
            .line
85
0
            .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.'))
86
        {
87
0
            Some(index) => index,
88
0
            None => self.line.len(),
89
        };
90
0
        self.pos += index;
91
0
        let key = String::from(&self.line[..index]);
92
0
        self.line = &self.line[index..];
93
0
        Ok(key)
94
0
    }
95
96
0
    fn expect_equal(&mut self) -> Result<()> {
97
0
        if !self.line.starts_with('=') {
98
0
            return Err(self.err());
99
0
        }
100
0
        self.line = &self.line[1..];
101
0
        self.pos += 1;
102
0
        Ok(())
103
0
    }
104
105
0
    fn skip_whitespace(&mut self) {
106
0
        if let Some(index) = self.line.find(|c: char| !c.is_whitespace()) {
107
0
            self.pos += index;
108
0
            self.line = &self.line[index..];
109
0
        } else {
110
0
            self.pos += self.line.len();
111
0
            self.line = "";
112
0
        }
113
0
    }
114
}
115
116
#[derive(Eq, PartialEq)]
117
enum SubstitutionMode {
118
    None,
119
    Block,
120
    EscapedBlock,
121
}
122
123
0
fn parse_value(
124
0
    input: &str,
125
0
    substitution_data: &mut HashMap<String, Option<String>>,
126
0
) -> Result<String> {
127
0
    let mut strong_quote = false; // '
128
0
    let mut weak_quote = false; // "
129
0
    let mut escaped = false;
130
0
    let mut expecting_end = false;
131
132
    //FIXME can this be done without yet another allocation per line?
133
0
    let mut output = String::new();
134
135
0
    let mut substitution_mode = SubstitutionMode::None;
136
0
    let mut substitution_name = String::new();
137
138
0
    for (index, c) in input.chars().enumerate() {
139
        //the regex _should_ already trim whitespace off the end
140
        //expecting_end is meant to permit: k=v #comment
141
        //without affecting: k=v#comment
142
        //and throwing on: k=v w
143
0
        if expecting_end {
144
0
            if c == ' ' || c == '\t' {
145
0
                continue;
146
0
            } else if c == '#' {
147
0
                break;
148
            } else {
149
0
                return Err(Error::LineParse(input.to_owned(), index));
150
            }
151
0
        } else if escaped {
152
            //TODO I tried handling literal \r but various issues
153
            //imo not worth worrying about until there's a use case
154
            //(actually handling backslash 0x10 would be a whole other matter)
155
            //then there's \v \f bell hex... etc
156
0
            match c {
157
0
                '\\' | '\'' | '"' | '$' | ' ' => output.push(c),
158
0
                'n' => output.push('\n'), // handle \n case
159
                _ => {
160
0
                    return Err(Error::LineParse(input.to_owned(), index));
161
                }
162
            }
163
164
0
            escaped = false;
165
0
        } else if strong_quote {
166
0
            if c == '\'' {
167
0
                strong_quote = false;
168
0
            } else {
169
0
                output.push(c);
170
0
            }
171
0
        } else if substitution_mode != SubstitutionMode::None {
172
0
            if c.is_alphanumeric() {
173
0
                substitution_name.push(c);
174
0
            } else {
175
0
                match substitution_mode {
176
0
                    SubstitutionMode::None => unreachable!(),
177
                    SubstitutionMode::Block => {
178
0
                        if c == '{' && substitution_name.is_empty() {
179
0
                            substitution_mode = SubstitutionMode::EscapedBlock;
180
0
                        } else {
181
0
                            apply_substitution(
182
0
                                substitution_data,
183
0
                                &substitution_name.drain(..).collect::<String>(),
184
0
                                &mut output,
185
                            );
186
0
                            if c == '$' {
187
0
                                substitution_mode = if !strong_quote && !escaped {
188
0
                                    SubstitutionMode::Block
189
                                } else {
190
0
                                    SubstitutionMode::None
191
                                }
192
0
                            } else {
193
0
                                substitution_mode = SubstitutionMode::None;
194
0
                                output.push(c);
195
0
                            }
196
                        }
197
                    }
198
                    SubstitutionMode::EscapedBlock => {
199
0
                        if c == '}' {
200
0
                            substitution_mode = SubstitutionMode::None;
201
0
                            apply_substitution(
202
0
                                substitution_data,
203
0
                                &substitution_name.drain(..).collect::<String>(),
204
0
                                &mut output,
205
0
                            );
206
0
                        } else {
207
0
                            substitution_name.push(c);
208
0
                        }
209
                    }
210
                }
211
            }
212
0
        } else if c == '$' {
213
0
            substitution_mode = if !strong_quote && !escaped {
214
0
                SubstitutionMode::Block
215
            } else {
216
0
                SubstitutionMode::None
217
            }
218
0
        } else if weak_quote {
219
0
            if c == '"' {
220
0
                weak_quote = false;
221
0
            } else if c == '\\' {
222
0
                escaped = true;
223
0
            } else {
224
0
                output.push(c);
225
0
            }
226
0
        } else if c == '\'' {
227
0
            strong_quote = true;
228
0
        } else if c == '"' {
229
0
            weak_quote = true;
230
0
        } else if c == '\\' {
231
0
            escaped = true;
232
0
        } else if c == ' ' || c == '\t' {
233
0
            expecting_end = true;
234
0
        } else {
235
0
            output.push(c);
236
0
        }
237
    }
238
239
    //XXX also fail if escaped? or...
240
0
    if substitution_mode == SubstitutionMode::EscapedBlock || strong_quote || weak_quote {
241
0
        let value_length = input.len();
242
        Err(Error::LineParse(
243
0
            input.to_owned(),
244
0
            if value_length == 0 {
245
0
                0
246
            } else {
247
0
                value_length - 1
248
            },
249
        ))
250
    } else {
251
0
        apply_substitution(
252
0
            substitution_data,
253
0
            &substitution_name.drain(..).collect::<String>(),
254
0
            &mut output,
255
        );
256
0
        Ok(output)
257
    }
258
0
}
259
260
0
fn apply_substitution(
261
0
    substitution_data: &mut HashMap<String, Option<String>>,
262
0
    substitution_name: &str,
263
0
    output: &mut String,
264
0
) {
265
0
    if let Ok(environment_value) = env::var(substitution_name) {
266
0
        output.push_str(&environment_value);
267
0
    } else {
268
0
        let stored_value = substitution_data
269
0
            .get(substitution_name)
270
0
            .unwrap_or(&None)
271
0
            .to_owned();
272
0
        output.push_str(&stored_value.unwrap_or_default());
273
0
    };
274
0
}
275
276
#[cfg(test)]
277
mod test {
278
    use crate::iter::Iter;
279
280
    use super::*;
281
282
    #[test]
283
    fn test_parse_line_env() {
284
        // Note 5 spaces after 'KEY8=' below
285
        let actual_iter = Iter::new(
286
            r#"
287
KEY=1
288
KEY2="2"
289
KEY3='3'
290
KEY4='fo ur'
291
KEY5="fi ve"
292
KEY6=s\ ix
293
KEY7=
294
KEY8=     
295
KEY9=   # foo
296
KEY10  ="whitespace before ="
297
KEY11=    "whitespace after ="
298
export="export as key"
299
export   SHELL_LOVER=1
300
"#
301
            .as_bytes(),
302
        );
303
304
        let expected_iter = vec![
305
            ("KEY", "1"),
306
            ("KEY2", "2"),
307
            ("KEY3", "3"),
308
            ("KEY4", "fo ur"),
309
            ("KEY5", "fi ve"),
310
            ("KEY6", "s ix"),
311
            ("KEY7", ""),
312
            ("KEY8", ""),
313
            ("KEY9", ""),
314
            ("KEY10", "whitespace before ="),
315
            ("KEY11", "whitespace after ="),
316
            ("export", "export as key"),
317
            ("SHELL_LOVER", "1"),
318
        ]
319
        .into_iter()
320
        .map(|(key, value)| (key.to_string(), value.to_string()));
321
322
        let mut count = 0;
323
        for (expected, actual) in expected_iter.zip(actual_iter) {
324
            assert!(actual.is_ok());
325
            assert_eq!(expected, actual.unwrap());
326
            count += 1;
327
        }
328
329
        assert_eq!(count, 13);
330
    }
331
332
    #[test]
333
    fn test_parse_line_comment() {
334
        let result: Result<Vec<(String, String)>> = Iter::new(
335
            r#"
336
# foo=bar
337
#    "#
338
                .as_bytes(),
339
        )
340
        .collect();
341
        assert!(result.unwrap().is_empty());
342
    }
343
344
    #[test]
345
    fn test_parse_line_invalid() {
346
        // Note 4 spaces after 'invalid' below
347
        let actual_iter = Iter::new(
348
            r#"
349
  invalid    
350
very bacon = yes indeed
351
=value"#
352
                .as_bytes(),
353
        );
354
355
        let mut count = 0;
356
        for actual in actual_iter {
357
            assert!(actual.is_err());
358
            count += 1;
359
        }
360
        assert_eq!(count, 3);
361
    }
362
363
    #[test]
364
    fn test_parse_value_escapes() {
365
        let actual_iter = Iter::new(
366
            r#"
367
KEY=my\ cool\ value
368
KEY2=\$sweet
369
KEY3="awesome stuff \"mang\""
370
KEY4='sweet $\fgs'\''fds'
371
KEY5="'\"yay\\"\ "stuff"
372
KEY6="lol" #well you see when I say lol wh
373
KEY7="line 1\nline 2"
374
"#
375
            .as_bytes(),
376
        );
377
378
        let expected_iter = vec![
379
            ("KEY", r#"my cool value"#),
380
            ("KEY2", r#"$sweet"#),
381
            ("KEY3", r#"awesome stuff "mang""#),
382
            ("KEY4", r#"sweet $\fgs'fds"#),
383
            ("KEY5", r#"'"yay\ stuff"#),
384
            ("KEY6", "lol"),
385
            ("KEY7", "line 1\nline 2"),
386
        ]
387
        .into_iter()
388
        .map(|(key, value)| (key.to_string(), value.to_string()));
389
390
        for (expected, actual) in expected_iter.zip(actual_iter) {
391
            assert!(actual.is_ok());
392
            assert_eq!(expected, actual.unwrap());
393
        }
394
    }
395
396
    #[test]
397
    fn test_parse_value_escapes_invalid() {
398
        let actual_iter = Iter::new(
399
            r#"
400
KEY=my uncool value
401
KEY2="why
402
KEY3='please stop''
403
KEY4=h\8u
404
"#
405
            .as_bytes(),
406
        );
407
408
        for actual in actual_iter {
409
            assert!(actual.is_err());
410
        }
411
    }
412
}
413
414
#[cfg(test)]
415
mod variable_substitution_tests {
416
    use crate::iter::Iter;
417
    use std::env;
418
419
    fn assert_parsed_string(input_string: &str, expected_parse_result: Vec<(&str, &str)>) {
420
        let actual_iter = Iter::new(input_string.as_bytes());
421
        let expected_count = &expected_parse_result.len();
422
423
        let expected_iter = expected_parse_result
424
            .into_iter()
425
            .map(|(key, value)| (key.to_string(), value.to_string()));
426
427
        let mut count = 0;
428
        for (expected, actual) in expected_iter.zip(actual_iter) {
429
            assert!(actual.is_ok());
430
            assert_eq!(expected, actual.unwrap());
431
            count += 1;
432
        }
433
434
        assert_eq!(count, *expected_count);
435
    }
436
437
    #[test]
438
    fn variable_in_parenthesis_surrounded_by_quotes() {
439
        assert_parsed_string(
440
            r#"
441
            KEY=test
442
            KEY1="${KEY}"
443
            "#,
444
            vec![("KEY", "test"), ("KEY1", "test")],
445
        );
446
    }
447
448
    #[test]
449
    fn substitute_undefined_variables_to_empty_string() {
450
        assert_parsed_string(r#"KEY=">$KEY1<>${KEY2}<""#, vec![("KEY", "><><")]);
451
    }
452
453
    #[test]
454
    fn do_not_substitute_variables_with_dollar_escaped() {
455
        assert_parsed_string(
456
            "KEY=>\\$KEY1<>\\${KEY2}<",
457
            vec![("KEY", ">$KEY1<>${KEY2}<")],
458
        );
459
    }
460
461
    #[test]
462
    fn do_not_substitute_variables_in_weak_quotes_with_dollar_escaped() {
463
        assert_parsed_string(
464
            r#"KEY=">\$KEY1<>\${KEY2}<""#,
465
            vec![("KEY", ">$KEY1<>${KEY2}<")],
466
        );
467
    }
468
469
    #[test]
470
    fn do_not_substitute_variables_in_strong_quotes() {
471
        assert_parsed_string("KEY='>${KEY1}<>$KEY2<'", vec![("KEY", ">${KEY1}<>$KEY2<")]);
472
    }
473
474
    #[test]
475
    fn same_variable_reused() {
476
        assert_parsed_string(
477
            r#"
478
    KEY=VALUE
479
    KEY1=$KEY$KEY
480
    "#,
481
            vec![("KEY", "VALUE"), ("KEY1", "VALUEVALUE")],
482
        );
483
    }
484
485
    #[test]
486
    fn with_dot() {
487
        assert_parsed_string(
488
            r#"
489
    KEY.Value=VALUE
490
    "#,
491
            vec![("KEY.Value", "VALUE")],
492
        );
493
    }
494
495
    #[test]
496
    fn recursive_substitution() {
497
        assert_parsed_string(
498
            r#"
499
            KEY=${KEY1}+KEY_VALUE
500
            KEY1=${KEY}+KEY1_VALUE
501
            "#,
502
            vec![("KEY", "+KEY_VALUE"), ("KEY1", "+KEY_VALUE+KEY1_VALUE")],
503
        );
504
    }
505
506
    #[test]
507
    fn variable_without_parenthesis_is_substituted_before_separators() {
508
        assert_parsed_string(
509
            r#"
510
            KEY1=test_user
511
            KEY1_1=test_user_with_separator
512
            KEY=">$KEY1_1<>$KEY1}<>$KEY1{<"
513
            "#,
514
            vec![
515
                ("KEY1", "test_user"),
516
                ("KEY1_1", "test_user_with_separator"),
517
                ("KEY", ">test_user_1<>test_user}<>test_user{<"),
518
            ],
519
        );
520
    }
521
522
    #[test]
523
    fn substitute_variable_from_env_variable() {
524
        env::set_var("KEY11", "test_user_env");
525
526
        assert_parsed_string(r#"KEY=">${KEY11}<""#, vec![("KEY", ">test_user_env<")]);
527
    }
528
529
    #[test]
530
    fn substitute_variable_env_variable_overrides_dotenv_in_substitution() {
531
        env::set_var("KEY11", "test_user_env");
532
533
        assert_parsed_string(
534
            r#"
535
    KEY11=test_user
536
    KEY=">${KEY11}<"
537
    "#,
538
            vec![("KEY11", "test_user"), ("KEY", ">test_user_env<")],
539
        );
540
    }
541
542
    #[test]
543
    fn consequent_substitutions() {
544
        assert_parsed_string(
545
            r#"
546
    KEY1=test_user
547
    KEY2=$KEY1_2
548
    KEY=>${KEY1}<>${KEY2}<
549
    "#,
550
            vec![
551
                ("KEY1", "test_user"),
552
                ("KEY2", "test_user_2"),
553
                ("KEY", ">test_user<>test_user_2<"),
554
            ],
555
        );
556
    }
557
558
    #[test]
559
    fn consequent_substitutions_with_one_missing() {
560
        assert_parsed_string(
561
            r#"
562
    KEY2=$KEY1_2
563
    KEY=>${KEY1}<>${KEY2}<
564
    "#,
565
            vec![("KEY2", "_2"), ("KEY", "><>_2<")],
566
        );
567
    }
568
}
569
570
#[cfg(test)]
571
mod error_tests {
572
    use crate::errors::Error::LineParse;
573
    use crate::iter::Iter;
574
575
    #[test]
576
    fn should_not_parse_unfinished_substitutions() {
577
        let wrong_value = ">${KEY{<";
578
579
        let parsed_values: Vec<_> = Iter::new(
580
            format!(
581
                r#"
582
    KEY=VALUE
583
    KEY1={}
584
    "#,
585
                wrong_value
586
            )
587
            .as_bytes(),
588
        )
589
        .collect();
590
591
        assert_eq!(parsed_values.len(), 2);
592
593
        if let Ok(first_line) = &parsed_values[0] {
594
            assert_eq!(first_line, &(String::from("KEY"), String::from("VALUE")))
595
        } else {
596
            panic!("Expected the first value to be parsed")
597
        }
598
599
        if let Err(LineParse(second_value, index)) = &parsed_values[1] {
600
            assert_eq!(second_value, wrong_value);
601
            assert_eq!(*index, wrong_value.len() - 1)
602
        } else {
603
            panic!("Expected the second value not to be parsed")
604
        }
605
    }
606
607
    #[test]
608
    fn should_not_allow_dot_as_first_character_of_key() {
609
        let wrong_key_value = ".Key=VALUE";
610
611
        let parsed_values: Vec<_> = Iter::new(wrong_key_value.as_bytes()).collect();
612
613
        assert_eq!(parsed_values.len(), 1);
614
615
        if let Err(LineParse(second_value, index)) = &parsed_values[0] {
616
            assert_eq!(second_value, wrong_key_value);
617
            assert_eq!(*index, 0)
618
        } else {
619
            panic!("Expected the second value not to be parsed")
620
        }
621
    }
622
623
    #[test]
624
    fn should_not_parse_illegal_format() {
625
        let wrong_format = r"<><><>";
626
        let parsed_values: Vec<_> = Iter::new(wrong_format.as_bytes()).collect();
627
628
        assert_eq!(parsed_values.len(), 1);
629
630
        if let Err(LineParse(wrong_value, index)) = &parsed_values[0] {
631
            assert_eq!(wrong_value, wrong_format);
632
            assert_eq!(*index, 0)
633
        } else {
634
            panic!("Expected the second value not to be parsed")
635
        }
636
    }
637
638
    #[test]
639
    fn should_not_parse_illegal_escape() {
640
        let wrong_escape = r">\f<";
641
        let parsed_values: Vec<_> =
642
            Iter::new(format!("VALUE={}", wrong_escape).as_bytes()).collect();
643
644
        assert_eq!(parsed_values.len(), 1);
645
646
        if let Err(LineParse(wrong_value, index)) = &parsed_values[0] {
647
            assert_eq!(wrong_value, wrong_escape);
648
            assert_eq!(*index, wrong_escape.find('\\').unwrap() + 1)
649
        } else {
650
            panic!("Expected the second value not to be parsed")
651
        }
652
    }
653
}