envx_secure/parser.rs
1//! Shared `.env` file parser.
2//!
3//! Parses a `.env` file into an [`IndexMap`] that preserves insertion order.
4//! Both single-quoted and double-quoted values are unquoted automatically.
5//! Blank lines and lines beginning with `#` are skipped.
6//!
7//! [`IndexMap`]: indexmap::IndexMap
8
9use anyhow::{bail, Context, Result};
10use indexmap::IndexMap;
11use std::path::Path;
12
13/// Parse a `.env` file at `path` into an ordered key→value map.
14///
15/// # Errors
16///
17/// Returns an error if:
18/// - the file cannot be read,
19/// - a non-blank, non-comment line has no `=` separator, or
20/// - a key is empty.
21///
22/// # Example
23///
24/// Given the file:
25/// ```text
26/// # comment
27/// DATABASE_URL=postgres://localhost/mydb
28/// SECRET_KEY="s3cr3t"
29/// ```
30///
31/// ```no_run
32/// use std::path::Path;
33/// use envx_secure::parser;
34///
35/// let map = parser::parse(Path::new(".env")).unwrap();
36/// assert_eq!(map["DATABASE_URL"], "postgres://localhost/mydb");
37/// assert_eq!(map["SECRET_KEY"], "s3cr3t");
38/// ```
39pub fn parse(path: &Path) -> Result<IndexMap<String, String>> {
40 let content = std::fs::read_to_string(path)
41 .with_context(|| format!("failed to read {}", path.display()))?;
42
43 let mut map = IndexMap::new();
44
45 for (lineno, line) in content.lines().enumerate() {
46 let line = line.trim();
47
48 if line.is_empty() || line.starts_with('#') {
49 continue;
50 }
51
52 let eq = line.find('=').with_context(|| {
53 format!(
54 "{}:{}: malformed line (expected KEY=VALUE): {:?}",
55 path.display(),
56 lineno + 1,
57 line
58 )
59 })?;
60
61 let key = line[..eq].trim().to_string();
62 let raw_val = line[eq + 1..].trim();
63
64 if key.is_empty() {
65 bail!(
66 "{}:{}: empty key in line: {:?}",
67 path.display(),
68 lineno + 1,
69 line
70 );
71 }
72
73 let value = strip_quotes(raw_val);
74 map.insert(key, value.to_string());
75 }
76
77 Ok(map)
78}
79
80/// Strip a single layer of matching `"…"` or `'…'` quotes from `s`.
81fn strip_quotes(s: &str) -> &str {
82 if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
83 &s[1..s.len() - 1]
84 } else {
85 s
86 }
87}