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//!
42//! # Deserializing into structs with `FromEnv`
43//!
44//! This crate provides a [`FromEnv`] trait (and a [`#[derive(FromEnv)]`](FromEnv)
45//! proc-macro) for constructing typed structs directly from environment
46//! variables.
47//!
48//! ## Basic usage
49//!
50//! ```rust,ignore
51//! use dotenv::FromEnv;
52//!
53//! #[derive(FromEnv)]
54//! struct Config {
55//!     #[env(rename = "MY_HOST")]
56//!     host: String,
57//!     #[env(rename = "MY_PORT")]
58//!     port: u16,
59//! }
60//!
61//! let cfg = Config::from_env().unwrap();
62//! ```
63//!
64//! ## Default values
65//!
66//! Use `#[env(default)]` for any type that implements [`Default`] (yields the
67//! default when unset e.g. `None` for [`Option<T>`], `0` for numbers, `""`
68//! for [`String`]) or `#[env(default = expr)]` for any type:
69//!
70//! ```rust,ignore
71//! #[derive(FromEnv)]
72//! struct Config {
73//!     #[env(rename = "MY_HOST")]
74//!     host: String,
75//!     #[env(default)]
76//!     verbose: Option<bool>,
77//!     #[env(default)]
78//!     timeout: u64,
79//!     #[env(rename = "MY_PORT", default = 8080)]
80//!     port: u16,
81//! }
82//! ```
83//!
84//! In all cases the environment variable is checked first. The default
85//! value is only used when the variable is not set.
86//!
87//! ## Custom parsers
88//!
89//! When the standard [`FromStr`] parsing is insufficient, provide a custom
90//! parser via `#[env(with = "func")]`. The function signature must be
91//! `fn(&str, &str) -> Result<T, FromEnvError>`:
92//!
93//! ```rust,ignore
94//! use dotenv::{FromEnv, FromEnvError};
95//!
96//! fn parse_port(_var: &str, val: &str) -> Result<u16, FromEnvError> {
97//!     val.parse().map_err(|e| FromEnvError::invalid("MY_PORT", val, e))
98//! }
99//!
100//! #[derive(FromEnv)]
101//! struct Config {
102//!     #[env(rename = "MY_PORT", with = "parse_port")]
103//!     port: u16,
104//! }
105//! ```
106//!
107//! ## Nested structs
108//!
109//! Fields whose type also implements [`FromEnv`] are automatically detected
110//! and populated as nested structs. The parent's field name (in
111//! `SCREAMING_SNAKE_CASE`) is used as a prefix so that child fields are read
112//! from prefixed variable names:
113//!
114//! ```rust,ignore
115//! #[derive(FromEnv)]
116//! struct AppConfig {
117//!     database: Database,
118//!     debug: bool,       // reads DEBUG
119//! }
120//!
121//! #[derive(FromEnv)]
122//! struct Database {
123//!     url: String,       // reads DATABASE_URL
124//!     pool_size: u32,    // reads DATABASE_POOL_SIZE
125//! }
126//! ```
127//!
128//! ## Custom `FromEnvValue` implementations
129//!
130//! For leaf types that need special parsing logic, implement [`FromEnvValue`]
131//! directly on your type. Types that implement [`FromStr`] get a blanket impl
132//! automatically.
133//!
134//! ## Convenience function
135//!
136//! The function [`from_env`] lets you avoid importing the trait:
137//!
138//! ```rust,ignore
139//! let cfg: Config = dotenv::from_env().unwrap();
140//! ```
141
142use std::{collections::HashSet, env, fmt, fmt::Display, fs, io, str::FromStr};
143
144use memchr::memchr;
145
146/// Errors that can occur when loading a `.env` file.
147#[derive(Debug)]
148pub enum Error {
149    /// An I/O error (file not found, permissions, etc.).
150    Io(io::Error),
151    /// A parse error on a specific line.
152    Parse(ParseError),
153}
154
155impl fmt::Display for Error {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        match self {
158            Error::Io(e) => write!(f, "dotenv I/O error: {e}"),
159            Error::Parse(e) => write!(f, "dotenv parse error at line {}: {}", e.line, e.kind),
160        }
161    }
162}
163
164impl std::error::Error for Error {
165    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
166        match self {
167            Error::Io(e) => Some(e),
168            Error::Parse(_) => None,
169        }
170    }
171}
172
173impl From<io::Error> for Error {
174    fn from(e: io::Error) -> Self {
175        Error::Io(e)
176    }
177}
178
179/// A parse error with the line number and kind.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct ParseError {
182    /// The 1-indexed line number where the error occurred.
183    pub line: usize,
184    /// The kind of parse error.
185    pub kind: ParseErrorKind,
186}
187
188/// The specific kind of parse error.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum ParseErrorKind {
191    /// A line without an `=` sign.
192    MissingEquals,
193    /// A quoted value (`"..."` or `'...'`) without a closing quote.
194    UnmatchedQuote,
195    /// A line with an empty key before the `=` sign.
196    EmptyKey,
197    /// A key containing characters outside the allowed set
198    /// (alphanumeric, `_`, `.`, `-`).
199    InvalidKey,
200    /// Extra content found after a closing quote.
201    TrailingContent,
202}
203
204impl fmt::Display for ParseErrorKind {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        match self {
207            ParseErrorKind::MissingEquals => f.write_str("missing equals sign"),
208            ParseErrorKind::UnmatchedQuote => f.write_str("unmatched quote"),
209            ParseErrorKind::EmptyKey => f.write_str("empty key"),
210            ParseErrorKind::InvalidKey => f.write_str("invalid key character"),
211            ParseErrorKind::TrailingContent => f.write_str("trailing content after closing quote"),
212        }
213    }
214}
215
216/// Loads the `.env` file from the current working directory.
217///
218/// Each key-value pair found in the file is set as an environment variable
219/// for the current process, subject to these rules:
220///
221/// 1. A variable already present in the environment is not overwritten.
222/// 2. When the same key appears multiple times in `.env`, the first
223///    declaration takes effect.
224///
225/// # Errors
226///
227/// Returns [`Error`] if the file cannot be read (missing, permissions,
228/// etc.) or if the `.env` file is malformed.
229///
230/// # Example
231///
232/// ```rust,ignore
233/// fn main() {
234///     if let Err(e) = dotenv::load() {
235///         eprintln!("Failed to load .env: {e}");
236///     }
237/// }
238/// ```
239pub fn load() -> Result<(), Error> {
240    let mut path = env::current_dir()?;
241    path.push(".env");
242    let content = fs::read_to_string(&path)?;
243    let pairs = parse(&content)?;
244
245    let existing: HashSet<String> = env::vars().map(|(k, _)| k).collect();
246
247    let mut seen = HashSet::new();
248    for (key, value) in &pairs {
249        if seen.insert(key.clone()) && !existing.contains(key.as_str()) {
250            // SAFETY: single-threaded at startup, no concurrent access to env
251            unsafe { env::set_var(key, value) };
252        }
253    }
254    Ok(())
255}
256
257/// Parse a `.env` file string into a list of `(key, value)` pairs.
258fn parse(input: &str) -> Result<Vec<(String, String)>, Error> {
259    let mut pairs = Vec::new();
260
261    for (line_idx, raw_line) in input.lines().enumerate() {
262        let line = raw_line.trim_start();
263
264        if line.is_empty() || line.starts_with('#') {
265            continue;
266        }
267
268        let eq_pos = memchr(b'=', line.as_bytes()).ok_or_else(|| {
269            Error::Parse(ParseError {
270                line: line_idx + 1,
271                kind: ParseErrorKind::MissingEquals,
272            })
273        })?;
274
275        let key = line[..eq_pos].trim_end();
276        let value_str = &line[eq_pos + 1..];
277
278        if key.is_empty() {
279            return Err(Error::Parse(ParseError {
280                line: line_idx + 1,
281                kind: ParseErrorKind::EmptyKey,
282            }));
283        }
284
285        if !key
286            .chars()
287            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
288        {
289            return Err(Error::Parse(ParseError {
290                line: line_idx + 1,
291                kind: ParseErrorKind::InvalidKey,
292            }));
293        }
294
295        let value = parse_value(value_str, line_idx + 1)?;
296        pairs.push((key.to_string(), value));
297    }
298
299    Ok(pairs)
300}
301
302/// Find the first `#` preceded by whitespace (indicating a comment start).
303fn find_comment_start(s: &str) -> Option<usize> {
304    let bytes = s.as_bytes();
305    let mut offset = 0;
306    while let Some(pos) = memchr(b'#', &bytes[offset..]) {
307        let abs = offset + pos;
308        if abs > 0 && bytes[abs - 1].is_ascii_whitespace() {
309            return Some(abs);
310        }
311        offset = abs + 1;
312    }
313    None
314}
315
316/// Parse a single value string (everything after `=`).
317fn parse_value(s: &str, line: usize) -> Result<String, Error> {
318    let trimmed = s.trim();
319
320    if trimmed.is_empty() {
321        return Ok(String::new());
322    }
323
324    match trimmed.as_bytes()[0] {
325        b'"' => {
326            let rest = &trimmed[1..];
327            let close = memchr(b'"', rest.as_bytes()).ok_or(Error::Parse(ParseError {
328                line,
329                kind: ParseErrorKind::UnmatchedQuote,
330            }))?;
331            let after = rest[close + 1..].trim();
332            if !after.is_empty() && !after.starts_with('#') {
333                return Err(Error::Parse(ParseError {
334                    line,
335                    kind: ParseErrorKind::TrailingContent,
336                }));
337            }
338            Ok(rest[..close].to_string())
339        }
340        b'\'' => {
341            let rest = &trimmed[1..];
342            let close = memchr(b'\'', rest.as_bytes()).ok_or(Error::Parse(ParseError {
343                line,
344                kind: ParseErrorKind::UnmatchedQuote,
345            }))?;
346            let after = rest[close + 1..].trim();
347            if !after.is_empty() && !after.starts_with('#') {
348                return Err(Error::Parse(ParseError {
349                    line,
350                    kind: ParseErrorKind::TrailingContent,
351                }));
352            }
353            Ok(rest[..close].to_string())
354        }
355        _ => {
356            let comment_start = find_comment_start(s);
357            let val = match comment_start {
358                Some(pos) => &s[..pos],
359                None => s,
360            };
361            Ok(val.trim().to_string())
362        }
363    }
364}
365
366// ---------------------------------------------------------------------------
367// FromEnv trait and derive support
368// ---------------------------------------------------------------------------
369
370pub use dotenv_derive::FromEnv;
371
372/// Error returned by [`FromEnv::from_env`].
373///
374/// # Example
375///
376/// ```
377/// # use dotenv::FromEnvError;
378/// let err = FromEnvError::missing("MY_VAR");
379/// assert_eq!(err.to_string(), "environment variable `MY_VAR` is not set");
380///
381/// let err = FromEnvError::invalid("PORT", "abc", "invalid digit");
382/// assert_eq!(
383///     err.to_string(),
384///     "environment variable `PORT` has invalid value `abc`: invalid digit"
385/// );
386/// ```
387#[derive(Debug, Clone)]
388pub enum FromEnvError {
389    /// An environment variable was not set.
390    Missing(String),
391    /// An environment variable was set but could not be parsed.
392    Invalid {
393        /// The name of the environment variable.
394        var: String,
395        /// The raw value of the environment variable.
396        value: String,
397        /// A description of why parsing failed.
398        message: String,
399    },
400}
401
402impl fmt::Display for FromEnvError {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        match self {
405            FromEnvError::Missing(var) => write!(f, "environment variable `{var}` is not set"),
406            FromEnvError::Invalid {
407                var,
408                value,
409                message,
410            } => {
411                write!(f, "environment variable `{var}` has invalid value `{value}`: {message}")
412            }
413        }
414    }
415}
416
417impl std::error::Error for FromEnvError {}
418
419impl FromEnvError {
420    pub fn missing(var: impl Into<String>) -> Self {
421        FromEnvError::Missing(var.into())
422    }
423
424    pub fn invalid(var: impl Into<String>, value: impl Into<String>, message: impl Into<String>) -> Self {
425        FromEnvError::Invalid {
426            var: var.into(),
427            value: value.into(),
428            message: message.into(),
429        }
430    }
431}
432
433/// Trait for types that can be constructed from environment variables.
434///
435/// Usually derived with [`#[derive(FromEnv)]`](FromEnv).
436pub trait FromEnv: Sized {
437    /// Load `Self` from environment variables using an empty prefix.
438    fn from_env() -> Result<Self, FromEnvError> {
439        Self::from_env_with_prefix("")
440    }
441
442    /// Load `Self` from environment variables, prepending `prefix` to each
443    /// env-var name derived from field names.
444    ///
445    /// This is used internally to support nested structs. Each parent field
446    /// passes its own SCREAMING_SNAKE name plus `_` as the child's prefix.
447    fn from_env_with_prefix(prefix: &str) -> Result<Self, FromEnvError>;
448}
449
450/// Trait for converting a raw env-var string into a typed value.
451///
452/// Implementations are provided for all [`FromStr`] types via a blanket impl.
453/// You can implement this trait for custom types that need special parsing.
454///
455/// # Example
456///
457/// ```
458/// use dotenv::FromEnvValue;
459///
460/// let n = <u16 as FromEnvValue>::from_env_value("42".into()).unwrap();
461/// assert_eq!(n, 42);
462///
463/// let b = <bool as FromEnvValue>::from_env_value("true".into()).unwrap();
464/// assert!(b);
465///
466/// let err = <u16 as FromEnvValue>::from_env_value("abc".into()).unwrap_err();
467/// assert!(!err.is_empty());
468/// ```
469pub trait FromEnvValue: Sized {
470    /// Convert the raw string value into `Self`.
471    fn from_env_value(s: String) -> Result<Self, String>;
472}
473
474impl<T: FromStr> FromEnvValue for T
475where
476    T::Err: Display,
477{
478    fn from_env_value(s: String) -> Result<Self, String> {
479        s.parse::<T>().map_err(|e| e.to_string())
480    }
481}
482
483/// Auto-dispatch trait for un-attributed `#[derive(FromEnv)]` fields.
484///
485/// For types that implement [`FromEnv`] (nested structs), calls
486/// `from_env_with_prefix`. For leaf types listed in the built-in impls,
487/// reads the env var and parses via [`FromStr`].
488///
489/// You should not need to implement this trait directly.
490pub trait FromEnvAuto: Sized {
491    fn from_env_auto(prefix: &str, var_name: &str) -> Result<Self, FromEnvError>;
492}
493
494impl<T: FromEnv> FromEnvAuto for T {
495    fn from_env_auto(prefix: &str, _var_name: &str) -> Result<Self, FromEnvError> {
496        Self::from_env_with_prefix(prefix)
497    }
498}
499
500macro_rules! impl_from_env_auto_leaf {
501    ($($t:ty),* $(,)?) => {
502        $(impl FromEnvAuto for $t {
503            fn from_env_auto(_prefix: &str, var_name: &str) -> Result<Self, FromEnvError> {
504                let val = ::std::env::var(var_name)
505                    .map_err(|_| FromEnvError::missing(var_name))?;
506                <Self as FromEnvValue>::from_env_value(val.clone())
507                    .map_err(|e| FromEnvError::invalid(var_name, val, e))
508            }
509        })*
510    };
511}
512
513impl_from_env_auto_leaf!(String, bool, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64,);
514
515/// Convenience function to load a `FromEnv` type from environment variables.
516///
517/// Equivalent to `<T as FromEnv>::from_env()` but doesn't require importing
518/// the `FromEnv` trait.
519///
520/// # Example
521///
522/// ```rust,ignore
523/// let config: Config = dotenv::from_env().unwrap();
524/// ```
525pub fn from_env<T: FromEnv>() -> Result<T, FromEnvError> {
526    T::from_env()
527}
528
529// ---------------------------------------------------------------------------
530// Tests
531// ---------------------------------------------------------------------------
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    fn parse_ok(input: &str) -> Vec<(String, String)> {
538        parse(input).unwrap()
539    }
540
541    fn parse_kind(input: &str) -> ParseErrorKind {
542        match parse(input).unwrap_err() {
543            Error::Parse(e) => e.kind,
544            _ => panic!("expected Parse error"),
545        }
546    }
547
548    fn parse_line(input: &str) -> usize {
549        match parse(input).unwrap_err() {
550            Error::Parse(e) => e.line,
551            _ => panic!("expected Parse error"),
552        }
553    }
554
555    // Unsafe helper for tests. Tests are single-threaded
556    unsafe fn set_env(k: &str, v: &str) {
557        unsafe { env::set_var(k, v) };
558    }
559
560    unsafe fn remove_env(k: &str) {
561        unsafe { env::remove_var(k) };
562    }
563
564    // ── Basic parsing ──────────────────────────────────────────────────────
565
566    #[test]
567    fn simple_key_value() {
568        assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
569    }
570
571    #[test]
572    fn multiple_pairs() {
573        let pairs = parse_ok("A=1\nB=2\nC=3");
574        assert_eq!(
575            pairs,
576            vec![
577                ("A".into(), "1".into()),
578                ("B".into(), "2".into()),
579                ("C".into(), "3".into()),
580            ]
581        );
582    }
583
584    #[test]
585    fn value_with_equals() {
586        assert_eq!(parse_ok("K=a=b=c"), vec![("K".into(), "a=b=c".into())]);
587    }
588
589    #[test]
590    fn key_with_underscore() {
591        assert_eq!(parse_ok("MY_KEY=val"), vec![("MY_KEY".into(), "val".into())]);
592    }
593
594    #[test]
595    fn key_with_dot() {
596        assert_eq!(parse_ok("my.key=val"), vec![("my.key".into(), "val".into())]);
597    }
598
599    #[test]
600    fn key_with_hyphen() {
601        assert_eq!(parse_ok("my-key=val"), vec![("my-key".into(), "val".into())]);
602    }
603
604    #[test]
605    fn key_with_digits() {
606        assert_eq!(parse_ok("KEY123=val"), vec![("KEY123".into(), "val".into())]);
607    }
608
609    #[test]
610    fn key_mixed() {
611        assert_eq!(parse_ok("A1.b-C_2=val"), vec![("A1.b-C_2".into(), "val".into())]);
612    }
613
614    #[test]
615    fn key_starting_with_hyphen() {
616        assert_eq!(parse_ok("-KEY=v"), vec![("-KEY".into(), "v".into())]);
617    }
618
619    #[test]
620    fn key_starting_with_dot() {
621        assert_eq!(parse_ok(".KEY=v"), vec![(".KEY".into(), "v".into())]);
622    }
623
624    #[test]
625    fn key_starting_with_underscore() {
626        assert_eq!(parse_ok("_KEY=v"), vec![("_KEY".into(), "v".into())]);
627    }
628
629    #[test]
630    fn key_only_dots() {
631        assert_eq!(parse_ok("...=value"), vec![("...".into(), "value".into())]);
632    }
633
634    #[test]
635    fn key_only_hyphens() {
636        assert_eq!(parse_ok("---=value"), vec![("---".into(), "value".into())]);
637    }
638
639    // ── Double-quoted values ───────────────────────────────────────────────
640
641    #[test]
642    fn double_quoted_value() {
643        assert_eq!(parse_ok("K=\"hello\""), vec![("K".into(), "hello".into())]);
644    }
645
646    #[test]
647    fn double_quoted_with_spaces() {
648        assert_eq!(parse_ok("K=\"hello world\""), vec![("K".into(), "hello world".into())]);
649    }
650
651    #[test]
652    fn double_quoted_empty() {
653        assert_eq!(parse_ok("K=\"\""), vec![("K".into(), "".into())]);
654    }
655
656    #[test]
657    fn double_quoted_hash_preserved() {
658        assert_eq!(parse_ok("K=\"a#b\""), vec![("K".into(), "a#b".into())]);
659    }
660
661    #[test]
662    fn double_quoted_equals_inside() {
663        assert_eq!(parse_ok("K=\"a=b\""), vec![("K".into(), "a=b".into())]);
664    }
665
666    #[test]
667    fn double_quoted_single_quotes_inside() {
668        assert_eq!(parse_ok("K=\"it's ok\""), vec![("K".into(), "it's ok".into())]);
669    }
670
671    #[test]
672    fn double_quoted_whitespace_preserved() {
673        assert_eq!(parse_ok("K=\" hello \""), vec![("K".into(), " hello ".into())]);
674    }
675
676    #[test]
677    fn double_quoted_trailing_content_error() {
678        assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
679    }
680
681    #[test]
682    fn double_quoted_trailing_comment_allowed() {
683        assert_eq!(parse_ok("K=\"hello\" # comment"), vec![("K".into(), "hello".into())]);
684    }
685
686    // ── Single-quoted values ───────────────────────────────────────────────
687
688    #[test]
689    fn single_quoted_value() {
690        assert_eq!(parse_ok("K='hello'"), vec![("K".into(), "hello".into())]);
691    }
692
693    #[test]
694    fn single_quoted_with_spaces() {
695        assert_eq!(parse_ok("K='hello world'"), vec![("K".into(), "hello world".into())]);
696    }
697
698    #[test]
699    fn single_quoted_empty() {
700        assert_eq!(parse_ok("K=''"), vec![("K".into(), "".into())]);
701    }
702
703    #[test]
704    fn single_quoted_hash_preserved() {
705        assert_eq!(parse_ok("K='a#b'"), vec![("K".into(), "a#b".into())]);
706    }
707
708    #[test]
709    fn single_quoted_double_quotes_inside() {
710        assert_eq!(parse_ok(r#"K='"hello"'"#), vec![("K".into(), r#""hello""#.into())]);
711    }
712
713    #[test]
714    fn single_quoted_whitespace_preserved() {
715        assert_eq!(parse_ok("K=' hello '"), vec![("K".into(), " hello ".into())]);
716    }
717
718    #[test]
719    fn single_quoted_trailing_content_error() {
720        assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
721    }
722
723    #[test]
724    fn single_quoted_trailing_comment_allowed() {
725        assert_eq!(parse_ok("K='hello' # comment"), vec![("K".into(), "hello".into())]);
726    }
727
728    // ── Quoted example from the spec ────────────────────────────────────────
729
730    #[test]
731    fn quoted_nested_example() {
732        assert_eq!(parse_ok("HELLO='\"hello\"'"), vec![("HELLO".into(), "\"hello\"".into())]);
733    }
734
735    // ── Unquoted values ────────────────────────────────────────────────────
736
737    #[test]
738    fn unquoted_hash_is_comment() {
739        assert_eq!(parse_ok("K=val # comment"), vec![("K".into(), "val".into())]);
740    }
741
742    #[test]
743    fn unquoted_hash_no_space_not_comment() {
744        assert_eq!(parse_ok("K=val#comment"), vec![("K".into(), "val#comment".into())]);
745    }
746
747    #[test]
748    fn unquoted_trimmed() {
749        assert_eq!(parse_ok("K=  val  "), vec![("K".into(), "val".into())]);
750    }
751
752    #[test]
753    fn unquoted_trailing_spaces_before_comment() {
754        assert_eq!(parse_ok("K=val   # comment"), vec![("K".into(), "val".into())]);
755    }
756
757    #[test]
758    fn unquoted_value_with_numbers() {
759        assert_eq!(parse_ok("PORT=8080"), vec![("PORT".into(), "8080".into())]);
760    }
761
762    #[test]
763    fn unquoted_value_with_dots() {
764        assert_eq!(parse_ok("HOST=192.168.1.1"), vec![("HOST".into(), "192.168.1.1".into())]);
765    }
766
767    #[test]
768    fn unquoted_value_containing_quote() {
769        assert_eq!(parse_ok("K=hello\"there"), vec![("K".into(), "hello\"there".into())]);
770    }
771
772    #[test]
773    fn unquoted_value_containing_only_hash() {
774        assert_eq!(parse_ok("K=#"), vec![("K".into(), "#".into())]);
775    }
776
777    #[test]
778    fn unquoted_value_hash_without_preceding_space() {
779        assert_eq!(parse_ok("K=val#ue"), vec![("K".into(), "val#ue".into())]);
780    }
781
782    #[test]
783    fn unquoted_hash_with_preceding_space_is_comment() {
784        assert_eq!(parse_ok("K=val #ue"), vec![("K".into(), "val".into())]);
785    }
786
787    // ── Empty values ───────────────────────────────────────────────────────
788
789    #[test]
790    fn empty_value_no_quotes() {
791        assert_eq!(parse_ok("K="), vec![("K".into(), "".into())]);
792    }
793
794    #[test]
795    fn empty_value_trailing_spaces() {
796        assert_eq!(parse_ok("K=   "), vec![("K".into(), "".into())]);
797    }
798
799    #[test]
800    fn empty_value_spaces_before_comment() {
801        assert_eq!(parse_ok("K=   # comment"), vec![("K".into(), "".into())]);
802    }
803
804    #[test]
805    fn empty_double_quoted_value_with_comment() {
806        assert_eq!(parse_ok("K=\"\" # comment"), vec![("K".into(), "".into())]);
807    }
808
809    // ── Whitespace handling ────────────────────────────────────────────────
810
811    #[test]
812    fn leading_whitespace_on_line() {
813        assert_eq!(parse_ok("  K=v"), vec![("K".into(), "v".into())]);
814    }
815
816    #[test]
817    fn trailing_whitespace_before_equals() {
818        assert_eq!(parse_ok("K  =v"), vec![("K".into(), "v".into())]);
819    }
820
821    #[test]
822    fn whitespace_around_equals() {
823        assert_eq!(parse_ok("K = v"), vec![("K".into(), "v".into())]);
824    }
825
826    #[test]
827    fn tabs_as_whitespace() {
828        assert_eq!(parse_ok("\tK\t=\tv"), vec![("K".into(), "v".into())]);
829    }
830
831    #[test]
832    fn tab_after_equals() {
833        assert_eq!(parse_ok("K=\tval"), vec![("K".into(), "val".into())]);
834    }
835
836    #[test]
837    fn double_equals_value() {
838        assert_eq!(parse_ok("K==v"), vec![("K".into(), "=v".into())]);
839    }
840
841    // ── Comments ───────────────────────────────────────────────────────────
842
843    #[test]
844    fn full_line_comment() {
845        assert!(parse_ok("# this is a comment").is_empty());
846    }
847
848    #[test]
849    fn comment_with_leading_spaces() {
850        assert!(parse_ok("  # indented comment").is_empty());
851    }
852
853    #[test]
854    fn empty_lines_skipped() {
855        assert!(parse_ok("\n\n\n").is_empty());
856    }
857
858    #[test]
859    fn mixed_comments_and_values() {
860        let pairs = parse_ok("# header\nA=1\n\nB=2 # inline\n");
861        assert_eq!(pairs, vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
862    }
863
864    // ── Line endings ───────────────────────────────────────────────────────
865
866    #[test]
867    fn unix_line_endings() {
868        assert_eq!(parse_ok("A=1\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
869    }
870
871    #[test]
872    fn windows_line_endings() {
873        assert_eq!(parse_ok("A=1\r\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
874    }
875
876    #[test]
877    fn no_trailing_newline() {
878        assert_eq!(parse_ok("A=1"), vec![("A".into(), "1".into())]);
879    }
880
881    #[test]
882    fn single_line_no_newline() {
883        assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
884    }
885
886    // ── Edge cases: empty / comment-only files ─────────────────────────────
887
888    #[test]
889    fn empty_file() {
890        assert!(parse_ok("").is_empty());
891    }
892
893    #[test]
894    fn only_comments() {
895        assert!(parse_ok("# a\n# b\n# c").is_empty());
896    }
897
898    #[test]
899    fn only_blank_lines() {
900        assert!(parse_ok("\n\n \n\t\n").is_empty());
901    }
902
903    // ── Error cases ────────────────────────────────────────────────────────
904
905    #[test]
906    fn error_missing_equals() {
907        assert_eq!(parse_kind("INVALID"), ParseErrorKind::MissingEquals);
908    }
909
910    #[test]
911    fn error_missing_equals_with_comment() {
912        assert_eq!(parse_kind("K # comment"), ParseErrorKind::MissingEquals);
913    }
914
915    #[test]
916    fn error_empty_key() {
917        assert_eq!(parse_kind("=value"), ParseErrorKind::EmptyKey);
918    }
919
920    #[test]
921    fn error_empty_key_with_spaces() {
922        assert_eq!(parse_kind("   =value"), ParseErrorKind::EmptyKey);
923    }
924
925    #[test]
926    fn error_unmatched_double_quote() {
927        assert_eq!(parse_kind("K=\"hello"), ParseErrorKind::UnmatchedQuote);
928    }
929
930    #[test]
931    fn error_unmatched_single_quote() {
932        assert_eq!(parse_kind("K='hello"), ParseErrorKind::UnmatchedQuote);
933    }
934
935    #[test]
936    fn error_unmatched_double_quote_with_hash() {
937        assert_eq!(parse_kind("K=\"hello#more"), ParseErrorKind::UnmatchedQuote);
938    }
939
940    #[test]
941    fn error_trailing_content_double_quote() {
942        assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
943    }
944
945    #[test]
946    fn error_trailing_content_single_quote() {
947        assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
948    }
949
950    #[test]
951    fn error_trailing_content_line_number() {
952        assert_eq!(parse_line("A=1\nK=\"v\"x\nB=2"), 2);
953    }
954
955    #[test]
956    fn error_invalid_key_exclamation() {
957        assert_eq!(parse_kind("K!EY=v"), ParseErrorKind::InvalidKey);
958    }
959
960    #[test]
961    fn error_invalid_key_dollar() {
962        assert_eq!(parse_kind("\u{0024}KEY=v"), ParseErrorKind::InvalidKey);
963    }
964
965    #[test]
966    fn error_invalid_key_at() {
967        assert_eq!(parse_kind("KEY@=v"), ParseErrorKind::InvalidKey);
968    }
969
970    #[test]
971    fn error_invalid_key_space() {
972        assert_eq!(parse_kind("K EY=v"), ParseErrorKind::InvalidKey);
973    }
974
975    #[test]
976    fn error_invalid_key_slash() {
977        assert_eq!(parse_kind("KEY/VAL=v"), ParseErrorKind::InvalidKey);
978    }
979
980    #[test]
981    fn error_invalid_key_unicode() {
982        assert_eq!(parse_kind("K\u{00C9}Y=v"), ParseErrorKind::InvalidKey);
983    }
984
985    #[test]
986    fn error_line_number_missing_equals() {
987        assert_eq!(parse_line("A=1\nINVALID\nB=2"), 2);
988    }
989
990    #[test]
991    fn error_line_number_invalid_key() {
992        assert_eq!(parse_line("A=1\n\"$\"BAD=v\nB=2"), 2);
993    }
994
995    #[test]
996    fn error_line_number_unmatched_quote() {
997        assert_eq!(parse_line("A=1\nK=\"unclosed\nB=2"), 2);
998    }
999
1000    // ── Unicode values ─────────────────────────────────────────────────────
1001
1002    #[test]
1003    fn unicode_value_unquoted() {
1004        assert_eq!(parse_ok("K=h\u{00E9}llo"), vec![("K".into(), "h\u{00E9}llo".into())]);
1005    }
1006
1007    #[test]
1008    fn unicode_value_double_quoted() {
1009        assert_eq!(parse_ok("K=\"h\u{00E9}llo\""), vec![("K".into(), "h\u{00E9}llo".into())]);
1010    }
1011
1012    #[test]
1013    fn unicode_value_single_quoted() {
1014        assert_eq!(parse_ok("K='h\u{00E9}llo'"), vec![("K".into(), "h\u{00E9}llo".into())]);
1015    }
1016
1017    // ── `load()` integration tests ─────────────────────────────────────────
1018
1019    #[test]
1020    fn load_sets_vars() {
1021        let dir = env::temp_dir().join(format!("dotenv_test_{}", std::process::id()));
1022        let _ = fs::create_dir_all(&dir);
1023        let env_path = dir.join(".env");
1024        fs::write(&env_path, "DOTENV_TEST_FOO=bar\nDOTENV_TEST_BAZ=qux").unwrap();
1025
1026        let old = env::current_dir().ok();
1027        env::set_current_dir(&dir).unwrap();
1028
1029        let result = load();
1030
1031        if let Some(p) = old {
1032            let _ = env::set_current_dir(p);
1033        }
1034        let _ = fs::remove_file(&env_path);
1035        let _ = fs::remove_dir(&dir);
1036
1037        assert!(result.is_ok());
1038        assert_eq!(env::var("DOTENV_TEST_FOO").unwrap(), "bar");
1039        assert_eq!(env::var("DOTENV_TEST_BAZ").unwrap(), "qux");
1040
1041        unsafe { remove_env("DOTENV_TEST_FOO") };
1042        unsafe { remove_env("DOTENV_TEST_BAZ") };
1043    }
1044
1045    #[test]
1046    fn load_preserves_existing_env_vars() {
1047        unsafe { set_env("DOTENV_EXISTING", "original") };
1048
1049        let dir = env::temp_dir().join(format!("dotenv_test_existing_{}", std::process::id()));
1050        let _ = fs::create_dir_all(&dir);
1051        let env_path = dir.join(".env");
1052        fs::write(&env_path, "DOTENV_EXISTING=from_file").unwrap();
1053
1054        let old = env::current_dir().ok();
1055        env::set_current_dir(&dir).unwrap();
1056
1057        let result = load();
1058
1059        if let Some(p) = old {
1060            let _ = env::set_current_dir(p);
1061        }
1062        let _ = fs::remove_file(&env_path);
1063        let _ = fs::remove_dir(&dir);
1064
1065        assert!(result.is_ok());
1066        assert_eq!(env::var("DOTENV_EXISTING").unwrap(), "original");
1067
1068        unsafe { remove_env("DOTENV_EXISTING") };
1069    }
1070
1071    #[test]
1072    fn load_first_declaration_wins() {
1073        let dir = env::temp_dir().join(format!("dotenv_test_first_{}", std::process::id()));
1074        let _ = fs::create_dir_all(&dir);
1075        let env_path = dir.join(".env");
1076        fs::write(&env_path, "DOTENV_DUP=first\nDOTENV_DUP=second").unwrap();
1077
1078        let old = env::current_dir().ok();
1079        env::set_current_dir(&dir).unwrap();
1080
1081        let result = load();
1082
1083        if let Some(p) = old {
1084            let _ = env::set_current_dir(p);
1085        }
1086        let _ = fs::remove_file(&env_path);
1087        let _ = fs::remove_dir(&dir);
1088
1089        assert!(result.is_ok());
1090        assert_eq!(env::var("DOTENV_DUP").unwrap(), "first");
1091
1092        unsafe { remove_env("DOTENV_DUP") };
1093    }
1094
1095    #[test]
1096    fn load_file_not_found() {
1097        let dir = env::temp_dir().join(format!("dotenv_test_missing_{}", std::process::id()));
1098        let _ = fs::create_dir_all(&dir);
1099
1100        let old = env::current_dir().ok();
1101        env::set_current_dir(&dir).unwrap();
1102
1103        let result = load();
1104
1105        if let Some(p) = old {
1106            let _ = env::set_current_dir(p);
1107        }
1108        let _ = fs::remove_dir(&dir);
1109
1110        match result.unwrap_err() {
1111            Error::Io(_) => {}
1112            _ => panic!("expected Io error"),
1113        }
1114    }
1115
1116    #[test]
1117    fn load_parse_error() {
1118        let dir = env::temp_dir().join(format!("dotenv_test_parse_err_{}", std::process::id()));
1119        let _ = fs::create_dir_all(&dir);
1120        let env_path = dir.join(".env");
1121        fs::write(&env_path, "A=1\nMALFORMED\nB=2").unwrap();
1122
1123        let old = env::current_dir().ok();
1124        env::set_current_dir(&dir).unwrap();
1125
1126        let result = load();
1127
1128        if let Some(p) = old {
1129            let _ = env::set_current_dir(p);
1130        }
1131        let _ = fs::remove_file(&env_path);
1132        let _ = fs::remove_dir(&dir);
1133
1134        match result.unwrap_err() {
1135            Error::Parse(e) => assert_eq!(e.line, 2),
1136            _ => panic!("expected Parse error"),
1137        }
1138    }
1139}