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 #[cfg(not(target_arch = "wasm32"))]
558 unsafe fn set_env(k: &str, v: &str) {
559 unsafe { env::set_var(k, v) };
560 }
561
562 #[cfg(not(target_arch = "wasm32"))]
563 unsafe fn remove_env(k: &str) {
564 unsafe { env::remove_var(k) };
565 }
566
567 #[test]
570 fn simple_key_value() {
571 assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
572 }
573
574 #[test]
575 fn multiple_pairs() {
576 let pairs = parse_ok("A=1\nB=2\nC=3");
577 assert_eq!(
578 pairs,
579 vec![
580 ("A".into(), "1".into()),
581 ("B".into(), "2".into()),
582 ("C".into(), "3".into()),
583 ]
584 );
585 }
586
587 #[test]
588 fn value_with_equals() {
589 assert_eq!(parse_ok("K=a=b=c"), vec![("K".into(), "a=b=c".into())]);
590 }
591
592 #[test]
593 fn key_with_underscore() {
594 assert_eq!(parse_ok("MY_KEY=val"), vec![("MY_KEY".into(), "val".into())]);
595 }
596
597 #[test]
598 fn key_with_dot() {
599 assert_eq!(parse_ok("my.key=val"), vec![("my.key".into(), "val".into())]);
600 }
601
602 #[test]
603 fn key_with_hyphen() {
604 assert_eq!(parse_ok("my-key=val"), vec![("my-key".into(), "val".into())]);
605 }
606
607 #[test]
608 fn key_with_digits() {
609 assert_eq!(parse_ok("KEY123=val"), vec![("KEY123".into(), "val".into())]);
610 }
611
612 #[test]
613 fn key_mixed() {
614 assert_eq!(parse_ok("A1.b-C_2=val"), vec![("A1.b-C_2".into(), "val".into())]);
615 }
616
617 #[test]
618 fn key_starting_with_hyphen() {
619 assert_eq!(parse_ok("-KEY=v"), vec![("-KEY".into(), "v".into())]);
620 }
621
622 #[test]
623 fn key_starting_with_dot() {
624 assert_eq!(parse_ok(".KEY=v"), vec![(".KEY".into(), "v".into())]);
625 }
626
627 #[test]
628 fn key_starting_with_underscore() {
629 assert_eq!(parse_ok("_KEY=v"), vec![("_KEY".into(), "v".into())]);
630 }
631
632 #[test]
633 fn key_only_dots() {
634 assert_eq!(parse_ok("...=value"), vec![("...".into(), "value".into())]);
635 }
636
637 #[test]
638 fn key_only_hyphens() {
639 assert_eq!(parse_ok("---=value"), vec![("---".into(), "value".into())]);
640 }
641
642 #[test]
645 fn double_quoted_value() {
646 assert_eq!(parse_ok("K=\"hello\""), vec![("K".into(), "hello".into())]);
647 }
648
649 #[test]
650 fn double_quoted_with_spaces() {
651 assert_eq!(parse_ok("K=\"hello world\""), vec![("K".into(), "hello world".into())]);
652 }
653
654 #[test]
655 fn double_quoted_empty() {
656 assert_eq!(parse_ok("K=\"\""), vec![("K".into(), "".into())]);
657 }
658
659 #[test]
660 fn double_quoted_hash_preserved() {
661 assert_eq!(parse_ok("K=\"a#b\""), vec![("K".into(), "a#b".into())]);
662 }
663
664 #[test]
665 fn double_quoted_equals_inside() {
666 assert_eq!(parse_ok("K=\"a=b\""), vec![("K".into(), "a=b".into())]);
667 }
668
669 #[test]
670 fn double_quoted_single_quotes_inside() {
671 assert_eq!(parse_ok("K=\"it's ok\""), vec![("K".into(), "it's ok".into())]);
672 }
673
674 #[test]
675 fn double_quoted_whitespace_preserved() {
676 assert_eq!(parse_ok("K=\" hello \""), vec![("K".into(), " hello ".into())]);
677 }
678
679 #[test]
680 fn double_quoted_trailing_content_error() {
681 assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
682 }
683
684 #[test]
685 fn double_quoted_trailing_comment_allowed() {
686 assert_eq!(parse_ok("K=\"hello\" # comment"), vec![("K".into(), "hello".into())]);
687 }
688
689 #[test]
692 fn single_quoted_value() {
693 assert_eq!(parse_ok("K='hello'"), vec![("K".into(), "hello".into())]);
694 }
695
696 #[test]
697 fn single_quoted_with_spaces() {
698 assert_eq!(parse_ok("K='hello world'"), vec![("K".into(), "hello world".into())]);
699 }
700
701 #[test]
702 fn single_quoted_empty() {
703 assert_eq!(parse_ok("K=''"), vec![("K".into(), "".into())]);
704 }
705
706 #[test]
707 fn single_quoted_hash_preserved() {
708 assert_eq!(parse_ok("K='a#b'"), vec![("K".into(), "a#b".into())]);
709 }
710
711 #[test]
712 fn single_quoted_double_quotes_inside() {
713 assert_eq!(parse_ok(r#"K='"hello"'"#), vec![("K".into(), r#""hello""#.into())]);
714 }
715
716 #[test]
717 fn single_quoted_whitespace_preserved() {
718 assert_eq!(parse_ok("K=' hello '"), vec![("K".into(), " hello ".into())]);
719 }
720
721 #[test]
722 fn single_quoted_trailing_content_error() {
723 assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
724 }
725
726 #[test]
727 fn single_quoted_trailing_comment_allowed() {
728 assert_eq!(parse_ok("K='hello' # comment"), vec![("K".into(), "hello".into())]);
729 }
730
731 #[test]
734 fn quoted_nested_example() {
735 assert_eq!(parse_ok("HELLO='\"hello\"'"), vec![("HELLO".into(), "\"hello\"".into())]);
736 }
737
738 #[test]
741 fn unquoted_hash_is_comment() {
742 assert_eq!(parse_ok("K=val # comment"), vec![("K".into(), "val".into())]);
743 }
744
745 #[test]
746 fn unquoted_hash_no_space_not_comment() {
747 assert_eq!(parse_ok("K=val#comment"), vec![("K".into(), "val#comment".into())]);
748 }
749
750 #[test]
751 fn unquoted_trimmed() {
752 assert_eq!(parse_ok("K= val "), vec![("K".into(), "val".into())]);
753 }
754
755 #[test]
756 fn unquoted_trailing_spaces_before_comment() {
757 assert_eq!(parse_ok("K=val # comment"), vec![("K".into(), "val".into())]);
758 }
759
760 #[test]
761 fn unquoted_value_with_numbers() {
762 assert_eq!(parse_ok("PORT=8080"), vec![("PORT".into(), "8080".into())]);
763 }
764
765 #[test]
766 fn unquoted_value_with_dots() {
767 assert_eq!(parse_ok("HOST=192.168.1.1"), vec![("HOST".into(), "192.168.1.1".into())]);
768 }
769
770 #[test]
771 fn unquoted_value_containing_quote() {
772 assert_eq!(parse_ok("K=hello\"there"), vec![("K".into(), "hello\"there".into())]);
773 }
774
775 #[test]
776 fn unquoted_value_containing_only_hash() {
777 assert_eq!(parse_ok("K=#"), vec![("K".into(), "#".into())]);
778 }
779
780 #[test]
781 fn unquoted_value_hash_without_preceding_space() {
782 assert_eq!(parse_ok("K=val#ue"), vec![("K".into(), "val#ue".into())]);
783 }
784
785 #[test]
786 fn unquoted_hash_with_preceding_space_is_comment() {
787 assert_eq!(parse_ok("K=val #ue"), vec![("K".into(), "val".into())]);
788 }
789
790 #[test]
793 fn empty_value_no_quotes() {
794 assert_eq!(parse_ok("K="), vec![("K".into(), "".into())]);
795 }
796
797 #[test]
798 fn empty_value_trailing_spaces() {
799 assert_eq!(parse_ok("K= "), vec![("K".into(), "".into())]);
800 }
801
802 #[test]
803 fn empty_value_spaces_before_comment() {
804 assert_eq!(parse_ok("K= # comment"), vec![("K".into(), "".into())]);
805 }
806
807 #[test]
808 fn empty_double_quoted_value_with_comment() {
809 assert_eq!(parse_ok("K=\"\" # comment"), vec![("K".into(), "".into())]);
810 }
811
812 #[test]
815 fn leading_whitespace_on_line() {
816 assert_eq!(parse_ok(" K=v"), vec![("K".into(), "v".into())]);
817 }
818
819 #[test]
820 fn trailing_whitespace_before_equals() {
821 assert_eq!(parse_ok("K =v"), vec![("K".into(), "v".into())]);
822 }
823
824 #[test]
825 fn whitespace_around_equals() {
826 assert_eq!(parse_ok("K = v"), vec![("K".into(), "v".into())]);
827 }
828
829 #[test]
830 fn tabs_as_whitespace() {
831 assert_eq!(parse_ok("\tK\t=\tv"), vec![("K".into(), "v".into())]);
832 }
833
834 #[test]
835 fn tab_after_equals() {
836 assert_eq!(parse_ok("K=\tval"), vec![("K".into(), "val".into())]);
837 }
838
839 #[test]
840 fn double_equals_value() {
841 assert_eq!(parse_ok("K==v"), vec![("K".into(), "=v".into())]);
842 }
843
844 #[test]
847 fn full_line_comment() {
848 assert!(parse_ok("# this is a comment").is_empty());
849 }
850
851 #[test]
852 fn comment_with_leading_spaces() {
853 assert!(parse_ok(" # indented comment").is_empty());
854 }
855
856 #[test]
857 fn empty_lines_skipped() {
858 assert!(parse_ok("\n\n\n").is_empty());
859 }
860
861 #[test]
862 fn mixed_comments_and_values() {
863 let pairs = parse_ok("# header\nA=1\n\nB=2 # inline\n");
864 assert_eq!(pairs, vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
865 }
866
867 #[test]
870 fn unix_line_endings() {
871 assert_eq!(parse_ok("A=1\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
872 }
873
874 #[test]
875 fn windows_line_endings() {
876 assert_eq!(parse_ok("A=1\r\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
877 }
878
879 #[test]
880 fn no_trailing_newline() {
881 assert_eq!(parse_ok("A=1"), vec![("A".into(), "1".into())]);
882 }
883
884 #[test]
885 fn single_line_no_newline() {
886 assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
887 }
888
889 #[test]
892 fn empty_file() {
893 assert!(parse_ok("").is_empty());
894 }
895
896 #[test]
897 fn only_comments() {
898 assert!(parse_ok("# a\n# b\n# c").is_empty());
899 }
900
901 #[test]
902 fn only_blank_lines() {
903 assert!(parse_ok("\n\n \n\t\n").is_empty());
904 }
905
906 #[test]
909 fn error_missing_equals() {
910 assert_eq!(parse_kind("INVALID"), ParseErrorKind::MissingEquals);
911 }
912
913 #[test]
914 fn error_missing_equals_with_comment() {
915 assert_eq!(parse_kind("K # comment"), ParseErrorKind::MissingEquals);
916 }
917
918 #[test]
919 fn error_empty_key() {
920 assert_eq!(parse_kind("=value"), ParseErrorKind::EmptyKey);
921 }
922
923 #[test]
924 fn error_empty_key_with_spaces() {
925 assert_eq!(parse_kind(" =value"), ParseErrorKind::EmptyKey);
926 }
927
928 #[test]
929 fn error_unmatched_double_quote() {
930 assert_eq!(parse_kind("K=\"hello"), ParseErrorKind::UnmatchedQuote);
931 }
932
933 #[test]
934 fn error_unmatched_single_quote() {
935 assert_eq!(parse_kind("K='hello"), ParseErrorKind::UnmatchedQuote);
936 }
937
938 #[test]
939 fn error_unmatched_double_quote_with_hash() {
940 assert_eq!(parse_kind("K=\"hello#more"), ParseErrorKind::UnmatchedQuote);
941 }
942
943 #[test]
944 fn error_trailing_content_double_quote() {
945 assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
946 }
947
948 #[test]
949 fn error_trailing_content_single_quote() {
950 assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
951 }
952
953 #[test]
954 fn error_trailing_content_line_number() {
955 assert_eq!(parse_line("A=1\nK=\"v\"x\nB=2"), 2);
956 }
957
958 #[test]
959 fn error_invalid_key_exclamation() {
960 assert_eq!(parse_kind("K!EY=v"), ParseErrorKind::InvalidKey);
961 }
962
963 #[test]
964 fn error_invalid_key_dollar() {
965 assert_eq!(parse_kind("\u{0024}KEY=v"), ParseErrorKind::InvalidKey);
966 }
967
968 #[test]
969 fn error_invalid_key_at() {
970 assert_eq!(parse_kind("KEY@=v"), ParseErrorKind::InvalidKey);
971 }
972
973 #[test]
974 fn error_invalid_key_space() {
975 assert_eq!(parse_kind("K EY=v"), ParseErrorKind::InvalidKey);
976 }
977
978 #[test]
979 fn error_invalid_key_slash() {
980 assert_eq!(parse_kind("KEY/VAL=v"), ParseErrorKind::InvalidKey);
981 }
982
983 #[test]
984 fn error_invalid_key_unicode() {
985 assert_eq!(parse_kind("K\u{00C9}Y=v"), ParseErrorKind::InvalidKey);
986 }
987
988 #[test]
989 fn error_line_number_missing_equals() {
990 assert_eq!(parse_line("A=1\nINVALID\nB=2"), 2);
991 }
992
993 #[test]
994 fn error_line_number_invalid_key() {
995 assert_eq!(parse_line("A=1\n\"$\"BAD=v\nB=2"), 2);
996 }
997
998 #[test]
999 fn error_line_number_unmatched_quote() {
1000 assert_eq!(parse_line("A=1\nK=\"unclosed\nB=2"), 2);
1001 }
1002
1003 #[test]
1006 fn unicode_value_unquoted() {
1007 assert_eq!(parse_ok("K=h\u{00E9}llo"), vec![("K".into(), "h\u{00E9}llo".into())]);
1008 }
1009
1010 #[test]
1011 fn unicode_value_double_quoted() {
1012 assert_eq!(parse_ok("K=\"h\u{00E9}llo\""), vec![("K".into(), "h\u{00E9}llo".into())]);
1013 }
1014
1015 #[test]
1016 fn unicode_value_single_quoted() {
1017 assert_eq!(parse_ok("K='h\u{00E9}llo'"), vec![("K".into(), "h\u{00E9}llo".into())]);
1018 }
1019
1020 #[cfg(not(target_arch = "wasm32"))]
1024 #[test]
1025 fn load_sets_vars() {
1026 let dir = env::temp_dir().join(format!("dotenv_test_{}", std::process::id()));
1027 let _ = fs::create_dir_all(&dir);
1028 let env_path = dir.join(".env");
1029 fs::write(&env_path, "DOTENV_TEST_FOO=bar\nDOTENV_TEST_BAZ=qux").unwrap();
1030
1031 let old = env::current_dir().ok();
1032 env::set_current_dir(&dir).unwrap();
1033
1034 let result = load();
1035
1036 if let Some(p) = old {
1037 let _ = env::set_current_dir(p);
1038 }
1039 let _ = fs::remove_file(&env_path);
1040 let _ = fs::remove_dir(&dir);
1041
1042 assert!(result.is_ok());
1043 assert_eq!(env::var("DOTENV_TEST_FOO").unwrap(), "bar");
1044 assert_eq!(env::var("DOTENV_TEST_BAZ").unwrap(), "qux");
1045
1046 unsafe { remove_env("DOTENV_TEST_FOO") };
1047 unsafe { remove_env("DOTENV_TEST_BAZ") };
1048 }
1049
1050 #[cfg(not(target_arch = "wasm32"))]
1052 #[test]
1053 fn load_preserves_existing_env_vars() {
1054 unsafe { set_env("DOTENV_EXISTING", "original") };
1055
1056 let dir = env::temp_dir().join(format!("dotenv_test_existing_{}", std::process::id()));
1057 let _ = fs::create_dir_all(&dir);
1058 let env_path = dir.join(".env");
1059 fs::write(&env_path, "DOTENV_EXISTING=from_file").unwrap();
1060
1061 let old = env::current_dir().ok();
1062 env::set_current_dir(&dir).unwrap();
1063
1064 let result = load();
1065
1066 if let Some(p) = old {
1067 let _ = env::set_current_dir(p);
1068 }
1069 let _ = fs::remove_file(&env_path);
1070 let _ = fs::remove_dir(&dir);
1071
1072 assert!(result.is_ok());
1073 assert_eq!(env::var("DOTENV_EXISTING").unwrap(), "original");
1074
1075 unsafe { remove_env("DOTENV_EXISTING") };
1076 }
1077
1078 #[cfg(not(target_arch = "wasm32"))]
1080 #[test]
1081 fn load_first_declaration_wins() {
1082 let dir = env::temp_dir().join(format!("dotenv_test_first_{}", std::process::id()));
1083 let _ = fs::create_dir_all(&dir);
1084 let env_path = dir.join(".env");
1085 fs::write(&env_path, "DOTENV_DUP=first\nDOTENV_DUP=second").unwrap();
1086
1087 let old = env::current_dir().ok();
1088 env::set_current_dir(&dir).unwrap();
1089
1090 let result = load();
1091
1092 if let Some(p) = old {
1093 let _ = env::set_current_dir(p);
1094 }
1095 let _ = fs::remove_file(&env_path);
1096 let _ = fs::remove_dir(&dir);
1097
1098 assert!(result.is_ok());
1099 assert_eq!(env::var("DOTENV_DUP").unwrap(), "first");
1100
1101 unsafe { remove_env("DOTENV_DUP") };
1102 }
1103
1104 #[cfg(not(target_arch = "wasm32"))]
1106 #[test]
1107 fn load_file_not_found() {
1108 let dir = env::temp_dir().join(format!("dotenv_test_missing_{}", std::process::id()));
1109 let _ = fs::create_dir_all(&dir);
1110
1111 let old = env::current_dir().ok();
1112 env::set_current_dir(&dir).unwrap();
1113
1114 let result = load();
1115
1116 if let Some(p) = old {
1117 let _ = env::set_current_dir(p);
1118 }
1119 let _ = fs::remove_dir(&dir);
1120
1121 match result.unwrap_err() {
1122 Error::Io(_) => {}
1123 _ => panic!("expected Io error"),
1124 }
1125 }
1126
1127 #[cfg(not(target_arch = "wasm32"))]
1129 #[test]
1130 fn load_parse_error() {
1131 let dir = env::temp_dir().join(format!("dotenv_test_parse_err_{}", std::process::id()));
1132 let _ = fs::create_dir_all(&dir);
1133 let env_path = dir.join(".env");
1134 fs::write(&env_path, "A=1\nMALFORMED\nB=2").unwrap();
1135
1136 let old = env::current_dir().ok();
1137 env::set_current_dir(&dir).unwrap();
1138
1139 let result = load();
1140
1141 if let Some(p) = old {
1142 let _ = env::set_current_dir(p);
1143 }
1144 let _ = fs::remove_file(&env_path);
1145 let _ = fs::remove_dir(&dir);
1146
1147 match result.unwrap_err() {
1148 Error::Parse(e) => assert_eq!(e.line, 2),
1149 _ => panic!("expected Parse error"),
1150 }
1151 }
1152}