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,no_run
8//! fn main() -> Result<(), dotenv::Error> {
9//!     dotenv::load()?;
10//!     Ok(())
11//! }
12//! ```
13//!
14//! Call [`load`] near the start of your program to load a `.env` file
15//! from the current working directory.
16//!
17//! # Precedence
18//!
19//! - **Existing environment variables are never overwritten.** A variable
20//!   already set in the environment takes priority over anything in `.env`.
21//! - **First declaration wins in `.env`.** If the same key appears multiple
22//!   times, only the first is used.
23//! - **Loading is atomic.** If the file contains a parse error, no environment
24//!   variable is modified, even for lines that appear before the error.
25//!
26//! # Supported syntax
27//!
28//! ```env
29//! HELLO=world
30//! HELLO="world"
31//! HELLO='world'
32//! HELLO='"nested"'
33//! HELLO=world  # inline comment
34//! # full-line comment
35//! ```
36//!
37//! ## Key names
38//!
39//! Keys may only contain ASCII letters, digits, `_`, `.`, and `-`. A key may
40//! start with any of those characters (including a digit, `.` or `-`).
41//!
42//! ## Comments
43//!
44//! A `#` starts an inline comment only when it is preceded by whitespace
45//! (`K=v # comment` strips the comment, but `K=v#comment` keeps the `#`).
46//!
47//! ## Whitespace
48//!
49//! Leading whitespace on a line and whitespace around `=` are ignored. An
50//! unquoted value is trimmed; whitespace inside an unquoted value is kept
51//! (`K=hello world` yields `hello world`).
52//!
53//! ## Quotes and escapes
54//!
55//! - Single-quoted values (`'...'`) are literal: no escapes are processed.
56//! - Double-quoted values (`"..."`) process the escape sequences `\\`, `\'`,
57//!   `\"`, `\$`, `\n`, `\t`, and `\r`. Any other backslash sequence is a
58//!   [`ParseErrorKind::InvalidEscape`] error.
59//! - Backslashes in unquoted values are literal (`K=C:\path` yields `C:\path`).
60//! - A UTF-8 byte-order mark (BOM) at the very start of the file is ignored.
61//!
62//! ## Limitations
63//!
64//! - Multi-line values are not supported.
65//! - Variable substitution (e.g. `${FOO}`) is not supported.
66//! - Export syntax (`export KEY=value`) is not supported.
67//! - Only `\n` and `\r\n` are recognised as line endings; a lone `\r` is not a
68//!   line break.
69//! - Values may not contain a NUL byte (`\0`), which cannot be placed in the
70//!   process environment.
71//!
72//! # Thread safety
73//!
74//! [`load`] and [`from_path`] call [`std::env::set_var`] internally, which is
75//! unsound to call while other threads are running or reading the environment.
76//! **Call them before spawning any threads** (typically at the very start of
77//! `main`). If you only need to inspect `.env` content, use [`parse`], which
78//! does not touch the process environment.
79//!
80//! # Performance
81//!
82//! `.env` files are configuration-sized. Because the platform `setenv` costs
83//! `O(n)` per variable once `n` variables are set, loading an extremely large
84//! number of variables is quadratic overall. Do not feed untrusted,
85//! arbitrarily large files to [`load`]; validate them with [`parse`] first.
86//!
87//! # Deserializing into structs with `FromEnv`
88//!
89//! This crate provides a [`FromEnv`] trait (and a [`#[derive(FromEnv)]`](FromEnv)
90//! proc-macro) for constructing typed structs directly from environment
91//! variables.
92//!
93//! ## Basic usage
94//!
95//! ```rust,ignore
96//! use dotenv::FromEnv;
97//!
98//! #[derive(FromEnv)]
99//! struct Config {
100//!     #[env(rename = "MY_HOST")]
101//!     host: String,
102//!     #[env(rename = "MY_PORT")]
103//!     port: u16,
104//! }
105//!
106//! let cfg = Config::from_env().unwrap();
107//! ```
108//!
109//! ## Default values
110//!
111//! Use `#[env(default)]` for any type that implements [`Default`] (yields the
112//! default when the variable is not set) or `#[env(default = expr)]` for any
113//! type. The environment variable is always read first; the default is only
114//! used when it is not set.
115//!
116//! ```rust,ignore
117//! #[derive(FromEnv)]
118//! struct Config {
119//!     #[env(rename = "MY_HOST")]
120//!     host: String,
121//!     #[env(rename = "MY_VERBOSE", default)]
122//!     verbose: Option<bool>,
123//!     #[env(default)]
124//!     timeout: u64,             // reads TIMEOUT, falls back to 0
125//!     #[env(rename = "MY_PORT", default = 8080)]
126//!     port: u16,
127//! }
128//! ```
129//!
130//! `Option<T>` fields are always optional: an unset (or empty) variable yields
131//! `None`, otherwise the value is parsed and wrapped in `Some`.
132//!
133//! ```rust,ignore
134//! #[derive(FromEnv)]
135//! struct Config {
136//!     #[env(rename = "MY_PORT")]
137//!     port: Option<u16>,        // None when MY_PORT is unset or empty
138//! }
139//! ```
140//!
141//! ## Custom parsers
142//!
143//! When the standard [`FromStr`] parsing is insufficient, provide a custom
144//! parser via `#[env(with = "func")]`. The function may be a full path
145//! (`module::func`) and must have the signature `fn(&str, &str) -> Result<T, FromEnvError>`:
146//!
147//! ```rust,ignore
148//! use dotenv::{FromEnv, FromEnvError};
149//!
150//! fn parse_port(_var: &str, val: &str) -> Result<u16, FromEnvError> {
151//!     val.parse().map_err(|e| FromEnvError::invalid("MY_PORT", val, e))
152//! }
153//!
154//! #[derive(FromEnv)]
155//! struct Config {
156//!     #[env(rename = "MY_PORT", with = "parse_port")]
157//!     port: u16,
158//! }
159//! ```
160//!
161//! ## Nested structs
162//!
163//! Fields whose type also implements [`FromEnv`] are automatically detected
164//! and populated as nested structs. The parent's field name (in
165//! `SCREAMING_SNAKE_CASE`) is used as a prefix so that child fields are read
166//! from prefixed variable names:
167//!
168//! ```rust,ignore
169//! #[derive(FromEnv)]
170//! struct AppConfig {
171//!     database: Database,
172//!     debug: bool,       // reads DEBUG
173//! }
174//!
175//! #[derive(FromEnv)]
176//! struct Database {
177//!     url: String,       // reads DATABASE_URL
178//!     pool_size: u32,    // reads DATABASE_POOL_SIZE
179//! }
180//! ```
181//!
182//! ## Custom `FromEnvValue` implementations
183//!
184//! For leaf types that need special parsing logic, implement [`FromEnvValue`]
185//! directly on your type. Types that implement [`FromStr`] get a blanket impl
186//! automatically.
187//!
188//! ## Compile-time diagnostics
189//!
190//! Invalid usage is reported as an ordinary compile error rather than a macro
191//! panic:
192//!
193//! ```compile_fail
194//! use dotenv::FromEnv;
195//!
196//! // Tuple structs are not supported.
197//! #[derive(FromEnv)]
198//! struct Config(u16);
199//! ```
200//!
201//! ```compile_fail
202//! use dotenv::FromEnv;
203//!
204//! // Multiple `#[env(...)]` attributes on one field are rejected.
205//! #[derive(FromEnv)]
206//! struct Config {
207//!     #[env(rename = "A")]
208//!     #[env(rename = "B")]
209//!     value: u16,
210//! }
211//! ```
212//!
213//! [`#[env(default)]`](FromEnv) without a `rename` reads the variable derived
214//! from the field name and requires the field type to implement
215//! [`FromEnvAuto`] (a built-in leaf type, or a nested [`FromEnv`] struct);
216//! `#[env(rename = "...", default)]` reads the named variable via
217//! [`FromEnvValue`].
218//!
219//! ## Convenience function
220//!
221//! The function [`from_env`] lets you avoid importing the trait:
222//!
223//! ```rust,ignore
224//! let cfg: Config = dotenv::from_env().unwrap();
225//! ```
226
227use std::{
228    borrow::Cow,
229    collections::{HashMap, HashSet},
230    env,
231    ffi::{OsStr, OsString},
232    fmt,
233    fmt::Display,
234    fs, io,
235    path::Path,
236    str::FromStr,
237};
238
239use memchr::memchr;
240
241/// Errors that can occur when loading a `.env` file.
242#[derive(Debug)]
243pub enum Error {
244    /// An I/O error (file not found, permissions, invalid UTF-8, etc.).
245    Io(io::Error),
246    /// A parse error on a specific line.
247    Parse(ParseError),
248}
249
250impl fmt::Display for Error {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        match self {
253            Error::Io(e) => write!(f, "dotenv I/O error: {e}"),
254            Error::Parse(e) => write!(f, "dotenv parse error at line {}: {}", e.line, e.kind),
255        }
256    }
257}
258
259impl core::error::Error for Error {
260    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
261        match self {
262            Error::Io(e) => Some(e),
263            Error::Parse(_) => None,
264        }
265    }
266}
267
268impl From<io::Error> for Error {
269    fn from(e: io::Error) -> Self {
270        Error::Io(e)
271    }
272}
273
274/// A parse error with the line number and kind.
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct ParseError {
277    /// The 1-indexed line number where the error occurred.
278    pub line: usize,
279    /// The kind of parse error.
280    pub kind: ParseErrorKind,
281}
282
283/// The specific kind of parse error.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub enum ParseErrorKind {
286    /// A line without an `=` sign.
287    MissingEquals,
288    /// A quoted value (`"..."` or `'...'`) without a closing quote.
289    UnmatchedQuote,
290    /// A line with an empty key before the `=` sign.
291    EmptyKey,
292    /// A key containing characters outside the allowed set
293    /// (alphanumeric, `_`, `.`, `-`).
294    InvalidKey,
295    /// Extra content found after a closing quote.
296    TrailingContent,
297    /// A value containing a NUL byte, which cannot be stored in the process
298    /// environment.
299    InvalidValue,
300    /// An unrecognised escape sequence inside a double-quoted value.
301    InvalidEscape,
302}
303
304impl fmt::Display for ParseErrorKind {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        match self {
307            ParseErrorKind::MissingEquals => f.write_str("missing equals sign"),
308            ParseErrorKind::UnmatchedQuote => f.write_str("unmatched quote"),
309            ParseErrorKind::EmptyKey => f.write_str("empty key"),
310            ParseErrorKind::InvalidKey => f.write_str("invalid key character"),
311            ParseErrorKind::TrailingContent => f.write_str("trailing content after closing quote"),
312            ParseErrorKind::InvalidValue => f.write_str("value contains a NUL byte"),
313            ParseErrorKind::InvalidEscape => f.write_str("invalid escape sequence"),
314        }
315    }
316}
317
318/// Loads the `.env` file from the current working directory.
319///
320/// Each key-value pair found in the file is set as an environment variable
321/// for the current process, subject to these rules:
322///
323/// 1. A variable already present in the environment is not overwritten.
324/// 2. When the same key appears multiple times in `.env`, the first
325///    declaration takes effect.
326/// 3. If the file is malformed, no variable is modified.
327///
328/// # Errors
329///
330/// Returns [`Error`] if the file cannot be read (missing, permissions,
331/// invalid UTF-8, etc.) or if the `.env` file is malformed.
332///
333/// # Thread safety
334///
335/// This function modifies the process environment. Call it before spawning any
336/// threads; see the [crate-level documentation](crate#thread-safety).
337///
338/// # Example
339///
340/// ```rust,no_run
341/// if let Err(e) = dotenv::load() {
342///     eprintln!("Failed to load .env: {e}");
343/// }
344/// ```
345pub fn load() -> Result<(), Error> {
346    let mut path = env::current_dir()?;
347    path.push(".env");
348    from_path(path)
349}
350
351/// Loads a `.env` file from an explicit path.
352///
353/// Behaves like [`load`] but reads from `path` instead of `./.env`. See
354/// [`load`] for the precedence rules and the thread-safety requirements.
355///
356/// # Errors
357///
358/// Returns [`Error`] if the file cannot be read or is malformed.
359///
360/// # Example
361///
362/// ```rust,no_run
363/// fn main() -> Result<(), dotenv::Error> {
364///     dotenv::from_path("config/.env")?;
365///     Ok(())
366/// }
367/// ```
368pub fn from_path(path: impl AsRef<Path>) -> Result<(), Error> {
369    let content = fs::read_to_string(path)?;
370    // `parse` resolves duplicate keys (first declaration wins) and returns
371    // before any environment variable is touched, so loading stays atomic.
372    let vars = parse(&content).map_err(Error::Parse)?;
373
374    // Snapshot of the pre-existing variable *names*. `vars_os` is used (rather
375    // than `vars`) so that variables whose keys or values are not valid UTF-8
376    // do not cause a panic.
377    let existing: HashSet<OsString> = env::vars_os().map(|(k, _)| k).collect();
378
379    for (key, value) in &vars {
380        if !existing.contains(OsStr::new(*key)) {
381            // SAFETY: the caller is required to invoke this before any other
382            // thread runs or reads the environment (see the crate docs).
383            unsafe { env::set_var(*key, value.as_ref()) };
384        }
385    }
386    Ok(())
387}
388
389/// Parses `.env` content into a map of key/value pairs.
390///
391/// Duplicate keys follow the same rule as [`load`]: the **first** declaration
392/// wins. Iteration order of the returned map is unspecified. Escape sequences
393/// in double-quoted values are expanded (see the [crate-level
394/// documentation](crate#quotes-and-escapes)).
395///
396/// Keys and values borrow from `input`. A value that contains escape
397/// sequences is materialised and returned as [`Cow::Owned`]; every other value
398/// is a [`Cow::Borrowed`] slice of `input`. Use [`Cow::as_ref`] (or `.as_ref()`)
399/// to obtain a plain `&str`, and convert with
400/// `into_iter().map(|(k, v)| (k.to_owned(), v.into_owned()))` if the map must
401/// outlive `input`.
402///
403/// This function does not touch the process environment, so it is safe to call
404/// from any thread and useful for validating or inspecting `.env` content.
405///
406/// # Errors
407///
408/// Returns [`ParseError`] if the content is malformed.
409///
410/// # Example
411///
412/// ```
413/// let vars = dotenv::parse("A=1\nB=two\nA=ignored").unwrap();
414/// assert_eq!(vars["A"].as_ref(), "1");
415/// assert_eq!(vars["B"].as_ref(), "two");
416/// assert_eq!(vars.len(), 2);
417/// ```
418pub fn parse(input: &str) -> Result<HashMap<&str, Cow<'_, str>>, ParseError> {
419    // Ignore a UTF-8 BOM at the very start of the file.
420    let input = input.strip_prefix('\u{FEFF}').unwrap_or(input);
421    let mut map: HashMap<&str, Cow<'_, str>> = HashMap::new();
422
423    for (line_idx, raw_line) in input.lines().enumerate() {
424        let line_no = line_idx + 1;
425        let line = raw_line.trim_start();
426
427        if line.is_empty() || line.starts_with('#') {
428            continue;
429        }
430
431        let eq_pos = memchr(b'=', line.as_bytes()).ok_or(ParseError {
432            line: line_no,
433            kind: ParseErrorKind::MissingEquals,
434        })?;
435
436        let key = line[..eq_pos].trim_end();
437        let value_str = &line[eq_pos + 1..];
438
439        if key.is_empty() {
440            return Err(ParseError {
441                line: line_no,
442                kind: ParseErrorKind::EmptyKey,
443            });
444        }
445
446        if !key
447            .bytes()
448            .all(|c| c.is_ascii_alphanumeric() || c == b'_' || c == b'.' || c == b'-')
449        {
450            return Err(ParseError {
451                line: line_no,
452                kind: ParseErrorKind::InvalidKey,
453            });
454        }
455
456        let value = parse_value(value_str, line_no)?;
457        // First declaration wins: `entry` only inserts when the key is absent.
458        map.entry(key).or_insert(value);
459    }
460
461    Ok(map)
462}
463
464/// Find the first `#` preceded by whitespace (indicating a comment start).
465fn find_comment_start(s: &str) -> Option<usize> {
466    let bytes = s.as_bytes();
467    let mut offset = 0;
468    while let Some(pos) = memchr(b'#', &bytes[offset..]) {
469        let abs = offset + pos;
470        if abs > 0 && bytes[abs - 1].is_ascii_whitespace() {
471            return Some(abs);
472        }
473        offset = abs + 1;
474    }
475    None
476}
477
478/// Parse a single value string (everything after `=`).
479fn parse_value(s: &str, line: usize) -> Result<Cow<'_, str>, ParseError> {
480    let trimmed = s.trim();
481
482    if trimmed.is_empty() {
483        return Ok(Cow::Borrowed(""));
484    }
485
486    match trimmed.as_bytes()[0] {
487        b'"' => parse_double_quoted(trimmed, line),
488        b'\'' => parse_single_quoted(trimmed, line),
489        _ => {
490            let comment_start = find_comment_start(s);
491            let val = match comment_start {
492                Some(pos) => &s[..pos],
493                None => s,
494            };
495            let val = val.trim();
496            if val.as_bytes().contains(&0) {
497                return Err(ParseError {
498                    line,
499                    kind: ParseErrorKind::InvalidValue,
500                });
501            }
502            Ok(Cow::Borrowed(val))
503        }
504    }
505}
506
507/// Parse a double-quoted value, finding the closing quote while honouring
508/// backslash escapes, then expanding the escapes.
509fn parse_double_quoted(trimmed: &str, line: usize) -> Result<Cow<'_, str>, ParseError> {
510    let content = &trimmed[1..];
511
512    let mut escaped = false;
513    let mut close = None;
514    for (i, &b) in content.as_bytes().iter().enumerate() {
515        if escaped {
516            escaped = false;
517            continue;
518        }
519        match b {
520            b'\\' => escaped = true,
521            b'"' => {
522                close = Some(i);
523                break;
524            }
525            _ => {}
526        }
527    }
528    let close = close.ok_or(ParseError {
529        line,
530        kind: ParseErrorKind::UnmatchedQuote,
531    })?;
532
533    let raw_content = &content[..close];
534    let after = content[close + 1..].trim();
535    if !after.is_empty() && !after.starts_with('#') {
536        return Err(ParseError {
537            line,
538            kind: ParseErrorKind::TrailingContent,
539        });
540    }
541
542    let value = expand_escapes(raw_content, line)?;
543    if value.as_bytes().contains(&0) {
544        return Err(ParseError {
545            line,
546            kind: ParseErrorKind::InvalidValue,
547        });
548    }
549    Ok(value)
550}
551
552/// Parse a single-quoted value. Single quotes are literal: no escapes.
553fn parse_single_quoted(trimmed: &str, line: usize) -> Result<Cow<'_, str>, ParseError> {
554    let content = &trimmed[1..];
555    let close = memchr(b'\'', content.as_bytes()).ok_or(ParseError {
556        line,
557        kind: ParseErrorKind::UnmatchedQuote,
558    })?;
559
560    let raw = &content[..close];
561    let after = content[close + 1..].trim();
562    if !after.is_empty() && !after.starts_with('#') {
563        return Err(ParseError {
564            line,
565            kind: ParseErrorKind::TrailingContent,
566        });
567    }
568
569    if raw.as_bytes().contains(&0) {
570        return Err(ParseError {
571            line,
572            kind: ParseErrorKind::InvalidValue,
573        });
574    }
575    Ok(Cow::Borrowed(raw))
576}
577
578/// Expand backslash escape sequences inside a double-quoted value.
579///
580/// Supported: `\\`, `\'`, `\"`, `\$`, `\n`, `\t`, `\r`. Any other sequence is
581/// an [`ParseErrorKind::InvalidEscape`] error.
582fn expand_escapes(raw: &str, line: usize) -> Result<Cow<'_, str>, ParseError> {
583    if !raw.as_bytes().contains(&b'\\') {
584        return Ok(Cow::Borrowed(raw));
585    }
586
587    let mut out = String::with_capacity(raw.len());
588    let mut chars = raw.chars();
589    while let Some(c) = chars.next() {
590        if c != '\\' {
591            out.push(c);
592            continue;
593        }
594        match chars.next() {
595            Some('\\') => out.push('\\'),
596            Some('\'') => out.push('\''),
597            Some('"') => out.push('"'),
598            Some('$') => out.push('$'),
599            Some('n') => out.push('\n'),
600            Some('t') => out.push('\t'),
601            Some('r') => out.push('\r'),
602            // A trailing backslash cannot occur here (the closing-quote scan
603            // would have consumed it), but report it rather than panicking.
604            Some(_) | None => {
605                return Err(ParseError {
606                    line,
607                    kind: ParseErrorKind::InvalidEscape,
608                });
609            }
610        }
611    }
612    Ok(Cow::Owned(out))
613}
614
615// ---------------------------------------------------------------------------
616// FromEnv trait and derive support
617// ---------------------------------------------------------------------------
618
619pub use dotenv_derive::FromEnv;
620
621/// Error returned by [`FromEnv::from_env`].
622///
623/// # Example
624///
625/// ```
626/// # use dotenv::FromEnvError;
627/// let err = FromEnvError::missing("MY_VAR");
628/// assert_eq!(err.to_string(), "environment variable `MY_VAR` is not set");
629///
630/// let err = FromEnvError::invalid("PORT", "abc", "invalid digit");
631/// assert_eq!(
632///     err.to_string(),
633///     "environment variable `PORT` has invalid value `abc`: invalid digit"
634/// );
635/// ```
636#[derive(Debug, Clone)]
637pub enum FromEnvError {
638    /// An environment variable was not set.
639    Missing(String),
640    /// An environment variable was set but could not be parsed.
641    Invalid {
642        /// The name of the environment variable.
643        var: String,
644        /// The raw value of the environment variable.
645        value: String,
646        /// A description of why parsing failed.
647        message: String,
648    },
649}
650
651impl fmt::Display for FromEnvError {
652    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
653        match self {
654            FromEnvError::Missing(var) => write!(f, "environment variable `{var}` is not set"),
655            FromEnvError::Invalid {
656                var,
657                value,
658                message,
659            } => {
660                write!(f, "environment variable `{var}` has invalid value `{value}`: {message}")
661            }
662        }
663    }
664}
665
666impl core::error::Error for FromEnvError {}
667
668impl FromEnvError {
669    /// Construct a [`FromEnvError::Missing`] error.
670    pub fn missing(var: impl Into<String>) -> Self {
671        FromEnvError::Missing(var.into())
672    }
673
674    /// Construct a [`FromEnvError::Invalid`] error.
675    pub fn invalid(var: impl Into<String>, value: impl Into<String>, message: impl Into<String>) -> Self {
676        FromEnvError::Invalid {
677            var: var.into(),
678            value: value.into(),
679            message: message.into(),
680        }
681    }
682}
683
684/// Trait for types that can be constructed from environment variables.
685///
686/// Usually derived with [`#[derive(FromEnv)]`](FromEnv).
687pub trait FromEnv: Sized {
688    /// Load `Self` from environment variables using an empty prefix.
689    fn from_env() -> Result<Self, FromEnvError> {
690        Self::from_env_with_prefix("")
691    }
692
693    /// Load `Self` from environment variables, prepending `prefix` to each
694    /// env-var name derived from field names.
695    ///
696    /// This is used internally to support nested structs. Each parent field
697    /// passes its own SCREAMING_SNAKE name plus `_` as the child's prefix.
698    fn from_env_with_prefix(prefix: &str) -> Result<Self, FromEnvError>;
699}
700
701/// Trait for converting a raw env-var string into a typed value.
702///
703/// Implementations are provided for all [`FromStr`] types via a blanket impl.
704/// You can implement this trait for custom types that need special parsing.
705///
706/// # Example
707///
708/// ```
709/// use dotenv::FromEnvValue;
710///
711/// let n = <u16 as FromEnvValue>::from_env_value("42").unwrap();
712/// assert_eq!(n, 42);
713///
714/// let b = <bool as FromEnvValue>::from_env_value("true").unwrap();
715/// assert!(b);
716///
717/// let err = <u16 as FromEnvValue>::from_env_value("abc").unwrap_err();
718/// assert!(!err.is_empty());
719/// ```
720pub trait FromEnvValue: Sized {
721    /// Convert the raw string value into `Self`.
722    fn from_env_value(s: &str) -> Result<Self, String>;
723}
724
725impl<T: FromStr> FromEnvValue for T
726where
727    T::Err: Display,
728{
729    fn from_env_value(s: &str) -> Result<Self, String> {
730        s.parse::<T>().map_err(|e| e.to_string())
731    }
732}
733
734/// Auto-dispatch trait for un-attributed `#[derive(FromEnv)]` fields.
735///
736/// For types that implement [`FromEnv`] (nested structs), calls
737/// `from_env_with_prefix`. For leaf types listed in the built-in impls,
738/// reads the env var and parses via [`FromStr`].
739///
740/// You should not need to implement this trait directly.
741pub trait FromEnvAuto: Sized {
742    /// Read and parse the variable `var_name`, or load a nested struct using
743    /// `prefix`.
744    fn from_env_auto(prefix: &str, var_name: &str) -> Result<Self, FromEnvError>;
745}
746
747impl<T: FromEnv> FromEnvAuto for T {
748    fn from_env_auto(prefix: &str, _var_name: &str) -> Result<Self, FromEnvError> {
749        Self::from_env_with_prefix(prefix)
750    }
751}
752
753macro_rules! impl_from_env_auto_leaf {
754    ($($t:ty),* $(,)?) => {
755        $(impl FromEnvAuto for $t {
756            fn from_env_auto(_prefix: &str, var_name: &str) -> Result<Self, FromEnvError> {
757                let val = ::std::env::var(var_name)
758                    .map_err(|_| FromEnvError::missing(var_name))?;
759                <Self as FromEnvValue>::from_env_value(&val)
760                    .map_err(|e| FromEnvError::invalid(var_name, val, e))
761            }
762        })*
763    };
764}
765
766impl_from_env_auto_leaf!(String, bool, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64,);
767
768/// Convenience function to load a `FromEnv` type from environment variables.
769///
770/// Equivalent to `<T as FromEnv>::from_env()` but doesn't require importing
771/// the `FromEnv` trait.
772///
773/// # Example
774///
775/// ```rust,ignore
776/// let config: Config = dotenv::from_env().unwrap();
777/// ```
778pub fn from_env<T: FromEnv>() -> Result<T, FromEnvError> {
779    T::from_env()
780}
781
782// ---------------------------------------------------------------------------
783// Tests
784// ---------------------------------------------------------------------------
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    // Serialize tests that mutate the process-wide environment or current
791    // directory. `cargo test` runs tests in parallel threads by default.
792    #[cfg(not(target_arch = "wasm32"))]
793    static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
794
795    // `parse` returns an unordered map; sort the resolved pairs so that
796    // assertions below are deterministic.
797    fn parse_ok(input: &str) -> Vec<(String, String)> {
798        let mut pairs: Vec<(String, String)> = parse(input)
799            .unwrap()
800            .into_iter()
801            .map(|(k, v)| (k.to_string(), v.into_owned()))
802            .collect();
803        pairs.sort();
804        pairs
805    }
806
807    fn parse_kind(input: &str) -> ParseErrorKind {
808        parse(input).unwrap_err().kind
809    }
810
811    fn parse_line(input: &str) -> usize {
812        parse(input).unwrap_err().line
813    }
814
815    // Unsafe helpers for tests. Tests that use them hold `TEST_LOCK`.
816    #[cfg(not(target_arch = "wasm32"))]
817    unsafe fn set_env(k: &str, v: &str) {
818        unsafe { env::set_var(k, v) };
819    }
820
821    #[cfg(not(target_arch = "wasm32"))]
822    unsafe fn remove_env(k: &str) {
823        unsafe { env::remove_var(k) };
824    }
825
826    // ── Basic parsing ──────────────────────────────────────────────────────
827
828    #[test]
829    fn simple_key_value() {
830        assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
831    }
832
833    #[test]
834    fn multiple_pairs() {
835        let pairs = parse_ok("A=1\nB=2\nC=3");
836        assert_eq!(
837            pairs,
838            vec![
839                ("A".into(), "1".into()),
840                ("B".into(), "2".into()),
841                ("C".into(), "3".into()),
842            ]
843        );
844    }
845
846    #[test]
847    fn value_with_equals() {
848        assert_eq!(parse_ok("K=a=b=c"), vec![("K".into(), "a=b=c".into())]);
849    }
850
851    #[test]
852    fn key_with_underscore() {
853        assert_eq!(parse_ok("MY_KEY=val"), vec![("MY_KEY".into(), "val".into())]);
854    }
855
856    #[test]
857    fn key_with_dot() {
858        assert_eq!(parse_ok("my.key=val"), vec![("my.key".into(), "val".into())]);
859    }
860
861    #[test]
862    fn key_with_hyphen() {
863        assert_eq!(parse_ok("my-key=val"), vec![("my-key".into(), "val".into())]);
864    }
865
866    #[test]
867    fn key_with_digits() {
868        assert_eq!(parse_ok("KEY123=val"), vec![("KEY123".into(), "val".into())]);
869    }
870
871    #[test]
872    fn key_mixed() {
873        assert_eq!(parse_ok("A1.b-C_2=val"), vec![("A1.b-C_2".into(), "val".into())]);
874    }
875
876    #[test]
877    fn key_starting_with_hyphen() {
878        assert_eq!(parse_ok("-KEY=v"), vec![("-KEY".into(), "v".into())]);
879    }
880
881    #[test]
882    fn key_starting_with_dot() {
883        assert_eq!(parse_ok(".KEY=v"), vec![(".KEY".into(), "v".into())]);
884    }
885
886    #[test]
887    fn key_starting_with_underscore() {
888        assert_eq!(parse_ok("_KEY=v"), vec![("_KEY".into(), "v".into())]);
889    }
890
891    #[test]
892    fn key_only_dots() {
893        assert_eq!(parse_ok("...=value"), vec![("...".into(), "value".into())]);
894    }
895
896    #[test]
897    fn key_only_hyphens() {
898        assert_eq!(parse_ok("---=value"), vec![("---".into(), "value".into())]);
899    }
900
901    // ── Double-quoted values ───────────────────────────────────────────────
902
903    #[test]
904    fn double_quoted_value() {
905        assert_eq!(parse_ok("K=\"hello\""), vec![("K".into(), "hello".into())]);
906    }
907
908    #[test]
909    fn double_quoted_with_spaces() {
910        assert_eq!(parse_ok("K=\"hello world\""), vec![("K".into(), "hello world".into())]);
911    }
912
913    #[test]
914    fn double_quoted_empty() {
915        assert_eq!(parse_ok("K=\"\""), vec![("K".into(), "".into())]);
916    }
917
918    #[test]
919    fn double_quoted_hash_preserved() {
920        assert_eq!(parse_ok("K=\"a#b\""), vec![("K".into(), "a#b".into())]);
921    }
922
923    #[test]
924    fn double_quoted_equals_inside() {
925        assert_eq!(parse_ok("K=\"a=b\""), vec![("K".into(), "a=b".into())]);
926    }
927
928    #[test]
929    fn double_quoted_single_quotes_inside() {
930        assert_eq!(parse_ok("K=\"it's ok\""), vec![("K".into(), "it's ok".into())]);
931    }
932
933    #[test]
934    fn double_quoted_whitespace_preserved() {
935        assert_eq!(parse_ok("K=\" hello \""), vec![("K".into(), " hello ".into())]);
936    }
937
938    #[test]
939    fn double_quoted_trailing_content_error() {
940        assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
941    }
942
943    #[test]
944    fn double_quoted_trailing_comment_allowed() {
945        assert_eq!(parse_ok("K=\"hello\" # comment"), vec![("K".into(), "hello".into())]);
946    }
947
948    #[test]
949    fn double_quoted_trailing_whitespace_allowed() {
950        assert_eq!(parse_ok("K=\"hello\"   "), vec![("K".into(), "hello".into())]);
951    }
952
953    // ── Escape sequences (double quotes only) ──────────────────────────────
954
955    #[test]
956    fn escaped_newline() {
957        assert_eq!(parse_ok("K=\"a\\nb\""), vec![("K".into(), "a\nb".into())]);
958    }
959
960    #[test]
961    fn escaped_tab_and_cr() {
962        assert_eq!(parse_ok("K=\"a\\tb\\rc\""), vec![("K".into(), "a\tb\rc".into())]);
963    }
964
965    #[test]
966    fn escaped_quote() {
967        assert_eq!(parse_ok("K=\"a\\\"b\""), vec![("K".into(), "a\"b".into())]);
968    }
969
970    #[test]
971    fn escaped_backslash() {
972        assert_eq!(parse_ok("K=\"a\\\\b\""), vec![("K".into(), "a\\b".into())]);
973    }
974
975    #[test]
976    fn escaped_single_quote_and_dollar() {
977        assert_eq!(parse_ok("K=\"\\'\\$\""), vec![("K".into(), "'$".into())]);
978    }
979
980    #[test]
981    fn escaped_quote_at_end() {
982        assert_eq!(parse_ok("K=\"a\\\"\""), vec![("K".into(), "a\"".into())]);
983    }
984
985    #[test]
986    fn invalid_escape_is_error() {
987        assert_eq!(parse_kind("K=\"a\\qb\""), ParseErrorKind::InvalidEscape);
988        assert_eq!(parse_kind("K=\"a\\8\""), ParseErrorKind::InvalidEscape);
989    }
990
991    #[test]
992    fn backslash_literal_in_single_quotes() {
993        assert_eq!(parse_ok("K='C:\\path\\to'"), vec![("K".into(), "C:\\path\\to".into())]);
994    }
995
996    #[test]
997    fn backslash_literal_unquoted() {
998        assert_eq!(parse_ok("K=C:\\path\\to"), vec![("K".into(), "C:\\path\\to".into())]);
999    }
1000
1001    #[test]
1002    fn escape_error_line_number() {
1003        assert_eq!(parse_line("A=1\nB=\"x\\q\""), 2);
1004    }
1005
1006    // ── Single-quoted values ───────────────────────────────────────────────
1007
1008    #[test]
1009    fn single_quoted_value() {
1010        assert_eq!(parse_ok("K='hello'"), vec![("K".into(), "hello".into())]);
1011    }
1012
1013    #[test]
1014    fn single_quoted_with_spaces() {
1015        assert_eq!(parse_ok("K='hello world'"), vec![("K".into(), "hello world".into())]);
1016    }
1017
1018    #[test]
1019    fn single_quoted_empty() {
1020        assert_eq!(parse_ok("K=''"), vec![("K".into(), "".into())]);
1021    }
1022
1023    #[test]
1024    fn single_quoted_hash_preserved() {
1025        assert_eq!(parse_ok("K='a#b'"), vec![("K".into(), "a#b".into())]);
1026    }
1027
1028    #[test]
1029    fn single_quoted_double_quotes_inside() {
1030        assert_eq!(parse_ok(r#"K='"hello"'"#), vec![("K".into(), r#""hello""#.into())]);
1031    }
1032
1033    #[test]
1034    fn single_quoted_whitespace_preserved() {
1035        assert_eq!(parse_ok("K=' hello '"), vec![("K".into(), " hello ".into())]);
1036    }
1037
1038    #[test]
1039    fn single_quoted_trailing_content_error() {
1040        assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
1041    }
1042
1043    #[test]
1044    fn single_quoted_trailing_comment_allowed() {
1045        assert_eq!(parse_ok("K='hello' # comment"), vec![("K".into(), "hello".into())]);
1046    }
1047
1048    // ── Quoted example from the spec ────────────────────────────────────────
1049
1050    #[test]
1051    fn quoted_nested_example() {
1052        assert_eq!(parse_ok("HELLO='\"hello\"'"), vec![("HELLO".into(), "\"hello\"".into())]);
1053    }
1054
1055    // ── Unquoted values ────────────────────────────────────────────────────
1056
1057    #[test]
1058    fn unquoted_hash_is_comment() {
1059        assert_eq!(parse_ok("K=val # comment"), vec![("K".into(), "val".into())]);
1060    }
1061
1062    #[test]
1063    fn unquoted_hash_no_space_not_comment() {
1064        assert_eq!(parse_ok("K=val#comment"), vec![("K".into(), "val#comment".into())]);
1065    }
1066
1067    #[test]
1068    fn unquoted_trimmed() {
1069        assert_eq!(parse_ok("K=  val  "), vec![("K".into(), "val".into())]);
1070    }
1071
1072    #[test]
1073    fn unquoted_value_with_internal_spaces() {
1074        assert_eq!(parse_ok("K=hello world"), vec![("K".into(), "hello world".into())]);
1075    }
1076
1077    #[test]
1078    fn unquoted_trailing_spaces_before_comment() {
1079        assert_eq!(parse_ok("K=val   # comment"), vec![("K".into(), "val".into())]);
1080    }
1081
1082    #[test]
1083    fn unquoted_value_with_numbers() {
1084        assert_eq!(parse_ok("PORT=8080"), vec![("PORT".into(), "8080".into())]);
1085    }
1086
1087    #[test]
1088    fn unquoted_value_with_dots() {
1089        assert_eq!(parse_ok("HOST=192.168.1.1"), vec![("HOST".into(), "192.168.1.1".into())]);
1090    }
1091
1092    #[test]
1093    fn unquoted_value_containing_quote() {
1094        assert_eq!(parse_ok("K=hello\"there"), vec![("K".into(), "hello\"there".into())]);
1095    }
1096
1097    #[test]
1098    fn unquoted_value_containing_only_hash() {
1099        assert_eq!(parse_ok("K=#"), vec![("K".into(), "#".into())]);
1100    }
1101
1102    #[test]
1103    fn unquoted_value_hash_without_preceding_space() {
1104        assert_eq!(parse_ok("K=val#ue"), vec![("K".into(), "val#ue".into())]);
1105    }
1106
1107    #[test]
1108    fn unquoted_hash_with_preceding_space_is_comment() {
1109        assert_eq!(parse_ok("K=val #ue"), vec![("K".into(), "val".into())]);
1110    }
1111
1112    // ── Empty values ───────────────────────────────────────────────────────
1113
1114    #[test]
1115    fn empty_value_no_quotes() {
1116        assert_eq!(parse_ok("K="), vec![("K".into(), "".into())]);
1117    }
1118
1119    #[test]
1120    fn empty_value_trailing_spaces() {
1121        assert_eq!(parse_ok("K=   "), vec![("K".into(), "".into())]);
1122    }
1123
1124    #[test]
1125    fn empty_value_spaces_before_comment() {
1126        assert_eq!(parse_ok("K=   # comment"), vec![("K".into(), "".into())]);
1127    }
1128
1129    #[test]
1130    fn empty_double_quoted_value_with_comment() {
1131        assert_eq!(parse_ok("K=\"\" # comment"), vec![("K".into(), "".into())]);
1132    }
1133
1134    // ── Whitespace handling ────────────────────────────────────────────────
1135
1136    #[test]
1137    fn leading_whitespace_on_line() {
1138        assert_eq!(parse_ok("  K=v"), vec![("K".into(), "v".into())]);
1139    }
1140
1141    #[test]
1142    fn trailing_whitespace_before_equals() {
1143        assert_eq!(parse_ok("K  =v"), vec![("K".into(), "v".into())]);
1144    }
1145
1146    #[test]
1147    fn whitespace_around_equals() {
1148        assert_eq!(parse_ok("K = v"), vec![("K".into(), "v".into())]);
1149    }
1150
1151    #[test]
1152    fn tabs_as_whitespace() {
1153        assert_eq!(parse_ok("\tK\t=\tv"), vec![("K".into(), "v".into())]);
1154    }
1155
1156    #[test]
1157    fn tab_after_equals() {
1158        assert_eq!(parse_ok("K=\tval"), vec![("K".into(), "val".into())]);
1159    }
1160
1161    #[test]
1162    fn double_equals_value() {
1163        assert_eq!(parse_ok("K==v"), vec![("K".into(), "=v".into())]);
1164    }
1165
1166    // ── Comments ───────────────────────────────────────────────────────────
1167
1168    #[test]
1169    fn full_line_comment() {
1170        assert!(parse_ok("# this is a comment").is_empty());
1171    }
1172
1173    #[test]
1174    fn comment_with_leading_spaces() {
1175        assert!(parse_ok("  # indented comment").is_empty());
1176    }
1177
1178    #[test]
1179    fn empty_lines_skipped() {
1180        assert!(parse_ok("\n\n\n").is_empty());
1181    }
1182
1183    #[test]
1184    fn mixed_comments_and_values() {
1185        let pairs = parse_ok("# header\nA=1\n\nB=2 # inline\n");
1186        assert_eq!(pairs, vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
1187    }
1188
1189    // ── Line endings ───────────────────────────────────────────────────────
1190
1191    #[test]
1192    fn unix_line_endings() {
1193        assert_eq!(parse_ok("A=1\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
1194    }
1195
1196    #[test]
1197    fn crlf_line_endings() {
1198        assert_eq!(parse_ok("A=1\r\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
1199    }
1200
1201    #[test]
1202    fn no_trailing_newline() {
1203        assert_eq!(parse_ok("A=1"), vec![("A".into(), "1".into())]);
1204    }
1205
1206    #[test]
1207    fn single_line_no_newline() {
1208        assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
1209    }
1210
1211    #[test]
1212    fn lone_cr_is_not_a_line_break() {
1213        // Documented limitation: only \n and \r\n split lines.
1214        assert_eq!(parse_ok("A=1\rB=2"), vec![("A".into(), "1\rB=2".into())]);
1215    }
1216
1217    #[test]
1218    fn cr_cr_lf() {
1219        assert_eq!(
1220            parse_ok("A=1\r\r\nB=2"),
1221            vec![("A".into(), "1".into()), ("B".into(), "2".into())]
1222        );
1223    }
1224
1225    // ── BOM ────────────────────────────────────────────────────────────────
1226
1227    #[test]
1228    fn bom_is_stripped() {
1229        assert_eq!(parse_ok("\u{FEFF}K=v"), vec![("K".into(), "v".into())]);
1230    }
1231
1232    #[test]
1233    fn bom_only_file() {
1234        assert!(parse_ok("\u{FEFF}").is_empty());
1235    }
1236
1237    #[test]
1238    fn bom_with_comment_first_line() {
1239        assert_eq!(parse_ok("\u{FEFF}# c\nK=v"), vec![("K".into(), "v".into())]);
1240    }
1241
1242    // ── NUL bytes ──────────────────────────────────────────────────────────
1243
1244    #[test]
1245    fn nul_in_unquoted_value_is_error() {
1246        assert_eq!(parse_kind("K=a\0b"), ParseErrorKind::InvalidValue);
1247    }
1248
1249    #[test]
1250    fn nul_in_double_quoted_value_is_error() {
1251        assert_eq!(parse_kind("K=\"a\0b\""), ParseErrorKind::InvalidValue);
1252    }
1253
1254    #[test]
1255    fn nul_in_single_quoted_value_is_error() {
1256        assert_eq!(parse_kind("K='a\0b'"), ParseErrorKind::InvalidValue);
1257    }
1258
1259    #[test]
1260    fn nul_in_key_is_invalid_key() {
1261        assert_eq!(parse_kind("K\0J=v"), ParseErrorKind::InvalidKey);
1262    }
1263
1264    #[cfg(not(target_arch = "wasm32"))]
1265    #[test]
1266    fn load_with_nul_value_errors_without_panicking() {
1267        let _guard = TEST_LOCK.lock().unwrap();
1268        let dir = unique_dir("nul");
1269        fs::create_dir_all(&dir).unwrap();
1270        let env_path = dir.join(".env");
1271        fs::write(&env_path, "DOTENV_NUL=a\0b").unwrap();
1272
1273        let old = env::current_dir().ok();
1274        env::set_current_dir(&dir).unwrap();
1275        let result = load();
1276        if let Some(p) = old {
1277            let _ = env::set_current_dir(p);
1278        }
1279        let _ = fs::remove_dir_all(&dir);
1280
1281        match result.unwrap_err() {
1282            Error::Parse(e) => assert_eq!(e.kind, ParseErrorKind::InvalidValue),
1283            other => panic!("expected parse error, got {other:?}"),
1284        }
1285    }
1286
1287    // ── Edge cases: empty / comment-only files ─────────────────────────────
1288
1289    #[test]
1290    fn empty_file() {
1291        assert!(parse_ok("").is_empty());
1292    }
1293
1294    #[test]
1295    fn only_comments() {
1296        assert!(parse_ok("# a\n# b\n# c").is_empty());
1297    }
1298
1299    #[test]
1300    fn only_blank_lines() {
1301        assert!(parse_ok("\n\n \n\t\n").is_empty());
1302    }
1303
1304    // ── Error cases ────────────────────────────────────────────────────────
1305
1306    #[test]
1307    fn error_missing_equals() {
1308        assert_eq!(parse_kind("INVALID"), ParseErrorKind::MissingEquals);
1309    }
1310
1311    #[test]
1312    fn error_missing_equals_with_comment() {
1313        assert_eq!(parse_kind("K # comment"), ParseErrorKind::MissingEquals);
1314    }
1315
1316    #[test]
1317    fn error_empty_key() {
1318        assert_eq!(parse_kind("=value"), ParseErrorKind::EmptyKey);
1319    }
1320
1321    #[test]
1322    fn error_empty_key_with_spaces() {
1323        assert_eq!(parse_kind("   =value"), ParseErrorKind::EmptyKey);
1324    }
1325
1326    #[test]
1327    fn error_unmatched_double_quote() {
1328        assert_eq!(parse_kind("K=\"hello"), ParseErrorKind::UnmatchedQuote);
1329    }
1330
1331    #[test]
1332    fn error_unmatched_single_quote() {
1333        assert_eq!(parse_kind("K='hello"), ParseErrorKind::UnmatchedQuote);
1334    }
1335
1336    #[test]
1337    fn error_unmatched_double_quote_with_hash() {
1338        assert_eq!(parse_kind("K=\"hello#more"), ParseErrorKind::UnmatchedQuote);
1339    }
1340
1341    #[test]
1342    fn error_trailing_content_double_quote() {
1343        assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
1344    }
1345
1346    #[test]
1347    fn error_trailing_content_single_quote() {
1348        assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
1349    }
1350
1351    #[test]
1352    fn error_trailing_content_line_number() {
1353        assert_eq!(parse_line("A=1\nK=\"v\"x\nB=2"), 2);
1354    }
1355
1356    #[test]
1357    fn error_invalid_key_exclamation() {
1358        assert_eq!(parse_kind("K!EY=v"), ParseErrorKind::InvalidKey);
1359    }
1360
1361    #[test]
1362    fn error_invalid_key_dollar() {
1363        assert_eq!(parse_kind("\u{0024}KEY=v"), ParseErrorKind::InvalidKey);
1364    }
1365
1366    #[test]
1367    fn error_invalid_key_at() {
1368        assert_eq!(parse_kind("KEY@=v"), ParseErrorKind::InvalidKey);
1369    }
1370
1371    #[test]
1372    fn error_invalid_key_space() {
1373        assert_eq!(parse_kind("K EY=v"), ParseErrorKind::InvalidKey);
1374    }
1375
1376    #[test]
1377    fn error_invalid_key_slash() {
1378        assert_eq!(parse_kind("KEY/VAL=v"), ParseErrorKind::InvalidKey);
1379    }
1380
1381    #[test]
1382    fn error_invalid_key_unicode() {
1383        assert_eq!(parse_kind("K\u{00C9}Y=v"), ParseErrorKind::InvalidKey);
1384    }
1385
1386    #[test]
1387    fn error_line_number_missing_equals() {
1388        assert_eq!(parse_line("A=1\nINVALID\nB=2"), 2);
1389    }
1390
1391    #[test]
1392    fn error_line_number_invalid_key() {
1393        assert_eq!(parse_line("A=1\n\"$\"BAD=v\nB=2"), 2);
1394    }
1395
1396    #[test]
1397    fn error_line_number_unmatched_quote() {
1398        assert_eq!(parse_line("A=1\nK=\"unclosed\nB=2"), 2);
1399    }
1400
1401    // ── Unicode values ─────────────────────────────────────────────────────
1402
1403    #[test]
1404    fn unicode_value_unquoted() {
1405        assert_eq!(parse_ok("K=h\u{00E9}llo"), vec![("K".into(), "h\u{00E9}llo".into())]);
1406    }
1407
1408    #[test]
1409    fn unicode_value_double_quoted() {
1410        assert_eq!(parse_ok("K=\"h\u{00E9}llo\""), vec![("K".into(), "h\u{00E9}llo".into())]);
1411    }
1412
1413    #[test]
1414    fn unicode_value_single_quoted() {
1415        assert_eq!(parse_ok("K='h\u{00E9}llo'"), vec![("K".into(), "h\u{00E9}llo".into())]);
1416    }
1417
1418    // ── Public `parse` (map) ───────────────────────────────────────────────
1419
1420    #[test]
1421    fn parse_map_first_wins() {
1422        let vars = parse("A=1\nA=2\nB=3").unwrap();
1423        assert_eq!(vars["A"].as_ref(), "1");
1424        assert_eq!(vars["B"].as_ref(), "3");
1425        assert_eq!(vars.len(), 2);
1426    }
1427
1428    #[test]
1429    fn parse_map_empty() {
1430        assert!(parse("# c\n\n").unwrap().is_empty());
1431    }
1432
1433    #[test]
1434    fn parse_map_expands_escapes() {
1435        let vars = parse("K=\"a\\nb\"").unwrap();
1436        assert_eq!(vars["K"].as_ref(), "a\nb");
1437    }
1438
1439    #[test]
1440    fn parse_borrows_plain_values_and_owns_escaped() {
1441        let vars = parse("A=plain\nB=\"x\\ny\"").unwrap();
1442        assert_eq!(vars["A"], Cow::Borrowed("plain"));
1443        assert!(matches!(&vars["B"], Cow::Owned(_)));
1444    }
1445
1446    #[test]
1447    fn parse_map_error() {
1448        assert!(parse("=").is_err());
1449    }
1450
1451    // ── `load()` / `from_path()` integration tests ─────────────────────────
1452
1453    #[cfg(not(target_arch = "wasm32"))]
1454    fn unique_dir(tag: &str) -> std::path::PathBuf {
1455        env::temp_dir().join(format!("dotenv_test_{tag}_{}", std::process::id()))
1456    }
1457
1458    // `env::temp_dir()` panics on WASI, so these tests cannot run on wasm32.
1459    #[cfg(not(target_arch = "wasm32"))]
1460    #[test]
1461    fn load_sets_vars() {
1462        let _guard = TEST_LOCK.lock().unwrap();
1463        let dir = unique_dir("basic");
1464        let _ = fs::create_dir_all(&dir);
1465        let env_path = dir.join(".env");
1466        fs::write(&env_path, "DOTENV_TEST_FOO=bar\nDOTENV_TEST_BAZ=qux").unwrap();
1467
1468        let old = env::current_dir().ok();
1469        env::set_current_dir(&dir).unwrap();
1470
1471        let result = load();
1472
1473        if let Some(p) = old {
1474            let _ = env::set_current_dir(p);
1475        }
1476        let _ = fs::remove_file(&env_path);
1477        let _ = fs::remove_dir(&dir);
1478
1479        assert!(result.is_ok());
1480        assert_eq!(env::var("DOTENV_TEST_FOO").unwrap(), "bar");
1481        assert_eq!(env::var("DOTENV_TEST_BAZ").unwrap(), "qux");
1482
1483        unsafe { remove_env("DOTENV_TEST_FOO") };
1484        unsafe { remove_env("DOTENV_TEST_BAZ") };
1485    }
1486
1487    #[cfg(not(target_arch = "wasm32"))]
1488    #[test]
1489    fn from_path_sets_vars() {
1490        let _guard = TEST_LOCK.lock().unwrap();
1491        let dir = unique_dir("from_path");
1492        let _ = fs::create_dir_all(&dir);
1493        let env_path = dir.join("custom.env");
1494        fs::write(&env_path, "DOTENV_FROM_PATH=yes").unwrap();
1495
1496        let result = from_path(&env_path);
1497
1498        let _ = fs::remove_file(&env_path);
1499        let _ = fs::remove_dir(&dir);
1500
1501        assert!(result.is_ok());
1502        assert_eq!(env::var("DOTENV_FROM_PATH").unwrap(), "yes");
1503        unsafe { remove_env("DOTENV_FROM_PATH") };
1504    }
1505
1506    #[cfg(not(target_arch = "wasm32"))]
1507    #[test]
1508    fn load_preserves_existing_env_vars() {
1509        let _guard = TEST_LOCK.lock().unwrap();
1510        unsafe { set_env("DOTENV_EXISTING", "original") };
1511
1512        let dir = unique_dir("existing");
1513        let _ = fs::create_dir_all(&dir);
1514        let env_path = dir.join(".env");
1515        fs::write(&env_path, "DOTENV_EXISTING=from_file").unwrap();
1516
1517        let old = env::current_dir().ok();
1518        env::set_current_dir(&dir).unwrap();
1519
1520        let result = load();
1521
1522        if let Some(p) = old {
1523            let _ = env::set_current_dir(p);
1524        }
1525        let _ = fs::remove_file(&env_path);
1526        let _ = fs::remove_dir(&dir);
1527
1528        assert!(result.is_ok());
1529        assert_eq!(env::var("DOTENV_EXISTING").unwrap(), "original");
1530
1531        unsafe { remove_env("DOTENV_EXISTING") };
1532    }
1533
1534    #[cfg(not(target_arch = "wasm32"))]
1535    #[test]
1536    fn load_first_declaration_wins() {
1537        let _guard = TEST_LOCK.lock().unwrap();
1538        let dir = unique_dir("first");
1539        let _ = fs::create_dir_all(&dir);
1540        let env_path = dir.join(".env");
1541        fs::write(&env_path, "DOTENV_DUP=first\nDOTENV_DUP=second").unwrap();
1542
1543        let old = env::current_dir().ok();
1544        env::set_current_dir(&dir).unwrap();
1545
1546        let result = load();
1547
1548        if let Some(p) = old {
1549            let _ = env::set_current_dir(p);
1550        }
1551        let _ = fs::remove_file(&env_path);
1552        let _ = fs::remove_dir(&dir);
1553
1554        assert!(result.is_ok());
1555        assert_eq!(env::var("DOTENV_DUP").unwrap(), "first");
1556
1557        unsafe { remove_env("DOTENV_DUP") };
1558    }
1559
1560    #[cfg(not(target_arch = "wasm32"))]
1561    #[test]
1562    fn load_parse_error_is_atomic() {
1563        let _guard = TEST_LOCK.lock().unwrap();
1564        let dir = unique_dir("atomic");
1565        let _ = fs::create_dir_all(&dir);
1566        let env_path = dir.join(".env");
1567        // A valid line precedes a malformed one.
1568        fs::write(&env_path, "DOTENV_ATOMIC=set\nMALFORMED LINE\n").unwrap();
1569
1570        let old = env::current_dir().ok();
1571        env::set_current_dir(&dir).unwrap();
1572        let result = load();
1573        if let Some(p) = old {
1574            let _ = env::set_current_dir(p);
1575        }
1576        let _ = fs::remove_file(&env_path);
1577        let _ = fs::remove_dir(&dir);
1578
1579        assert!(matches!(result, Err(Error::Parse(_))));
1580        assert!(
1581            env::var_os("DOTENV_ATOMIC").is_none(),
1582            "variable from before the error must not be set"
1583        );
1584    }
1585
1586    #[cfg(not(target_arch = "wasm32"))]
1587    #[test]
1588    fn load_file_not_found() {
1589        let _guard = TEST_LOCK.lock().unwrap();
1590        let dir = unique_dir("missing");
1591        let _ = fs::create_dir_all(&dir);
1592
1593        let old = env::current_dir().ok();
1594        env::set_current_dir(&dir).unwrap();
1595
1596        let result = load();
1597
1598        if let Some(p) = old {
1599            let _ = env::set_current_dir(p);
1600        }
1601        let _ = fs::remove_dir(&dir);
1602
1603        match result.unwrap_err() {
1604            Error::Io(_) => {}
1605            _ => panic!("expected Io error"),
1606        }
1607    }
1608
1609    #[cfg(not(target_arch = "wasm32"))]
1610    #[test]
1611    fn load_parse_error() {
1612        let _guard = TEST_LOCK.lock().unwrap();
1613        let dir = unique_dir("parse_err");
1614        let _ = fs::create_dir_all(&dir);
1615        let env_path = dir.join(".env");
1616        fs::write(&env_path, "A=1\nMALFORMED\nB=2").unwrap();
1617
1618        let old = env::current_dir().ok();
1619        env::set_current_dir(&dir).unwrap();
1620
1621        let result = load();
1622
1623        if let Some(p) = old {
1624            let _ = env::set_current_dir(p);
1625        }
1626        let _ = fs::remove_file(&env_path);
1627        let _ = fs::remove_dir(&dir);
1628
1629        match result.unwrap_err() {
1630            Error::Parse(e) => assert_eq!(e.line, 2),
1631            other => panic!("expected Parse error, got {other:?}"),
1632        }
1633    }
1634
1635    // A non-UTF-8 variable in the environment must not make `load` panic.
1636    #[cfg(unix)]
1637    #[test]
1638    fn load_survives_non_utf8_env_var() {
1639        use std::os::unix::ffi::OsStringExt;
1640
1641        let _guard = TEST_LOCK.lock().unwrap();
1642        let dir = unique_dir("nonutf8");
1643        let _ = fs::create_dir_all(&dir);
1644        let env_path = dir.join(".env");
1645        fs::write(&env_path, "DOTENV_NONUTF8_OK=1").unwrap();
1646
1647        let bad_key = OsString::from_vec(b"DOTENV_BAD\xFF".to_vec());
1648        let bad_val = OsString::from_vec(vec![0xfe, 0xff]);
1649        unsafe { env::set_var(&bad_key, &bad_val) };
1650
1651        let old = env::current_dir().ok();
1652        env::set_current_dir(&dir).unwrap();
1653        let result = std::panic::catch_unwind(load);
1654        if let Some(p) = old {
1655            let _ = env::set_current_dir(p);
1656        }
1657        let _ = fs::remove_file(&env_path);
1658        let _ = fs::remove_dir(&dir);
1659        unsafe { env::remove_var(&bad_key) };
1660
1661        assert!(result.is_ok(), "load() panicked with a non-UTF-8 env var present");
1662        assert_eq!(env::var("DOTENV_NONUTF8_OK").unwrap(), "1");
1663        unsafe { remove_env("DOTENV_NONUTF8_OK") };
1664    }
1665}