use std::{
collections::{BTreeMap, HashMap},
fmt, fs, io,
ops::Range,
path::{Path, PathBuf},
string::String,
sync::OnceLock,
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)]
struct CompactEntry {
key: String,
origin: LoadOrigin,
value: Option<String>,
}
#[derive(Debug, Clone)]
pub struct LoadedEnvironment {
values: BTreeMap<String, String>,
origins: BTreeMap<String, LoadOrigin>,
raw_entries: BTreeMap<String, (usize, Range<usize>, Range<usize>)>,
raw_source: String,
dialect: Dialect,
entry_records: Vec<CompactEntry>,
entries_cache: OnceLock<Vec<LoadedEntry>>,
overrides: Vec<LoadOverride>,
repeated: Vec<LoadRepeated>,
skipped: Vec<LoadSkipped>,
raw_tokens: bool,
origins_in_entries: bool,
origin_index: OnceLock<BTreeMap<String, usize>>,
}
impl PartialEq for LoadedEnvironment {
fn eq(&self, other: &Self) -> bool {
self.values == other.values
&& self.origins == other.origins
&& self.raw_entries == other.raw_entries
&& self.dialect == other.dialect
&& self.entry_records == other.entry_records
&& self.overrides == other.overrides
&& self.repeated == other.repeated
&& self.skipped == other.skipped
&& self.raw_tokens == other.raw_tokens
&& self.origins_in_entries == other.origins_in_entries
}
}
impl Eq for LoadedEnvironment {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct LoadOptions {
pub dialect: Dialect,
pub retain_raw_tokens: bool,
pub retain_reports: bool,
pub retain_origins: 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,
retain_origins: 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
}
#[must_use]
pub const fn with_origins(mut self, retain: bool) -> Self {
self.retain_origins = 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> {
if self.origins_in_entries {
let index = self.origin_index.get_or_init(|| {
self.entry_records
.iter()
.enumerate()
.map(|(index, entry)| (entry.key.clone(), index))
.collect()
});
index
.get(key)
.and_then(|index| self.entry_records.get(*index))
.map(|entry| &entry.origin)
} else {
self.origins.get(key)
}
}
fn origins_map(&self) -> BTreeMap<String, LoadOrigin> {
if self.origins_in_entries {
self.entry_records
.iter()
.map(|entry| (entry.key.clone(), entry.origin.clone()))
.collect()
} else {
self.origins.clone()
}
}
#[must_use]
pub fn entries(&self) -> &[LoadedEntry] {
self.entries_cache.get_or_init(|| {
self.entry_records
.iter()
.map(|entry| LoadedEntry {
key: entry.key.clone(),
value: entry
.value
.clone()
.or_else(|| self.values.get(&entry.key).cloned())
.unwrap_or_default(),
origin: entry.origin.clone(),
})
.collect()
})
}
#[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_map(),
})
}
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 (_, (_, key_range, value_range)) in entries {
source.push_str(&self.raw_source[key_range.clone()]);
source.push('=');
source.push_str(&self.raw_source[value_range.clone()]);
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_map(),
})
}
}
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(),
raw_source: String::new(),
dialect: options.dialect,
entry_records: Vec::new(),
entries_cache: OnceLock::new(),
overrides: Vec::new(),
repeated: Vec::new(),
skipped: Vec::new(),
raw_tokens: options.retain_raw_tokens,
origins_in_entries: options.retain_reports && options.retain_origins,
origin_index: OnceLock::new(),
};
let mut previous_origins = BTreeMap::new();
let mut previous_index = BTreeMap::new();
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 previous_origins,
&mut previous_index,
&mut next_order,
);
}
if !loaded.origins_in_entries && options.retain_origins {
loaded.origins = previous_origins;
}
loaded.raw_source.shrink_to_fit();
Ok(loaded)
}
struct MergeState<'a> {
loaded: &'a mut LoadedEnvironment,
path: &'a Path,
options: LoadOptions,
previous_origins: &'a mut BTreeMap<String, LoadOrigin>,
previous_index: &'a mut BTreeMap<String, usize>,
next_order: usize,
}
fn merge_document(
loaded: &mut LoadedEnvironment,
path: &Path,
document: &crate::Document<'_>,
options: LoadOptions,
previous_origins: &mut BTreeMap<String, LoadOrigin>,
previous_index: &mut BTreeMap<String, usize>,
next_order: &mut usize,
) {
let mut state = MergeState {
loaded,
path,
options,
previous_origins,
previous_index,
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();
let key_start = state.loaded.raw_source.len();
state.loaded.raw_source.push_str(raw_key);
let key_range = key_start..state.loaded.raw_source.len();
let value_start = state.loaded.raw_source.len();
state.loaded.raw_source.push_str(raw_value);
let value_range = value_start..state.loaded.raw_source.len();
state
.loaded
.raw_entries
.insert(key.clone(), (order, key_range, value_range));
}
if state.options.retain_reports {
let entry_index = state.loaded.entry_records.len();
let previous = if state.options.retain_origins {
state.previous_index.insert(key.clone(), entry_index)
} else {
None
};
let previous_value = state.loaded.values.get(&key).cloned();
state.loaded.entry_records.push(CompactEntry {
key: key.clone(),
origin: entry_origin.clone(),
value: None,
});
if let (Some(previous), true) = (previous, state.options.retain_origins) {
let previous_origin = state.loaded.entry_records[previous].origin.clone();
if let Some(value) = previous_value {
state.loaded.entry_records[previous].value = Some(value);
}
state.loaded.overrides.push(LoadOverride {
key: key.clone(),
previous: previous_origin,
replacement: entry_origin,
});
}
} else if state.options.retain_origins {
state.previous_origins.insert(key.clone(), 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(),
}
}