From 10a991004e2aa2cda919c160a71985ead05a8782 Mon Sep 17 00:00:00 2001 From: Raphael Taylor-Davies Date: Sat, 5 Sep 2020 17:58:30 +0100 Subject: [PATCH 1/5] Add multi-line support --- dotenv/src/iter.rs | 66 +++++++++++++++++++++++++++++++--- dotenv/tests/test-multiline.rs | 20 +++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 dotenv/tests/test-multiline.rs diff --git a/dotenv/src/iter.rs b/dotenv/src/iter.rs index 40506e2d..8cfd5739 100644 --- a/dotenv/src/iter.rs +++ b/dotenv/src/iter.rs @@ -1,20 +1,22 @@ use std::collections::HashMap; use std::env; use std::io::prelude::*; -use std::io::{BufReader, Lines}; +use std::io::BufReader; use crate::errors::*; use crate::parse; pub struct Iter { - lines: Lines>, + lines: QuotedLines>, substitution_data: HashMap>, } impl Iter { pub fn new(reader: R) -> Iter { Iter { - lines: BufReader::new(reader).lines(), + lines: QuotedLines { + buf: BufReader::new(reader), + }, substitution_data: HashMap::new(), } } @@ -31,6 +33,62 @@ impl Iter { } } +struct QuotedLines { + buf: B, +} + +fn is_complete(buf: &String) -> bool { + let mut escape = false; + let mut strong_quote = false; + let mut weak_quote = false; + let mut count = 0_u32; + + for c in buf.chars() { + if escape { + escape = false + } else { + match c { + '\\' => escape = true, + '"' if !strong_quote => { + count += 1; + weak_quote = true + } + '\'' if !weak_quote => { + count += 1; + strong_quote = true + } + _ => (), + } + } + } + count % 2 == 0 +} + +impl Iterator for QuotedLines { + type Item = Result; + + fn next(&mut self) -> Option> { + let mut buf = String::new(); + loop { + match self.buf.read_line(&mut buf) { + Ok(0) => return None, + Ok(_n) => { + if is_complete(&buf) { + if buf.ends_with('\n') { + buf.pop(); + if buf.ends_with('\r') { + buf.pop(); + } + } + return Some(Ok(buf)); + } + } + Err(e) => return Some(Err(Error::Io(e))), + } + } + } +} + impl Iterator for Iter { type Item = Result<(String, String)>; @@ -38,7 +96,7 @@ impl Iterator for Iter { loop { let line = match self.lines.next() { Some(Ok(line)) => line, - Some(Err(err)) => return Some(Err(Error::Io(err))), + Some(Err(err)) => return Some(Err(err)), None => return None, }; diff --git a/dotenv/tests/test-multiline.rs b/dotenv/tests/test-multiline.rs new file mode 100644 index 00000000..2a6eec83 --- /dev/null +++ b/dotenv/tests/test-multiline.rs @@ -0,0 +1,20 @@ +mod common; + +use dotenv::*; +use std::env; + +use crate::common::*; + +#[test] +fn test_multiline() { + let value = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\\n\\\"QUOTED\\\""; + let weak = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\n\"QUOTED\""; + let dir = tempdir_with_dotenv(&format!("WEAK=\"{}\"\nSTRONG='{}'", value, value)).unwrap(); + + dotenv().ok(); + assert_eq!(var("WEAK").unwrap(), weak); + assert_eq!(var("STRONG").unwrap(), value); + + env::set_current_dir(dir.path().parent().unwrap()).unwrap(); + dir.close().unwrap(); +} From 8cec2e9ad88badc3dba9211ffd7b607b6bb0e54a Mon Sep 17 00:00:00 2001 From: Robin Vobruba Date: Sun, 27 Feb 2022 18:36:32 +0100 Subject: [PATCH 2/5] Uses a state-machine for multi-line values --- dotenv/src/iter.rs | 69 ++++++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/dotenv/src/iter.rs b/dotenv/src/iter.rs index 8cfd5739..fc71fe8a 100644 --- a/dotenv/src/iter.rs +++ b/dotenv/src/iter.rs @@ -37,31 +37,42 @@ struct QuotedLines { buf: B, } -fn is_complete(buf: &String) -> bool { - let mut escape = false; - let mut strong_quote = false; - let mut weak_quote = false; - let mut count = 0_u32; +enum QuoteState { + Complete, + Escape, + StrongOpen, + StrongOpenEscape, + WeakOpen, + WeakOpenEscape, +} + +fn eval_end_state(prev_state: QuoteState, buf: &str) -> QuoteState { + let mut cur_state = prev_state; for c in buf.chars() { - if escape { - escape = false - } else { - match c { - '\\' => escape = true, - '"' if !strong_quote => { - count += 1; - weak_quote = true - } - '\'' if !weak_quote => { - count += 1; - strong_quote = true - } - _ => (), - } - } + cur_state = match cur_state { + QuoteState::Escape => QuoteState::Complete, + QuoteState::Complete => match c { + '\\' => QuoteState::Escape, + '"' => QuoteState::WeakOpen, + '\'' => QuoteState::StrongOpen, + _ => QuoteState::Complete, + }, + QuoteState::WeakOpen => match c { + '\\' => QuoteState::WeakOpenEscape, + '"' => QuoteState::Complete, + _ => QuoteState::WeakOpen, + }, + QuoteState::WeakOpenEscape => QuoteState::WeakOpen, + QuoteState::StrongOpen => match c { + '\\' => QuoteState::StrongOpenEscape, + '\'' => QuoteState::Complete, + _ => QuoteState::StrongOpen, + }, + QuoteState::StrongOpenEscape => QuoteState::StrongOpen, + }; } - count % 2 == 0 + cur_state } impl Iterator for QuotedLines { @@ -69,11 +80,21 @@ impl Iterator for QuotedLines { fn next(&mut self) -> Option> { let mut buf = String::new(); + let mut cur_state = QuoteState::Complete; + let mut buf_pos; loop { + buf_pos = buf.len(); match self.buf.read_line(&mut buf) { - Ok(0) => return None, + Ok(0) => match cur_state { + QuoteState::Complete => return None, + _ => { + let len = buf.len(); + return Some(Err(Error::LineParse(buf, len))); + } + }, Ok(_n) => { - if is_complete(&buf) { + cur_state = eval_end_state(cur_state, &buf[buf_pos..]); + if let QuoteState::Complete = cur_state { if buf.ends_with('\n') { buf.pop(); if buf.ends_with('\r') { From 84f661b8a0aec2813b20f71fc89593ce1473e90e Mon Sep 17 00:00:00 2001 From: Robin Vobruba Date: Sun, 27 Feb 2022 18:27:28 +0100 Subject: [PATCH 3/5] Extends multi-line integration-test --- dotenv/tests/test-multiline.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/dotenv/tests/test-multiline.rs b/dotenv/tests/test-multiline.rs index 2a6eec83..262285d6 100644 --- a/dotenv/tests/test-multiline.rs +++ b/dotenv/tests/test-multiline.rs @@ -9,9 +9,36 @@ use crate::common::*; fn test_multiline() { let value = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\\n\\\"QUOTED\\\""; let weak = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\n\"QUOTED\""; - let dir = tempdir_with_dotenv(&format!("WEAK=\"{}\"\nSTRONG='{}'", value, value)).unwrap(); + let dir = tempdir_with_dotenv(&format!( + r#" +KEY=my\ cool\ value +KEY3="awesome stuff \"mang\" +more +on other +lines" +KEY4='hello '\''fds'" +good ' \'morning" +WEAK="{}" +STRONG='{}' +"#, + value, value + )) + .unwrap(); dotenv().ok(); + assert_eq!(var("KEY").unwrap(), r#"my cool value"#); + assert_eq!( + var("KEY3").unwrap(), + r#"awesome stuff "mang" +more +on other +lines"# + ); + assert_eq!( + var("KEY4").unwrap(), + r#"hello 'fds +good ' 'morning"# + ); assert_eq!(var("WEAK").unwrap(), weak); assert_eq!(var("STRONG").unwrap(), value); From 6063514f55d91e5fb1c18087da843fa070950c3c Mon Sep 17 00:00:00 2001 From: Robin Vobruba Date: Mon, 28 Feb 2022 19:01:16 +0100 Subject: [PATCH 4/5] Documents iter::Iter::load [minor] --- dotenv/src/iter.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotenv/src/iter.rs b/dotenv/src/iter.rs index fc71fe8a..bf68cf91 100644 --- a/dotenv/src/iter.rs +++ b/dotenv/src/iter.rs @@ -21,6 +21,7 @@ impl Iter { } } + /// Loads all variables found in the `reader` into the environment. pub fn load(self) -> Result<()> { for item in self { let (key, value) = item?; From ccb2c536e364c751c3dfda806b4278a142cf22f7 Mon Sep 17 00:00:00 2001 From: Allan Zhang Date: Mon, 22 Aug 2022 12:48:34 -0400 Subject: [PATCH 5/5] Multiline support --- dotenv/tests/test-multiline.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/dotenv/tests/test-multiline.rs b/dotenv/tests/test-multiline.rs index 262285d6..35020c20 100644 --- a/dotenv/tests/test-multiline.rs +++ b/dotenv/tests/test-multiline.rs @@ -1,9 +1,8 @@ mod common; -use dotenv::*; -use std::env; - use crate::common::*; +use dotenvy::*; +use std::env; #[test] fn test_multiline() { @@ -12,11 +11,11 @@ fn test_multiline() { let dir = tempdir_with_dotenv(&format!( r#" KEY=my\ cool\ value -KEY3="awesome stuff \"mang\" +KEY3="awesome \"stuff\" more on other lines" -KEY4='hello '\''fds'" +KEY4='hello '\''world'" good ' \'morning" WEAK="{}" STRONG='{}' @@ -29,14 +28,14 @@ STRONG='{}' assert_eq!(var("KEY").unwrap(), r#"my cool value"#); assert_eq!( var("KEY3").unwrap(), - r#"awesome stuff "mang" + r#"awesome "stuff" more on other lines"# ); assert_eq!( var("KEY4").unwrap(), - r#"hello 'fds + r#"hello 'world good ' 'morning"# ); assert_eq!(var("WEAK").unwrap(), weak);