1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use 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#[derive(Debug)]
243pub enum Error {
244 Io(io::Error),
246 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#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct ParseError {
277 pub line: usize,
279 pub kind: ParseErrorKind,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
285pub enum ParseErrorKind {
286 MissingEquals,
288 UnmatchedQuote,
290 EmptyKey,
292 InvalidKey,
295 TrailingContent,
297 InvalidValue,
300 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
318pub fn load() -> Result<(), Error> {
346 let mut path = env::current_dir()?;
347 path.push(".env");
348 from_path(path)
349}
350
351pub fn from_path(path: impl AsRef<Path>) -> Result<(), Error> {
369 let content = fs::read_to_string(path)?;
370 let vars = parse(&content).map_err(Error::Parse)?;
373
374 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 unsafe { env::set_var(*key, value.as_ref()) };
384 }
385 }
386 Ok(())
387}
388
389pub fn parse(input: &str) -> Result<HashMap<&str, Cow<'_, str>>, ParseError> {
419 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 map.entry(key).or_insert(value);
459 }
460
461 Ok(map)
462}
463
464fn 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
478fn 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
507fn 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
552fn 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
578fn 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 Some(_) | None => {
605 return Err(ParseError {
606 line,
607 kind: ParseErrorKind::InvalidEscape,
608 });
609 }
610 }
611 }
612 Ok(Cow::Owned(out))
613}
614
615pub use dotenv_derive::FromEnv;
620
621#[derive(Debug, Clone)]
637pub enum FromEnvError {
638 Missing(String),
640 Invalid {
642 var: String,
644 value: String,
646 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 pub fn missing(var: impl Into<String>) -> Self {
671 FromEnvError::Missing(var.into())
672 }
673
674 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
684pub trait FromEnv: Sized {
688 fn from_env() -> Result<Self, FromEnvError> {
690 Self::from_env_with_prefix("")
691 }
692
693 fn from_env_with_prefix(prefix: &str) -> Result<Self, FromEnvError>;
699}
700
701pub trait FromEnvValue: Sized {
721 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
734pub trait FromEnvAuto: Sized {
742 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
768pub fn from_env<T: FromEnv>() -> Result<T, FromEnvError> {
779 T::from_env()
780}
781
782#[cfg(test)]
787mod tests {
788 use super::*;
789
790 #[cfg(not(target_arch = "wasm32"))]
793 static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
794
795 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 #[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 #[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 #[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 #[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 #[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 #[test]
1051 fn quoted_nested_example() {
1052 assert_eq!(parse_ok("HELLO='\"hello\"'"), vec![("HELLO".into(), "\"hello\"".into())]);
1053 }
1054
1055 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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}