use crate::{
collection::Vector,
misc::{EnvVars, FromVars, find_file, str_rsplit_once1, str_split_once1},
};
use alloc::string::String;
use core::{fmt::Write as _, str};
use std::{
env, fs,
io::{BufRead as _, BufReader, Read},
path::Path,
};
impl<T> EnvVars<T>
where
T: FromVars,
{
#[inline]
pub fn from_available(others: impl IntoIterator<Item = (String, String)>) -> crate::Result<Self> {
let err0 = match Self::from_process(others) {
Ok(elem) => return Ok(elem),
Err(err) => err,
};
let err1 = match Self::from_nearest_env_file() {
Ok(elem) => return Ok(elem),
Err(err) => err,
};
let mut error = String::new();
error.write_fmt(format_args!("Errors: {err0}, {err1}"))?;
Err(crate::Error::NoAvailableVars(error.into()))
}
#[inline]
pub fn from_env_data(data: &[u8]) -> crate::Result<Self> {
let vector = env(data)?;
Ok(Self(T::from_vars(vector)?))
}
#[inline]
pub fn from_env_path<P>(path: P) -> crate::Result<Self>
where
P: AsRef<Path>,
{
let vector = env(fs::File::open(path)?)?;
Ok(Self(T::from_vars(vector)?))
}
#[inline]
pub fn from_nearest_env_file() -> crate::Result<Self> {
let mut buffer = env::current_dir()?;
find_file(&mut buffer, Path::new(".env"))?;
let vector = env(fs::File::open(buffer)?)?;
Ok(Self(T::from_vars(vector)?))
}
#[inline]
pub fn from_process(others: impl IntoIterator<Item = (String, String)>) -> crate::Result<Self> {
Ok(Self(T::from_vars(env::vars().chain(others))?))
}
#[inline]
pub fn finish(self) -> T {
self.0
}
}
#[expect(clippy::ref_patterns, reason = "false-positive")]
fn env<R>(read: R) -> crate::Result<Vector<(String, String)>>
where
R: Read,
{
let buffer = &mut String::new();
let reader = &mut BufReader::new(read);
let mut vars = Vector::new();
loop {
if reader.read_line(buffer)? == 0 {
break;
}
let buffer_ref = buffer.trim();
if buffer_ref.is_empty() || buffer_ref.starts_with('#') {
buffer.clear();
continue;
}
let Some((key, value)) = str_split_once1(buffer_ref, b'=') else {
buffer.clear();
continue;
};
let key_trimmed = key.trim_end().into();
let value_trimmed = value.trim_start();
if let &[delimiter @ (b'\'' | b'"'), ref value_after_del @ ..] = value_trimmed.as_bytes() {
let diff = value.len().wrapping_sub(value_trimmed.len());
let value_begin = key.len().wrapping_add(1).wrapping_add(diff).wrapping_add(1);
if let &[ref value_surrounded @ .., last] = value_after_del {
if delimiter == last {
let value_final = unsafe { str::from_utf8_unchecked(value_surrounded) };
vars.push((key_trimmed, unescape(value_final)))?;
} else {
process_multiline(buffer, reader, delimiter, key_trimmed, value_begin, &mut vars)?;
}
} else {
process_multiline(buffer, reader, delimiter, key_trimmed, value_begin, &mut vars)?;
}
} else {
vars.push((key_trimmed, unescape(strip_ending_comment(value_trimmed))))?;
}
buffer.clear();
}
Ok(vars)
}
fn process_multiline<R>(
buffer: &mut String,
buf_reader: &mut BufReader<R>,
delimiter: u8,
key_trimmed: String,
value_begin: usize,
vars: &mut Vector<(String, String)>,
) -> crate::Result<()>
where
R: Read,
{
let mut ends_with_delimiter = false;
let actual_buffer_data = loop {
if buf_reader.read_line(buffer)? == 0 {
break buffer.as_str();
}
let trimmed = buffer.trim_end();
if trimmed.ends_with(char::from(delimiter)) {
ends_with_delimiter = true;
break trimmed;
}
};
let mut value_all = actual_buffer_data.get(value_begin..).unwrap_or_default();
if !ends_with_delimiter {
value_all = strip_ending_comment(value_all);
}
let mut value_final = unescape(value_all);
if Some(char::from(delimiter)) != value_final.pop() {
return Err(crate::Error::MissingVarQuote(key_trimmed.into()));
}
vars.push((key_trimmed, value_final))?;
Ok(())
}
fn strip_ending_comment(value: &str) -> &str {
if let Some((lhs, _)) = str_rsplit_once1(value, b'#') { lhs.trim_end() } else { value }
}
fn unescape(str: &str) -> String {
let mut rslt = String::with_capacity(str.len());
let mut chars = str.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('"') => rslt.push('"'),
Some('\'') => rslt.push('\''),
Some('\\') | None => rslt.push('\\'),
Some('n') => rslt.push('\n'),
Some('r') => rslt.push('\r'),
Some('t') => rslt.push('\t'),
Some(other) => {
rslt.push('\\');
rslt.push(other);
}
}
} else {
rslt.push(ch);
}
}
rslt
}
#[cfg(test)]
mod tests {
use crate::misc::env_vars::std::env;
#[test]
fn basic() {
let data = "HOST='localhost'\nPORT=8080\n Comment\nNAME=\"foo\"";
let result = env(data.as_bytes()).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0], ("HOST".into(), "localhost".into()));
assert_eq!(result[1], ("PORT".into(), "8080".into()));
assert_eq!(result[2], ("NAME".into(), "foo".into()));
}
#[test]
fn comments() {
{
let data = "PORT=8080 # The server port";
let result = env(data.as_bytes()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ("PORT".into(), "8080".into()));
}
{
let data = "PORT='8080' # The server port";
let result = env(data.as_bytes()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ("PORT".into(), "8080".into()));
}
{
let data = "PORT='\n80\n80\n' # The server port";
let result = env(data.as_bytes()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ("PORT".into(), "\n80\n80\n".into()));
}
}
#[test]
fn escaped_quotes() {
let data = r#"JSON="{\"a\":1}""#;
let result = env(data.as_bytes()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ("JSON".into(), r#"{"a":1}"#.into()));
}
#[test]
fn multiline_with_trailing_newline() {
let data = "FOO=\"Line 1\nLine 2\"\nNEXT=bar";
let result = env(data.as_bytes()).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], ("FOO".into(), "Line 1\nLine 2".into()));
assert_eq!(result[1], ("NEXT".into(), "bar".into()));
}
#[test]
fn new_lines() {
{
let data = "FOO='bar\nbaz'";
let result = env(data.as_bytes()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ("FOO".into(), "bar\nbaz".into()));
}
{
let data = "FOO='
bar
baz
'";
let result = env(data.as_bytes()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ("FOO".into(), "\n bar\n baz\n ".into()));
}
}
#[test]
fn unclosed_variable() {
let data = "FOO='bar";
assert!(env(data.as_bytes()).is_err());
}
#[test]
fn with_value_spaces() {
let data = "FOO=\" bar\"";
let result = env(data.as_bytes()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], ("FOO".into(), " bar".into()));
}
}