Skip to main content

dotenv/
dotenv.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! Loads environment variables from `.env` files.
4//!
5//! # Quick start
6//!
7//! ```rust,ignore
8//! dotenv::load();
9//! ```
10//!
11//! Call [`load`] near the start of your program to load a `.env` file
12//! from the current working directory.
13//!
14//! # Precedence
15//!
16//! - **Existing environment variables are never overwritten.** A variable
17//!   already set in the environment takes priority over anything in `.env`.
18//! - **First declaration wins in `.env`.** If the same key appears multiple
19//!   times, only the first is used.
20//!
21//! # Supported syntax
22//!
23//! ```env
24//! HELLO=world
25//! HELLO="world"
26//! HELLO='world'
27//! HELLO='"nested"'
28//! HELLO=world  # inline comment
29//! # full-line comment
30//! ```
31//!
32//! ## Key names
33//!
34//! Keys may only contain ASCII letters, digits, `_`, `.`, and `-`.
35//!
36//! ## Limitations
37//!
38//! - Multi-line values are not supported.
39//! - Variable substitution (e.g. `${FOO}`) is not supported.
40//! - Export syntax (`export KEY=value`) is not supported.
41
42use std::{collections::HashSet, env, fmt, fs, io};
43
44/// Errors that can occur when loading a `.env` file.
45#[derive(Debug)]
46pub enum Error {
47    /// An I/O error (file not found, permissions, etc.).
48    Io(io::Error),
49    /// A parse error on a specific line.
50    Parse(ParseError),
51}
52
53impl fmt::Display for Error {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Error::Io(e) => write!(f, "dotenv I/O error: {e}"),
57            Error::Parse(e) => write!(f, "dotenv parse error at line {}: {}", e.line, e.kind),
58        }
59    }
60}
61
62impl std::error::Error for Error {
63    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
64        match self {
65            Error::Io(e) => Some(e),
66            Error::Parse(_) => None,
67        }
68    }
69}
70
71impl From<io::Error> for Error {
72    fn from(e: io::Error) -> Self {
73        Error::Io(e)
74    }
75}
76
77/// A parse error with the line number and kind.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ParseError {
80    /// The 1-indexed line number where the error occurred.
81    pub line: usize,
82    /// The kind of parse error.
83    pub kind: ParseErrorKind,
84}
85
86/// The specific kind of parse error.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum ParseErrorKind {
89    /// A line without an `=` sign.
90    MissingEquals,
91    /// A quoted value (`"..."` or `'...'`) without a closing quote.
92    UnmatchedQuote,
93    /// A line with an empty key before the `=` sign.
94    EmptyKey,
95    /// A key containing characters outside the allowed set
96    /// (alphanumeric, `_`, `.`, `-`).
97    InvalidKey,
98    /// Extra content found after a closing quote.
99    TrailingContent,
100}
101
102impl fmt::Display for ParseErrorKind {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            ParseErrorKind::MissingEquals => f.write_str("missing equals sign"),
106            ParseErrorKind::UnmatchedQuote => f.write_str("unmatched quote"),
107            ParseErrorKind::EmptyKey => f.write_str("empty key"),
108            ParseErrorKind::InvalidKey => f.write_str("invalid key character"),
109            ParseErrorKind::TrailingContent => f.write_str("trailing content after closing quote"),
110        }
111    }
112}
113
114/// Loads the `.env` file from the current working directory.
115///
116/// Each key-value pair found in the file is set as an environment variable
117/// for the current process, subject to these rules:
118///
119/// 1. A variable already present in the environment is not overwritten.
120/// 2. When the same key appears multiple times in `.env`, the first
121///    declaration takes effect.
122///
123/// # Errors
124///
125/// Returns [`Error`] if the file cannot be read (missing, permissions,
126/// etc.) or if the `.env` file is malformed.
127///
128/// # Example
129///
130/// ```rust,ignore
131/// fn main() {
132///     if let Err(e) = dotenv::load() {
133///         eprintln!("Failed to load .env: {e}");
134///     }
135/// }
136/// ```
137pub fn load() -> Result<(), Error> {
138    let mut path = env::current_dir()?;
139    path.push(".env");
140    let content = fs::read_to_string(&path)?;
141    let pairs = parse(&content)?;
142
143    let existing: HashSet<String> = env::vars().map(|(k, _)| k).collect();
144
145    let mut seen = HashSet::new();
146    for (key, value) in &pairs {
147        if seen.insert(key.clone()) && !existing.contains(key.as_str()) {
148            // SAFETY: single-threaded at startup, no concurrent access to env
149            unsafe { env::set_var(key, value) };
150        }
151    }
152    Ok(())
153}
154
155/// Parse a `.env` file string into a list of `(key, value)` pairs.
156fn parse(input: &str) -> Result<Vec<(String, String)>, Error> {
157    let mut pairs = Vec::new();
158
159    for (line_idx, raw_line) in input.lines().enumerate() {
160        let line = raw_line.trim_start();
161
162        if line.is_empty() || line.starts_with('#') {
163            continue;
164        }
165
166        let eq_pos = line.find('=').ok_or_else(|| {
167            Error::Parse(ParseError {
168                line: line_idx + 1,
169                kind: ParseErrorKind::MissingEquals,
170            })
171        })?;
172
173        let key = line[..eq_pos].trim_end();
174        let value_str = &line[eq_pos + 1..];
175
176        if key.is_empty() {
177            return Err(Error::Parse(ParseError {
178                line: line_idx + 1,
179                kind: ParseErrorKind::EmptyKey,
180            }));
181        }
182
183        if !key
184            .chars()
185            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
186        {
187            return Err(Error::Parse(ParseError {
188                line: line_idx + 1,
189                kind: ParseErrorKind::InvalidKey,
190            }));
191        }
192
193        let value = parse_value(value_str, line_idx + 1)?;
194        pairs.push((key.to_string(), value));
195    }
196
197    Ok(pairs)
198}
199
200/// Parse a single value string (everything after `=`).
201fn parse_value(s: &str, line: usize) -> Result<String, Error> {
202    let trimmed = s.trim();
203
204    if trimmed.is_empty() {
205        return Ok(String::new());
206    }
207
208    match trimmed.as_bytes()[0] {
209        b'"' => {
210            let rest = &trimmed[1..];
211            let close = rest.find('"').ok_or_else(|| {
212                Error::Parse(ParseError {
213                    line,
214                    kind: ParseErrorKind::UnmatchedQuote,
215                })
216            })?;
217            let after = rest[close + 1..].trim();
218            if !after.is_empty() && !after.starts_with('#') {
219                return Err(Error::Parse(ParseError {
220                    line,
221                    kind: ParseErrorKind::TrailingContent,
222                }));
223            }
224            Ok(rest[..close].to_string())
225        }
226        b'\'' => {
227            let rest = &trimmed[1..];
228            let close = rest.find('\'').ok_or_else(|| {
229                Error::Parse(ParseError {
230                    line,
231                    kind: ParseErrorKind::UnmatchedQuote,
232                })
233            })?;
234            let after = rest[close + 1..].trim();
235            if !after.is_empty() && !after.starts_with('#') {
236                return Err(Error::Parse(ParseError {
237                    line,
238                    kind: ParseErrorKind::TrailingContent,
239                }));
240            }
241            Ok(rest[..close].to_string())
242        }
243        _ => {
244            let comment_start = s
245                .as_bytes()
246                .windows(2)
247                .position(|w| w[0].is_ascii_whitespace() && w[1] == b'#')
248                .map(|i| i + 1);
249            let val = match comment_start {
250                Some(pos) => &s[..pos],
251                None => s,
252            };
253            Ok(val.trim().to_string())
254        }
255    }
256}
257
258// ---------------------------------------------------------------------------
259// Tests
260// ---------------------------------------------------------------------------
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    fn parse_ok(input: &str) -> Vec<(String, String)> {
267        parse(input).unwrap()
268    }
269
270    fn parse_kind(input: &str) -> ParseErrorKind {
271        match parse(input).unwrap_err() {
272            Error::Parse(e) => e.kind,
273            _ => panic!("expected Parse error"),
274        }
275    }
276
277    fn parse_line(input: &str) -> usize {
278        match parse(input).unwrap_err() {
279            Error::Parse(e) => e.line,
280            _ => panic!("expected Parse error"),
281        }
282    }
283
284    // Unsafe helper for tests — tests are single-threaded
285    unsafe fn set_env(k: &str, v: &str) {
286        unsafe { env::set_var(k, v) };
287    }
288
289    unsafe fn remove_env(k: &str) {
290        unsafe { env::remove_var(k) };
291    }
292
293    // ── Basic parsing ──────────────────────────────────────────────────────
294
295    #[test]
296    fn simple_key_value() {
297        assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
298    }
299
300    #[test]
301    fn multiple_pairs() {
302        let pairs = parse_ok("A=1\nB=2\nC=3");
303        assert_eq!(
304            pairs,
305            vec![
306                ("A".into(), "1".into()),
307                ("B".into(), "2".into()),
308                ("C".into(), "3".into()),
309            ]
310        );
311    }
312
313    #[test]
314    fn value_with_equals() {
315        assert_eq!(parse_ok("K=a=b=c"), vec![("K".into(), "a=b=c".into())]);
316    }
317
318    #[test]
319    fn key_with_underscore() {
320        assert_eq!(parse_ok("MY_KEY=val"), vec![("MY_KEY".into(), "val".into())]);
321    }
322
323    #[test]
324    fn key_with_dot() {
325        assert_eq!(parse_ok("my.key=val"), vec![("my.key".into(), "val".into())]);
326    }
327
328    #[test]
329    fn key_with_hyphen() {
330        assert_eq!(parse_ok("my-key=val"), vec![("my-key".into(), "val".into())]);
331    }
332
333    #[test]
334    fn key_with_digits() {
335        assert_eq!(parse_ok("KEY123=val"), vec![("KEY123".into(), "val".into())]);
336    }
337
338    #[test]
339    fn key_mixed() {
340        assert_eq!(parse_ok("A1.b-C_2=val"), vec![("A1.b-C_2".into(), "val".into())]);
341    }
342
343    #[test]
344    fn key_starting_with_hyphen() {
345        assert_eq!(parse_ok("-KEY=v"), vec![("-KEY".into(), "v".into())]);
346    }
347
348    #[test]
349    fn key_starting_with_dot() {
350        assert_eq!(parse_ok(".KEY=v"), vec![(".KEY".into(), "v".into())]);
351    }
352
353    #[test]
354    fn key_starting_with_underscore() {
355        assert_eq!(parse_ok("_KEY=v"), vec![("_KEY".into(), "v".into())]);
356    }
357
358    // ── Double-quoted values ───────────────────────────────────────────────
359
360    #[test]
361    fn double_quoted_value() {
362        assert_eq!(parse_ok("K=\"hello\""), vec![("K".into(), "hello".into())]);
363    }
364
365    #[test]
366    fn double_quoted_with_spaces() {
367        assert_eq!(parse_ok("K=\"hello world\""), vec![("K".into(), "hello world".into())]);
368    }
369
370    #[test]
371    fn double_quoted_empty() {
372        assert_eq!(parse_ok("K=\"\""), vec![("K".into(), "".into())]);
373    }
374
375    #[test]
376    fn double_quoted_hash_preserved() {
377        assert_eq!(parse_ok("K=\"a#b\""), vec![("K".into(), "a#b".into())]);
378    }
379
380    #[test]
381    fn double_quoted_equals_inside() {
382        assert_eq!(parse_ok("K=\"a=b\""), vec![("K".into(), "a=b".into())]);
383    }
384
385    #[test]
386    fn double_quoted_single_quotes_inside() {
387        assert_eq!(parse_ok("K=\"it's ok\""), vec![("K".into(), "it's ok".into())]);
388    }
389
390    #[test]
391    fn double_quoted_whitespace_preserved() {
392        assert_eq!(parse_ok("K=\" hello \""), vec![("K".into(), " hello ".into())]);
393    }
394
395    #[test]
396    fn double_quoted_trailing_content_error() {
397        assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
398    }
399
400    #[test]
401    fn double_quoted_trailing_comment_allowed() {
402        assert_eq!(parse_ok("K=\"hello\" # comment"), vec![("K".into(), "hello".into())]);
403    }
404
405    // ── Single-quoted values ───────────────────────────────────────────────
406
407    #[test]
408    fn single_quoted_value() {
409        assert_eq!(parse_ok("K='hello'"), vec![("K".into(), "hello".into())]);
410    }
411
412    #[test]
413    fn single_quoted_with_spaces() {
414        assert_eq!(parse_ok("K='hello world'"), vec![("K".into(), "hello world".into())]);
415    }
416
417    #[test]
418    fn single_quoted_empty() {
419        assert_eq!(parse_ok("K=''"), vec![("K".into(), "".into())]);
420    }
421
422    #[test]
423    fn single_quoted_hash_preserved() {
424        assert_eq!(parse_ok("K='a#b'"), vec![("K".into(), "a#b".into())]);
425    }
426
427    #[test]
428    fn single_quoted_double_quotes_inside() {
429        assert_eq!(parse_ok(r#"K='"hello"'"#), vec![("K".into(), r#""hello""#.into())]);
430    }
431
432    #[test]
433    fn single_quoted_whitespace_preserved() {
434        assert_eq!(parse_ok("K=' hello '"), vec![("K".into(), " hello ".into())]);
435    }
436
437    #[test]
438    fn single_quoted_trailing_content_error() {
439        assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
440    }
441
442    // ── Quoted example from the spec ────────────────────────────────────────
443
444    #[test]
445    fn quoted_nested_example() {
446        assert_eq!(parse_ok("HELLO='\"hello\"'"), vec![("HELLO".into(), "\"hello\"".into())]);
447    }
448
449    // ── Unquoted values ────────────────────────────────────────────────────
450
451    #[test]
452    fn unquoted_hash_is_comment() {
453        assert_eq!(parse_ok("K=val # comment"), vec![("K".into(), "val".into())]);
454    }
455
456    #[test]
457    fn unquoted_hash_no_space_not_comment() {
458        assert_eq!(parse_ok("K=val#comment"), vec![("K".into(), "val#comment".into())]);
459    }
460
461    #[test]
462    fn unquoted_trimmed() {
463        assert_eq!(parse_ok("K=  val  "), vec![("K".into(), "val".into())]);
464    }
465
466    #[test]
467    fn unquoted_trailing_spaces_before_comment() {
468        assert_eq!(parse_ok("K=val   # comment"), vec![("K".into(), "val".into())]);
469    }
470
471    #[test]
472    fn unquoted_value_with_numbers() {
473        assert_eq!(parse_ok("PORT=8080"), vec![("PORT".into(), "8080".into())]);
474    }
475
476    #[test]
477    fn unquoted_value_with_dots() {
478        assert_eq!(parse_ok("HOST=192.168.1.1"), vec![("HOST".into(), "192.168.1.1".into())]);
479    }
480
481    #[test]
482    fn unquoted_value_containing_quote() {
483        assert_eq!(parse_ok("K=hello\"there"), vec![("K".into(), "hello\"there".into())]);
484    }
485
486    #[test]
487    fn unquoted_value_containing_only_hash() {
488        assert_eq!(parse_ok("K=#"), vec![("K".into(), "#".into())]);
489    }
490
491    #[test]
492    fn unquoted_value_hash_without_preceding_space() {
493        assert_eq!(parse_ok("K=val#ue"), vec![("K".into(), "val#ue".into())]);
494    }
495
496    #[test]
497    fn unquoted_hash_with_preceding_space_is_comment() {
498        assert_eq!(parse_ok("K=val #ue"), vec![("K".into(), "val".into())]);
499    }
500
501    // ── Empty values ───────────────────────────────────────────────────────
502
503    #[test]
504    fn empty_value_no_quotes() {
505        assert_eq!(parse_ok("K="), vec![("K".into(), "".into())]);
506    }
507
508    #[test]
509    fn empty_value_trailing_spaces() {
510        assert_eq!(parse_ok("K=   "), vec![("K".into(), "".into())]);
511    }
512
513    #[test]
514    fn empty_value_spaces_before_comment() {
515        assert_eq!(parse_ok("K=   # comment"), vec![("K".into(), "".into())]);
516    }
517
518    // ── Whitespace handling ────────────────────────────────────────────────
519
520    #[test]
521    fn leading_whitespace_on_line() {
522        assert_eq!(parse_ok("  K=v"), vec![("K".into(), "v".into())]);
523    }
524
525    #[test]
526    fn trailing_whitespace_before_equals() {
527        assert_eq!(parse_ok("K  =v"), vec![("K".into(), "v".into())]);
528    }
529
530    #[test]
531    fn whitespace_around_equals() {
532        assert_eq!(parse_ok("K = v"), vec![("K".into(), "v".into())]);
533    }
534
535    #[test]
536    fn tabs_as_whitespace() {
537        assert_eq!(parse_ok("\tK\t=\tv"), vec![("K".into(), "v".into())]);
538    }
539
540    // ── Comments ───────────────────────────────────────────────────────────
541
542    #[test]
543    fn full_line_comment() {
544        assert!(parse_ok("# this is a comment").is_empty());
545    }
546
547    #[test]
548    fn comment_with_leading_spaces() {
549        assert!(parse_ok("  # indented comment").is_empty());
550    }
551
552    #[test]
553    fn empty_lines_skipped() {
554        assert!(parse_ok("\n\n\n").is_empty());
555    }
556
557    #[test]
558    fn mixed_comments_and_values() {
559        let pairs = parse_ok("# header\nA=1\n\nB=2 # inline\n");
560        assert_eq!(pairs, vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
561    }
562
563    // ── Line endings ───────────────────────────────────────────────────────
564
565    #[test]
566    fn unix_line_endings() {
567        assert_eq!(parse_ok("A=1\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
568    }
569
570    #[test]
571    fn windows_line_endings() {
572        assert_eq!(parse_ok("A=1\r\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
573    }
574
575    #[test]
576    fn no_trailing_newline() {
577        assert_eq!(parse_ok("A=1"), vec![("A".into(), "1".into())]);
578    }
579
580    #[test]
581    fn single_line_no_newline() {
582        assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
583    }
584
585    // ── Edge cases: empty / comment-only files ─────────────────────────────
586
587    #[test]
588    fn empty_file() {
589        assert!(parse_ok("").is_empty());
590    }
591
592    #[test]
593    fn only_comments() {
594        assert!(parse_ok("# a\n# b\n# c").is_empty());
595    }
596
597    #[test]
598    fn only_blank_lines() {
599        assert!(parse_ok("\n\n \n\t\n").is_empty());
600    }
601
602    // ── Error cases ────────────────────────────────────────────────────────
603
604    #[test]
605    fn error_missing_equals() {
606        assert_eq!(parse_kind("INVALID"), ParseErrorKind::MissingEquals);
607    }
608
609    #[test]
610    fn error_missing_equals_with_comment() {
611        assert_eq!(parse_kind("K # comment"), ParseErrorKind::MissingEquals);
612    }
613
614    #[test]
615    fn error_empty_key() {
616        assert_eq!(parse_kind("=value"), ParseErrorKind::EmptyKey);
617    }
618
619    #[test]
620    fn error_empty_key_with_spaces() {
621        assert_eq!(parse_kind("   =value"), ParseErrorKind::EmptyKey);
622    }
623
624    #[test]
625    fn error_unmatched_double_quote() {
626        assert_eq!(parse_kind("K=\"hello"), ParseErrorKind::UnmatchedQuote);
627    }
628
629    #[test]
630    fn error_unmatched_single_quote() {
631        assert_eq!(parse_kind("K='hello"), ParseErrorKind::UnmatchedQuote);
632    }
633
634    #[test]
635    fn error_unmatched_double_quote_with_hash() {
636        assert_eq!(parse_kind("K=\"hello#more"), ParseErrorKind::UnmatchedQuote);
637    }
638
639    #[test]
640    fn error_trailing_content_double_quote() {
641        assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
642    }
643
644    #[test]
645    fn error_trailing_content_single_quote() {
646        assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
647    }
648
649    #[test]
650    fn error_trailing_content_line_number() {
651        assert_eq!(parse_line("A=1\nK=\"v\"x\nB=2"), 2);
652    }
653
654    #[test]
655    fn error_invalid_key_exclamation() {
656        assert_eq!(parse_kind("K!EY=v"), ParseErrorKind::InvalidKey);
657    }
658
659    #[test]
660    fn error_invalid_key_dollar() {
661        assert_eq!(parse_kind("\u{0024}KEY=v"), ParseErrorKind::InvalidKey);
662    }
663
664    #[test]
665    fn error_invalid_key_at() {
666        assert_eq!(parse_kind("KEY@=v"), ParseErrorKind::InvalidKey);
667    }
668
669    #[test]
670    fn error_invalid_key_space() {
671        assert_eq!(parse_kind("K EY=v"), ParseErrorKind::InvalidKey);
672    }
673
674    #[test]
675    fn error_invalid_key_slash() {
676        assert_eq!(parse_kind("KEY/VAL=v"), ParseErrorKind::InvalidKey);
677    }
678
679    #[test]
680    fn error_invalid_key_unicode() {
681        assert_eq!(parse_kind("K\u{00C9}Y=v"), ParseErrorKind::InvalidKey);
682    }
683
684    #[test]
685    fn error_line_number_missing_equals() {
686        assert_eq!(parse_line("A=1\nINVALID\nB=2"), 2);
687    }
688
689    #[test]
690    fn error_line_number_invalid_key() {
691        assert_eq!(parse_line("A=1\n\"$\"BAD=v\nB=2"), 2);
692    }
693
694    #[test]
695    fn error_line_number_unmatched_quote() {
696        assert_eq!(parse_line("A=1\nK=\"unclosed\nB=2"), 2);
697    }
698
699    // ── Unicode values ─────────────────────────────────────────────────────
700
701    #[test]
702    fn unicode_value_unquoted() {
703        assert_eq!(parse_ok("K=h\u{00E9}llo"), vec![("K".into(), "h\u{00E9}llo".into())]);
704    }
705
706    #[test]
707    fn unicode_value_double_quoted() {
708        assert_eq!(parse_ok("K=\"h\u{00E9}llo\""), vec![("K".into(), "h\u{00E9}llo".into())]);
709    }
710
711    #[test]
712    fn unicode_value_single_quoted() {
713        assert_eq!(parse_ok("K='h\u{00E9}llo'"), vec![("K".into(), "h\u{00E9}llo".into())]);
714    }
715
716    // ── `load()` integration tests ─────────────────────────────────────────
717
718    #[test]
719    fn load_sets_vars() {
720        let dir = env::temp_dir().join(format!("dotenv_test_{}", std::process::id()));
721        let _ = fs::create_dir_all(&dir);
722        let env_path = dir.join(".env");
723        fs::write(&env_path, "DOTENV_TEST_FOO=bar\nDOTENV_TEST_BAZ=qux").unwrap();
724
725        let old = env::current_dir().ok();
726        env::set_current_dir(&dir).unwrap();
727
728        let result = load();
729
730        if let Some(p) = old {
731            let _ = env::set_current_dir(p);
732        }
733        let _ = fs::remove_file(&env_path);
734        let _ = fs::remove_dir(&dir);
735
736        assert!(result.is_ok());
737        assert_eq!(env::var("DOTENV_TEST_FOO").unwrap(), "bar");
738        assert_eq!(env::var("DOTENV_TEST_BAZ").unwrap(), "qux");
739
740        unsafe { remove_env("DOTENV_TEST_FOO") };
741        unsafe { remove_env("DOTENV_TEST_BAZ") };
742    }
743
744    #[test]
745    fn load_preserves_existing_env_vars() {
746        unsafe { set_env("DOTENV_EXISTING", "original") };
747
748        let dir = env::temp_dir().join(format!("dotenv_test_existing_{}", std::process::id()));
749        let _ = fs::create_dir_all(&dir);
750        let env_path = dir.join(".env");
751        fs::write(&env_path, "DOTENV_EXISTING=from_file").unwrap();
752
753        let old = env::current_dir().ok();
754        env::set_current_dir(&dir).unwrap();
755
756        let result = load();
757
758        if let Some(p) = old {
759            let _ = env::set_current_dir(p);
760        }
761        let _ = fs::remove_file(&env_path);
762        let _ = fs::remove_dir(&dir);
763
764        assert!(result.is_ok());
765        assert_eq!(env::var("DOTENV_EXISTING").unwrap(), "original");
766
767        unsafe { remove_env("DOTENV_EXISTING") };
768    }
769
770    #[test]
771    fn load_first_declaration_wins() {
772        let dir = env::temp_dir().join(format!("dotenv_test_first_{}", std::process::id()));
773        let _ = fs::create_dir_all(&dir);
774        let env_path = dir.join(".env");
775        fs::write(&env_path, "DOTENV_DUP=first\nDOTENV_DUP=second").unwrap();
776
777        let old = env::current_dir().ok();
778        env::set_current_dir(&dir).unwrap();
779
780        let result = load();
781
782        if let Some(p) = old {
783            let _ = env::set_current_dir(p);
784        }
785        let _ = fs::remove_file(&env_path);
786        let _ = fs::remove_dir(&dir);
787
788        assert!(result.is_ok());
789        assert_eq!(env::var("DOTENV_DUP").unwrap(), "first");
790
791        unsafe { remove_env("DOTENV_DUP") };
792    }
793
794    #[test]
795    fn load_file_not_found() {
796        let dir = env::temp_dir().join(format!("dotenv_test_missing_{}", std::process::id()));
797        let _ = fs::create_dir_all(&dir);
798
799        let old = env::current_dir().ok();
800        env::set_current_dir(&dir).unwrap();
801
802        let result = load();
803
804        if let Some(p) = old {
805            let _ = env::set_current_dir(p);
806        }
807        let _ = fs::remove_dir(&dir);
808
809        match result.unwrap_err() {
810            Error::Io(_) => {}
811            _ => panic!("expected Io error"),
812        }
813    }
814
815    #[test]
816    fn load_parse_error() {
817        let dir = env::temp_dir().join(format!("dotenv_test_parse_err_{}", std::process::id()));
818        let _ = fs::create_dir_all(&dir);
819        let env_path = dir.join(".env");
820        fs::write(&env_path, "A=1\nMALFORMED\nB=2").unwrap();
821
822        let old = env::current_dir().ok();
823        env::set_current_dir(&dir).unwrap();
824
825        let result = load();
826
827        if let Some(p) = old {
828            let _ = env::set_current_dir(p);
829        }
830        let _ = fs::remove_file(&env_path);
831        let _ = fs::remove_dir(&dir);
832
833        match result.unwrap_err() {
834            Error::Parse(e) => assert_eq!(e.line, 2),
835            _ => panic!("expected Parse error"),
836        }
837    }
838}