use std::collections::HashMap;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io::BufRead;
#[derive(Debug, PartialEq)]
enum ParserState {
Start,
ParseKey,
AwaitAssignment,
AwaitValue,
ParseValue,
}
#[derive(Debug, PartialEq)]
enum ParseLineError {
BeginsWithAssignment,
IllegalChar(char),
}
impl Display for ParseLineError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ParseLineError::BeginsWithAssignment => write!(f, "line begins with assignment"),
ParseLineError::IllegalChar(c) => write!(f, "illegal character in key: `{}`", c),
}
}
}
pub fn parse<R: BufRead>(reader: R) -> Result<HashMap<String, String>, String> {
let mut vars = HashMap::new();
for (line_number, mut line) in reader.lines().map(|l| l.unwrap()).enumerate() {
if line.ends_with('\r') {
line.pop();
}
match parse_line(&line) {
Ok(Some((k, v))) => {
vars.insert(k.to_owned(), v.to_owned());
}
Err(reason) => {
return Err(format!("error on line {}: {}", line_number + 1, reason));
}
_ => (),
};
}
Ok(vars)
}
fn parse_line(line: &str) -> Result<Option<(&str, &str)>, ParseLineError> {
let mut k: usize = 0;
let mut v: usize = 0;
let mut state = ParserState::Start;
let line = line.trim();
for (i, c) in line.chars().enumerate() {
match state {
ParserState::Start => match c {
'A'..='Z' | '0'..='9' | '_' => state = ParserState::ParseKey,
'#' => break,
'=' => return Err(ParseLineError::BeginsWithAssignment),
_ => return Err(ParseLineError::IllegalChar(c)),
},
ParserState::ParseKey => match c {
' ' | '\t' => {
k = i;
state = ParserState::AwaitAssignment
}
'=' => {
k = i;
state = ParserState::AwaitValue;
}
'A'..='Z' | '0'..='9' | '_' => (),
_ => return Err(ParseLineError::IllegalChar(c)),
},
ParserState::AwaitAssignment => match c {
'=' => {
state = ParserState::AwaitValue;
}
' ' | '\t' => (),
_ => return Err(ParseLineError::IllegalChar(c)),
},
ParserState::AwaitValue => match c {
' ' | '\t' => (),
_ => {
v = i;
state = ParserState::ParseValue;
break;
}
},
_ => (),
}
}
match state {
ParserState::Start => Ok(None),
ParserState::ParseKey => Ok(Some((&line, ""))),
ParserState::AwaitAssignment | ParserState::AwaitValue => {
Ok(Some((&line[0..k], "")))
}
ParserState::ParseValue => Ok(Some((&line[0..k], &line[v..]))),
}
}
#[cfg(test)]
mod test_parse_line {
use super::{parse_line, ParseLineError};
#[test]
fn empty_line() {
assert_eq!(parse_line(""), Ok(None));
}
#[test]
fn empty_line_whitespace() {
assert_eq!(parse_line(" "), Ok(None));
}
#[test]
fn key_only() {
assert_eq!(parse_line("KEY"), Ok(Some(("KEY", ""))));
}
#[test]
fn key_only_illegal_chars() {
assert_eq!(
parse_line("key").unwrap_err(),
ParseLineError::IllegalChar('k')
);
}
#[test]
fn key_illegal_pair() {
assert_eq!(
parse_line("key = illegal").unwrap_err(),
ParseLineError::IllegalChar('k')
);
}
#[test]
fn key_only_numeric() {
assert_eq!(parse_line("1234567890"), Ok(Some(("1234567890", ""))));
}
#[test]
fn key_only_underscore() {
assert_eq!(parse_line("_"), Ok(Some(("_", ""))));
}
#[test]
fn key_only_all_legal_chars() {
assert_eq!(
parse_line("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"),
Ok(Some(("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_", "")))
);
}
#[test]
fn key_only_leading_whitespace() {
assert_eq!(parse_line(" KEY"), Ok(Some(("KEY", ""))));
}
#[test]
fn key_only_trailing_whitespace() {
assert_eq!(parse_line("KEY "), Ok(Some(("KEY", ""))));
}
#[test]
fn key_only_whitespace() {
assert_eq!(parse_line(" KEY "), Ok(Some(("KEY", ""))));
}
#[test]
fn val_only() {
assert!(parse_line("=val").is_err());
}
#[test]
fn val_only_leading_whitespace() {
assert!(parse_line(" =val").is_err());
}
#[test]
fn val_only_trailing_whitespace() {
assert!(parse_line("=val ").is_err());
}
#[test]
fn val_only_surrounding_whitespace() {
assert!(parse_line(" =val ").is_err());
}
#[test]
fn val_only_whitespace() {
assert!(parse_line(" = val ").is_err());
}
#[test]
fn key_val() {
assert_eq!(parse_line("KEY=VAL"), Ok(Some(("KEY", "VAL"))));
}
#[test]
fn key_val_illegal_chars_in_val() {
assert_eq!(parse_line("KEY=val"), Ok(Some(("KEY", "val"))));
}
#[test]
fn key_val_illegal_whitespace() {
assert_eq!(
parse_line("K Y=VAL").unwrap_err(),
ParseLineError::IllegalChar('Y')
);
}
#[test]
fn key_val_leading_whitespace() {
assert_eq!(parse_line(" KEY=VAL"), Ok(Some(("KEY", "VAL"))));
}
#[test]
fn key_val_trailing_whitespace() {
assert_eq!(parse_line("KEY=VAL "), Ok(Some(("KEY", "VAL"))));
}
#[test]
fn key_val_surrounding_whitespace() {
assert_eq!(parse_line(" KEY=VAL "), Ok(Some(("KEY", "VAL"))));
}
#[test]
fn key_val_between_whitespace() {
assert_eq!(parse_line("KEY = VAL"), Ok(Some(("KEY", "VAL"))));
}
#[test]
fn key_val_leading_between_whitespace() {
assert_eq!(parse_line("KEY =VAL"), Ok(Some(("KEY", "VAL"))));
}
#[test]
fn key_val_trailing_between_whitespace() {
assert_eq!(parse_line("KEY= VAL"), Ok(Some(("KEY", "VAL"))));
}
#[test]
fn key_val_whitespace() {
assert_eq!(
Ok(Some(("KEY", "VAL"))),
parse_line(" KEY = VAL ")
);
}
#[test]
fn key_val_empty() {
assert_eq!(parse_line("KEY="), Ok(Some(("KEY", ""))));
}
#[test]
fn key_val_empty_leading_between_whitespace() {
assert_eq!(parse_line("KEY ="), Ok(Some(("KEY", ""))));
}
#[test]
fn key_val_empty_trailing_whitespace() {
assert_eq!(parse_line("KEY= "), Ok(Some(("KEY", ""))));
}
#[test]
fn key_val_empty_leading_whitespace() {
assert_eq!(parse_line(" KEY="), Ok(Some(("KEY", ""))));
}
#[test]
fn key_val_empty_whitespace() {
assert_eq!(parse_line(" KEY = "), Ok(Some(("KEY", ""))));
}
#[test]
fn key_val_raw_is_removed() {
assert_eq!(parse_line("KEY=="), Ok(Some(("KEY", "="))));
}
#[test]
fn key_val_raw_is_removed_with_whitespace() {
assert_eq!(parse_line("KEY == "), Ok(Some(("KEY", "="))));
}
#[test]
fn comment() {
assert_eq!(parse_line("# comment"), Ok(None));
}
#[test]
fn comment_with_whitespace() {
assert_eq!(parse_line(" # comment "), Ok(None));
}
#[test]
fn dollar_sign_is_removed() {
assert_eq!(
parse_line("$KEY=val").unwrap_err(),
ParseLineError::IllegalChar('$')
);
}
}
#[cfg(test)]
mod test_parse {
use super::parse;
#[test]
fn empty_line() {
let dotenv = "".as_bytes();
let result = parse(dotenv).unwrap();
assert!(result.is_empty());
}
#[test]
fn three_empty_lines() {
let dotenv = "\n\n".as_bytes();
let result = parse(dotenv).unwrap();
assert!(result.is_empty());
}
#[test]
fn three_whitespace_lines() {
let dotenv = " \n \n ".as_bytes();
let result = parse(dotenv).unwrap();
assert!(result.is_empty());
}
#[test]
fn comment_line() {
let dotenv = "# this is a comment".as_bytes();
let result = parse(dotenv).unwrap();
assert!(result.is_empty());
}
#[test]
fn three_comment_lines() {
let dotenv = " #COMMENT\n# look on my works, ye mighty!\n#####NEW SECTION#####".as_bytes();
let result = parse(dotenv).unwrap();
assert!(result.is_empty());
}
#[test]
fn one_line() {
let dotenv = "FOO=BAR".as_bytes();
let result = parse(dotenv).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result.get("FOO").unwrap(), "BAR");
}
#[test]
fn remove_raw_processing() {
let dotenv = "FOO==BAR".as_bytes();
let result = parse(dotenv).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result.get("FOO").unwrap(), "=BAR");
}
#[test]
fn two_lines() {
let dotenv = "FOO=BAR\nBOP=BAZ".as_bytes();
let result = parse(dotenv).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result.get("FOO").unwrap(), "BAR");
assert_eq!(result.get("BOP").unwrap(), "BAZ");
}
#[test]
fn three_lines() {
let dotenv = "FOO=BAR\nBOP=3344\nDUCK=https://duckduckgo.com/".as_bytes();
let result = parse(dotenv).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result.get("FOO").unwrap(), "BAR");
assert_eq!(result.get("BOP").unwrap(), "3344");
assert_eq!(result.get("DUCK").unwrap(), "https://duckduckgo.com/");
}
#[test]
fn three_lines_with_comment() {
let dotenv = "FOO=BAR\n# search engine\nDUCK=https://duckduckgo.com/".as_bytes();
let result = parse(dotenv).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result.get("FOO").unwrap(), "BAR");
assert_eq!(result.get("DUCK").unwrap(), "https://duckduckgo.com/");
}
#[test]
fn example_1() {
let dotenv = "# My database configuration\n\nDB_PORT=5432\nDB_HOSTNAME=localhost\nDB_USER=postgres\nDB_PASSWORD=postgres\n".as_bytes();
let result = parse(dotenv).unwrap();
assert_eq!(result.len(), 4);
assert_eq!(result.get("DB_PORT").unwrap(), "5432");
assert_eq!(result.get("DB_HOSTNAME").unwrap(), "localhost");
assert_eq!(result.get("DB_USER").unwrap(), "postgres");
assert_eq!(result.get("DB_PASSWORD").unwrap(), "postgres");
}
#[test]
fn example_2() {
let dotenv =
" #------SECTION 1---------#\n\n\nDEBUG\n FILE = /home/user01/test.db \n"
.as_bytes();
let result = parse(dotenv).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result.get("DEBUG").unwrap(), "");
assert_eq!(result.get("FILE").unwrap(), "/home/user01/test.db");
}
#[test]
fn mixed_example() {
let dotenv = "#### Minecraft Server ####\n\n\nPORT=25565\nHOST=0.0.0.0\nMOTD=* * * HELLO FANS OF MINECRAFT * * * \nPASSWORD=abc123 def456\n".as_bytes();
let result = parse(dotenv).unwrap();
assert_eq!(result.len(), 4);
assert_eq!(result.get("PORT").unwrap(), "25565");
assert_eq!(result.get("HOST").unwrap(), "0.0.0.0");
assert_eq!(
result.get("MOTD").unwrap(),
"* * * HELLO FANS OF MINECRAFT * * *"
);
assert_eq!(result.get("PASSWORD").unwrap(), "abc123 def456");
}
#[test]
fn illegal_char_error_on_line_1() {
let dotenv = "database=localhost\nPORT=3344".as_bytes();
let err = parse(dotenv).unwrap_err();
assert_eq!(err, "error on line 1: illegal character in key: `d`");
}
#[test]
fn illegal_char_error_on_line_2() {
let dotenv = "DATABASE=localhost\nport=3344".as_bytes();
let err = parse(dotenv).unwrap_err();
assert_eq!(err, "error on line 2: illegal character in key: `p`");
}
#[test]
fn begins_with_assignment_error_on_line_1() {
let dotenv = "=localhost\nPORT=3344".as_bytes();
let err = parse(dotenv).unwrap_err();
assert_eq!(err, "error on line 1: line begins with assignment");
}
#[test]
fn begins_with_assignment_error_on_line_2() {
let dotenv = "DB=localhost\n=3344".as_bytes();
let err = parse(dotenv).unwrap_err();
assert_eq!(err, "error on line 2: line begins with assignment");
}
}