1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use std::{collections::HashSet, env, fmt, fmt::Display, fs, io, str::FromStr};
143
144use memchr::memchr;
145
146#[derive(Debug)]
148pub enum Error {
149 Io(io::Error),
151 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#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct ParseError {
182 pub line: usize,
184 pub kind: ParseErrorKind,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum ParseErrorKind {
191 MissingEquals,
193 UnmatchedQuote,
195 EmptyKey,
197 InvalidKey,
200 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
216pub 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 unsafe { env::set_var(key, value) };
252 }
253 }
254 Ok(())
255}
256
257fn 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
302fn 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
316fn 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
366pub use dotenv_derive::FromEnv;
371
372#[derive(Debug, Clone)]
388pub enum FromEnvError {
389 Missing(String),
391 Invalid {
393 var: String,
395 value: String,
397 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
433pub trait FromEnv: Sized {
437 fn from_env() -> Result<Self, FromEnvError> {
439 Self::from_env_with_prefix("")
440 }
441
442 fn from_env_with_prefix(prefix: &str) -> Result<Self, FromEnvError>;
448}
449
450pub trait FromEnvValue: Sized {
470 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
483pub 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
515pub fn from_env<T: FromEnv>() -> Result<T, FromEnvError> {
526 T::from_env()
527}
528
529#[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 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 #[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 #[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 #[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 #[test]
731 fn quoted_nested_example() {
732 assert_eq!(parse_ok("HELLO='\"hello\"'"), vec![("HELLO".into(), "\"hello\"".into())]);
733 }
734
735 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}