use std::{
collections::{BTreeMap, HashMap},
fmt, fs, io,
ops::Range,
path::{Path, PathBuf},
string::String,
vec::Vec,
};
use crate::{
encode_value, parse_strict, parse_strict_with, parse_with, Diagnostic, Dialect, ExpandOptions,
ExpandScope, ResolveError, Severity,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct LoadOrigin {
pub path: PathBuf,
pub span: Range<usize>,
pub line: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct LoadedEntry {
pub key: String,
pub value: String,
pub origin: LoadOrigin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct LoadOverride {
pub key: String,
pub previous: LoadOrigin,
pub replacement: LoadOrigin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
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)]
#[non_exhaustive]
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>,
raw_entries: BTreeMap<String, (usize, String, String)>,
dialect: Dialect,
entries: Vec<LoadedEntry>,
overrides: Vec<LoadOverride>,
repeated: Vec<LoadRepeated>,
skipped: Vec<LoadSkipped>,
raw_tokens: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct LoadOptions {
pub dialect: Dialect,
pub retain_raw_tokens: bool,
pub retain_reports: bool,
}
impl Default for LoadOptions {
fn default() -> Self {
Self::new(Dialect::PORTABLE)
}
}
impl LoadOptions {
#[must_use]
pub const fn new(dialect: Dialect) -> Self {
Self {
dialect,
retain_raw_tokens: true,
retain_reports: true,
}
}
#[must_use]
pub const fn with_dialect(mut self, dialect: Dialect) -> Self {
self.dialect = dialect;
self
}
#[must_use]
pub const fn with_raw_tokens(mut self, retain: bool) -> Self {
self.retain_raw_tokens = retain;
self
}
#[must_use]
pub const fn with_reports(mut self, retain: bool) -> Self {
self.retain_reports = retain;
self
}
}
impl LoadedEnvironment {
#[must_use]
pub fn values(&self) -> &BTreeMap<String, String> {
&self.values
}
#[must_use]
pub const fn retains_raw_tokens(&self) -> bool {
self.raw_tokens
}
#[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 len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LoadedValues {
values: BTreeMap<String, String>,
}
impl LoadedValues {
#[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 len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
#[must_use]
pub fn into_values(self) -> BTreeMap<String, String> {
self.values
}
}
impl LoadedEnvironment {
#[must_use]
pub fn into_values(self) -> LoadedValues {
LoadedValues {
values: self.values,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedEnvironment {
values: BTreeMap<String, String>,
origins: BTreeMap<String, LoadOrigin>,
}
impl ResolvedEnvironment {
#[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 len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedResolveError {
key: Option<String>,
origin: Option<LoadOrigin>,
message: String,
}
impl LoadedResolveError {
fn new(key: Option<String>, origin: Option<LoadOrigin>, message: impl Into<String>) -> Self {
Self {
key,
origin,
message: message.into(),
}
}
#[must_use]
pub fn key(&self) -> Option<&str> {
self.key.as_deref()
}
#[must_use]
pub fn origin(&self) -> Option<&LoadOrigin> {
self.origin.as_ref()
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for LoadedResolveError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(origin) = &self.origin {
write!(formatter, "{}:{}: ", origin.path.display(), origin.line)?;
}
formatter.write_str(&self.message)
}
}
impl std::error::Error for LoadedResolveError {}
impl LoadedEnvironment {
pub fn resolve_decoded<F>(
&self,
lookup: F,
options: &ExpandOptions,
) -> Result<ResolvedEnvironment, LoadedResolveError>
where
F: Fn(&str) -> Option<String>,
{
if options.scope != ExpandScope::WholeFile {
return Err(LoadedResolveError::new(
None,
None,
"loaded environments require whole-file resolution scope",
));
}
let mut source = String::new();
for (key, value) in &self.values {
let token = encode_value(value).ok_or_else(|| {
LoadedResolveError::new(
Some(key.clone()),
self.origin(key).cloned(),
"value cannot be represented exactly",
)
})?;
source.push_str(key);
source.push('=');
source.push_str(&token);
source.push('\n');
}
let document = parse_strict(&source).map_err(|error| {
LoadedResolveError::new(None, None, format!("cannot prepare resolution: {error}"))
})?;
let mut options = *options;
options.rules.single_quotes = true;
options.rules.backtick_quotes = true;
options.rules.escaped_dollar = true;
let resolved = document.resolve(lookup, &options).map_err(|error| {
let key = resolve_error_key(&error).to_string();
let origin = self.origin(&key).cloned();
LoadedResolveError::new(Some(key), origin, error.to_string())
})?;
let mut values = BTreeMap::new();
for entry in resolved.entries() {
values.insert(entry.key.clone(), entry.value.clone().unwrap_or_default());
}
Ok(ResolvedEnvironment {
values,
origins: self.origins.clone(),
})
}
pub fn resolve_contextual<F>(
&self,
lookup: F,
options: &ExpandOptions,
) -> Result<ResolvedEnvironment, LoadedResolveError>
where
F: Fn(&str) -> Option<String>,
{
if !self.raw_tokens {
return Err(LoadedResolveError::new(
None,
None,
"raw token context was not retained; load with raw tokens or use resolve_decoded",
));
}
let mut entries: Vec<_> = self.raw_entries.iter().collect();
entries.sort_unstable_by_key(|(_, (order, _, _))| *order);
let mut source = String::new();
for (_, (_, raw_key, raw_value)) in entries {
source.push_str(raw_key);
source.push('=');
source.push_str(raw_value);
source.push('\n');
}
let document = parse_strict_with(&source, self.dialect).map_err(|error| {
LoadedResolveError::new(None, None, format!("cannot prepare resolution: {error}"))
})?;
let resolved = document.resolve(lookup, options).map_err(|error| {
let key = resolve_error_key(&error).to_string();
let origin = self.origin(&key).cloned();
LoadedResolveError::new(Some(key), origin, error.to_string())
})?;
let mut values = BTreeMap::new();
for entry in resolved.entries() {
values.insert(entry.key.clone(), entry.value.clone().unwrap_or_default());
}
Ok(ResolvedEnvironment {
values,
origins: self.origins.clone(),
})
}
}
fn resolve_error_key(error: &ResolveError) -> &str {
match error {
ResolveError::Unresolved { key, .. }
| ResolveError::Required { key, .. }
| ResolveError::Cycle { key, .. }
| ResolveError::DepthExceeded { key, .. }
| ResolveError::ValueTooLarge { key, .. }
| ResolveError::Command { key, .. } => key,
}
}
#[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> {
load_cascade_with_options(directory, mode, LoadOptions::new(dialect))
}
pub fn load_cascade_with_options<P: AsRef<Path>>(
directory: P,
mode: &str,
options: LoadOptions,
) -> 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, options, true)
}
pub fn load_files<I, P>(paths: I, dialect: Dialect) -> Result<LoadedEnvironment, LoadError>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
load_files_with_options(paths, LoadOptions::new(dialect))
}
pub fn load_files_with_options<I, P>(
paths: I,
options: LoadOptions,
) -> Result<LoadedEnvironment, LoadError>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
load_paths(paths, options, false)
}
pub fn load_cascade_values<P: AsRef<Path>>(
directory: P,
mode: &str,
) -> Result<LoadedValues, LoadError> {
load_cascade_values_with(directory, mode, Dialect::PORTABLE)
}
pub fn load_cascade_values_with<P: AsRef<Path>>(
directory: P,
mode: &str,
dialect: Dialect,
) -> Result<LoadedValues, 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_value_paths(paths, dialect, true)
}
pub fn load_values<I, P>(paths: I, dialect: Dialect) -> Result<LoadedValues, LoadError>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
load_value_paths(paths, dialect, false)
}
fn load_value_paths<I, P>(
paths: I,
dialect: Dialect,
skip_missing: bool,
) -> Result<LoadedValues, LoadError>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let mut values: Option<BTreeMap<String, String>> = None;
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 projected = match crate::parser::parse_projected_values(&text, dialect) {
Ok(values) => values,
Err(diagnostic) => {
return Err(LoadError::Invalid {
path: path.to_path_buf(),
diagnostic,
});
}
};
if let Some(values) = &mut values {
values.extend(projected);
} else {
values = Some(projected);
}
}
Ok(LoadedValues {
values: values.unwrap_or_default(),
})
}
fn valid_mode(mode: &str) -> bool {
!mode.contains(['/', '\\', '\0']) && mode != "." && mode != ".."
}
fn load_paths<I, P>(
paths: I,
options: LoadOptions,
skip_missing: bool,
) -> Result<LoadedEnvironment, LoadError>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let mut loaded = LoadedEnvironment {
values: BTreeMap::new(),
origins: BTreeMap::new(),
raw_entries: BTreeMap::new(),
dialect: options.dialect,
entries: Vec::new(),
overrides: Vec::new(),
repeated: Vec::new(),
skipped: Vec::new(),
raw_tokens: options.retain_raw_tokens,
};
let mut next_order = 0;
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, options.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, options, &mut next_order);
}
Ok(loaded)
}
struct MergeState<'a> {
loaded: &'a mut LoadedEnvironment,
path: &'a Path,
options: LoadOptions,
next_order: usize,
}
fn merge_document(
loaded: &mut LoadedEnvironment,
path: &Path,
document: &crate::Document<'_>,
options: LoadOptions,
next_order: &mut usize,
) {
let mut state = MergeState {
loaded,
path,
options,
next_order: *next_order,
};
if options.retain_reports {
merge_reported(&mut state, document);
} else {
merge_effective_entries(&mut state, document);
}
*next_order = state.next_order;
}
fn merge_effective_entries(state: &mut MergeState<'_>, document: &crate::Document<'_>) {
for effective in document.effective_entries() {
merge_effective(
state,
document,
effective.key().to_string(),
effective.value().to_string(),
effective.entry(),
);
}
}
fn merge_reported(state: &mut MergeState<'_>, document: &crate::Document<'_>) {
let mut first: HashMap<&str, crate::Entry<'_>> = HashMap::new();
let mut duplicates: HashMap<&str, Vec<crate::Entry<'_>>> = HashMap::new();
let mut keys = Vec::new();
for entry in document.entries() {
let key = entry.key();
if let Some(previous) = first.get(key).copied() {
duplicates
.entry(key)
.or_insert_with(|| vec![previous])
.push(entry);
} else {
keys.push(key);
first.insert(key, entry);
}
}
for key in keys {
let all: &[crate::Entry<'_>] = if let Some(all) = duplicates.get(key) {
all
} else {
std::slice::from_ref(
first
.get(key)
.expect("every key in the occurrence order map has an entry"),
)
};
merge_reported_key(state, document, key, all);
}
}
fn merge_reported_key(
state: &mut MergeState<'_>,
document: &crate::Document<'_>,
key: &str,
all: &[crate::Entry<'_>],
) {
if key.is_empty() {
for entry in all {
state.loaded.skipped.push(LoadSkipped {
key: None,
origin: origin(state.path, *entry),
reason: SkipReason::EmptyKey,
});
}
return;
}
let mut first_participating = None;
let mut last_participating = None;
let mut participating_count = 0;
for entry in all {
if entry.participates_in_lookup() {
if first_participating.is_none() {
first_participating = Some(*entry);
}
last_participating = Some(*entry);
participating_count += 1;
}
}
if participating_count > 1 {
state.loaded.repeated.push(LoadRepeated {
key: key.to_string(),
origins: all
.iter()
.filter(|entry| entry.participates_in_lookup())
.map(|entry| origin(state.path, *entry))
.collect(),
});
}
let effective = match document.dialect().duplicates {
crate::Duplicates::First => first_participating,
crate::Duplicates::Last => last_participating,
};
let Some(effective) = effective else {
state.loaded.skipped.push(LoadSkipped {
key: Some(key.to_string()),
origin: origin(state.path, all[0]),
reason: SkipReason::NonParticipating,
});
return;
};
let value = if let Some(value) = effective.value() {
value.to_string()
} else if document.dialect().bare_keys == crate::BareKeys::Empty {
String::new()
} else {
state.loaded.skipped.push(LoadSkipped {
key: Some(key.to_string()),
origin: origin(state.path, effective),
reason: SkipReason::Bare,
});
return;
};
merge_effective(state, document, key.to_string(), value, effective);
}
fn merge_effective(
state: &mut MergeState<'_>,
document: &crate::Document<'_>,
key: String,
value: String,
effective: crate::Entry<'_>,
) {
let entry_origin = origin(state.path, effective);
let order = state.next_order;
state.next_order += 1;
if state.options.retain_raw_tokens {
let raw_key = &document.source()[effective.tokens().key.clone()];
let raw_value = effective.raw_value().unwrap_or_default();
state.loaded.raw_entries.insert(
key.clone(),
(order, raw_key.to_string(), raw_value.to_string()),
);
}
let previous = state
.loaded
.origins
.insert(key.clone(), entry_origin.clone());
if state.options.retain_reports {
state.loaded.entries.push(LoadedEntry {
key: key.clone(),
value: value.clone(),
origin: entry_origin.clone(),
});
if let Some(previous) = previous {
state.loaded.overrides.push(LoadOverride {
key: key.clone(),
previous,
replacement: entry_origin,
});
}
}
state.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(),
}
}