use std::{
collections::{BTreeMap, BTreeSet},
fmt, fs, io,
ops::Range,
path::{Path, PathBuf},
string::String,
vec::Vec,
};
use crate::{parse_with, Diagnostic, Dialect, Severity};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LoadOrigin {
pub path: PathBuf,
pub span: Range<usize>,
pub line: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedEntry {
pub key: String,
pub value: String,
pub origin: LoadOrigin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadOverride {
pub key: String,
pub previous: LoadOrigin,
pub replacement: LoadOrigin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadRepeated {
pub key: String,
pub origins: Vec<LoadOrigin>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SkipReason {
Bare,
NonParticipating,
EmptyKey,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadSkipped {
pub key: Option<String>,
pub origin: LoadOrigin,
pub reason: SkipReason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedEnvironment {
values: BTreeMap<String, String>,
origins: BTreeMap<String, LoadOrigin>,
entries: Vec<LoadedEntry>,
overrides: Vec<LoadOverride>,
repeated: Vec<LoadRepeated>,
skipped: Vec<LoadSkipped>,
}
impl LoadedEnvironment {
#[must_use]
pub fn values(&self) -> &BTreeMap<String, String> {
&self.values
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
self.values.get(key).map(String::as_str)
}
#[must_use]
pub fn origin(&self, key: &str) -> Option<&LoadOrigin> {
self.origins.get(key)
}
#[must_use]
pub fn entries(&self) -> &[LoadedEntry] {
&self.entries
}
#[must_use]
pub fn overrides(&self) -> &[LoadOverride] {
&self.overrides
}
#[must_use]
pub fn repeated(&self) -> &[LoadRepeated] {
&self.repeated
}
#[must_use]
pub fn skipped(&self) -> &[LoadSkipped] {
&self.skipped
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum LoadError {
Io {
path: PathBuf,
source: io::Error,
},
InvalidUtf8 {
path: PathBuf,
},
Invalid {
path: PathBuf,
diagnostic: Diagnostic,
},
InvalidMode,
}
impl fmt::Display for LoadError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io { path, source } => {
write!(formatter, "failed to read {}: {source}", path.display())
}
Self::InvalidUtf8 { path } => {
write!(formatter, "{} is not valid UTF-8", path.display())
}
Self::Invalid { path, diagnostic } => {
write!(formatter, "{}: {diagnostic}", path.display())
}
Self::InvalidMode => formatter.write_str("mode must not contain path separators"),
}
}
}
impl std::error::Error for LoadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
_ => None,
}
}
}
pub fn load_cascade<P: AsRef<Path>>(
directory: P,
mode: &str,
) -> Result<LoadedEnvironment, LoadError> {
load_cascade_with(directory, mode, Dialect::PORTABLE)
}
pub fn load_cascade_with<P: AsRef<Path>>(
directory: P,
mode: &str,
dialect: Dialect,
) -> Result<LoadedEnvironment, LoadError> {
if !valid_mode(mode) {
return Err(LoadError::InvalidMode);
}
let directory = directory.as_ref();
let mut paths = vec![directory.join(".env"), directory.join(".env.local")];
if !mode.is_empty() {
paths.push(directory.join(format!(".env.{mode}")));
paths.push(directory.join(format!(".env.{mode}.local")));
}
load_paths(paths, dialect, true)
}
pub fn load_files<I, P>(paths: I, dialect: Dialect) -> Result<LoadedEnvironment, LoadError>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
load_paths(paths, dialect, false)
}
fn valid_mode(mode: &str) -> bool {
!mode.contains(['/', '\\', '\0']) && mode != "." && mode != ".."
}
fn load_paths<I, P>(
paths: I,
dialect: Dialect,
skip_missing: bool,
) -> Result<LoadedEnvironment, LoadError>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let mut loaded = LoadedEnvironment {
values: BTreeMap::new(),
origins: BTreeMap::new(),
entries: Vec::new(),
overrides: Vec::new(),
repeated: Vec::new(),
skipped: Vec::new(),
};
for path in paths {
let path = path.as_ref();
let bytes = match fs::read(path) {
Ok(bytes) => bytes,
Err(error) if skip_missing && error.kind() == io::ErrorKind::NotFound => continue,
Err(source) => {
return Err(LoadError::Io {
path: path.to_path_buf(),
source,
});
}
};
let text = String::from_utf8(bytes).map_err(|_| LoadError::InvalidUtf8 {
path: path.to_path_buf(),
})?;
let (document, diagnostics) = parse_with(&text, dialect);
if let Some(diagnostic) = diagnostics
.into_iter()
.find(|diagnostic| diagnostic.severity() == Severity::Error)
{
return Err(LoadError::Invalid {
path: path.to_path_buf(),
diagnostic,
});
}
merge_document(&mut loaded, path, &document);
}
Ok(loaded)
}
fn merge_document(loaded: &mut LoadedEnvironment, path: &Path, document: &crate::Document<'_>) {
let mut seen = BTreeSet::new();
for entry in document.entries() {
let key = entry.key().to_string();
if key.is_empty() {
loaded.skipped.push(LoadSkipped {
key: None,
origin: origin(path, entry),
reason: SkipReason::EmptyKey,
});
continue;
}
if !seen.insert(key.clone()) {
continue;
}
let occurrences: Vec<_> = document.get_all(&key).collect();
if occurrences.len() > 1 {
loaded.repeated.push(LoadRepeated {
key: key.clone(),
origins: occurrences
.iter()
.map(|entry| origin(path, *entry))
.collect(),
});
}
let Some(effective) = document.get_entry(&key) else {
loaded.skipped.push(LoadSkipped {
key: Some(key),
origin: origin(path, entry),
reason: SkipReason::NonParticipating,
});
continue;
};
if !effective.participates_in_lookup() {
loaded.skipped.push(LoadSkipped {
key: Some(key),
origin: origin(path, effective),
reason: SkipReason::NonParticipating,
});
continue;
}
let value = if let Some(value) = effective.value() {
value.to_string()
} else if document.dialect().bare_keys == crate::BareKeys::Empty {
String::new()
} else {
loaded.skipped.push(LoadSkipped {
key: Some(key),
origin: origin(path, effective),
reason: SkipReason::Bare,
});
continue;
};
let entry_origin = origin(path, effective);
loaded.entries.push(LoadedEntry {
key: key.clone(),
value: value.clone(),
origin: entry_origin.clone(),
});
if let Some(previous) = loaded.origins.insert(key.clone(), entry_origin.clone()) {
loaded.overrides.push(LoadOverride {
key: key.clone(),
previous,
replacement: entry_origin,
});
}
loaded.values.insert(key, value);
}
}
fn origin(path: &Path, entry: crate::Entry<'_>) -> LoadOrigin {
LoadOrigin {
path: path.to_path_buf(),
span: entry.value_span().unwrap_or_else(|| entry.key_span()),
line: entry.line(),
}
}