Skip to main content

adotenv/
resolve.rs

1//! Opt-in literal variable resolution with explicit lookup and origins.
2//!
3//! Resolution never reads the process environment, files or commands. A caller
4//! supplies a lookup closure; command substitutions remain literal in this
5//! slice. Every substituted value records its direct source.
6
7use alloc::{
8    collections::BTreeMap,
9    string::{String, ToString},
10    vec,
11    vec::Vec,
12};
13use core::{fmt, ops::Range};
14
15use crate::{
16    AssignmentState, BareKeys, Document, Entry, Reference, ReferenceKind, ReferenceOperator,
17    ReferenceRules,
18};
19
20/// Whether later definitions can see earlier definitions or the final file.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub enum ExpandScope {
24    /// A reference sees the latest participating declaration before it.
25    Sequential,
26    /// A reference sees the final participating declaration in the file.
27    WholeFile,
28}
29
30/// Whether the caller lookup or the document's own bindings win.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32#[non_exhaustive]
33pub enum LookupOrder {
34    /// Consult the caller lookup before document bindings.
35    CallerFirst,
36    /// Consult document bindings before the caller lookup. A participating
37    /// declaration with no value shadows the caller lookup.
38    FileFirst,
39}
40
41/// Treatment of an eligible reference with no value from the lookup order.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[non_exhaustive]
44pub enum Unresolved {
45    /// Leave the reference exactly as written.
46    Keep,
47    /// Substitute an empty string.
48    Empty,
49    /// Return [`ResolveError::Unresolved`].
50    Error,
51}
52
53/// Explicit resolution policy and resource limits.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55#[non_exhaustive]
56pub struct ExpandOptions {
57    /// Quote, escape and operator eligibility rules.
58    pub rules: ReferenceRules,
59    /// Sequential or whole-file bindings.
60    pub scope: ExpandScope,
61    /// Caller lookup versus file binding precedence.
62    pub lookup_order: LookupOrder,
63    /// Treatment of an eligible reference with no value.
64    pub unresolved: Unresolved,
65    /// Maximum active binding and reference nesting depth.
66    pub max_depth: usize,
67    /// Maximum resolved bytes for one entry value.
68    pub max_value_bytes: usize,
69}
70
71impl ExpandOptions {
72    /// Use the given reference eligibility rules.
73    #[must_use]
74    pub const fn with_rules(mut self, value: ReferenceRules) -> Self {
75        self.rules = value;
76        self
77    }
78    /// Use the given resolution scope.
79    #[must_use]
80    pub const fn with_scope(mut self, value: ExpandScope) -> Self {
81        self.scope = value;
82        self
83    }
84    /// Use the given lookup precedence.
85    #[must_use]
86    pub const fn with_lookup_order(mut self, value: LookupOrder) -> Self {
87        self.lookup_order = value;
88        self
89    }
90    /// Use the given unresolved-reference policy.
91    #[must_use]
92    pub const fn with_unresolved(mut self, value: Unresolved) -> Self {
93        self.unresolved = value;
94        self
95    }
96    /// Set the maximum nesting depth.
97    #[must_use]
98    pub const fn with_max_depth(mut self, value: usize) -> Self {
99        self.max_depth = value;
100        self
101    }
102    /// Set the maximum resolved value size.
103    #[must_use]
104    pub const fn with_max_value_bytes(mut self, value: usize) -> Self {
105        self.max_value_bytes = value;
106        self
107    }
108}
109impl ExpandOptions {
110    /// Portable-style basic variables with file-first sequential bindings.
111    #[must_use]
112    pub const fn portable() -> Self {
113        Self {
114            rules: ReferenceRules::PORTABLE,
115            scope: ExpandScope::Sequential,
116            lookup_order: LookupOrder::FileFirst,
117            unresolved: Unresolved::Keep,
118            max_depth: 64,
119            max_value_bytes: 1_048_576,
120        }
121    }
122    /// Compose-style operators and nesting with file-first sequential bindings.
123    #[must_use]
124    pub const fn compose() -> Self {
125        Self {
126            rules: ReferenceRules::COMPOSE,
127            ..Self::portable()
128        }
129    }
130    /// dotenv-expand-style rules with file-first sequential bindings.
131    #[must_use]
132    pub const fn dotenv_expand() -> Self {
133        Self {
134            rules: ReferenceRules::DOTENV_EXPAND,
135            ..Self::portable()
136        }
137    }
138    /// No variable forms are eligible; values remain literal.
139    #[must_use]
140    pub const fn none() -> Self {
141        Self {
142            rules: ReferenceRules::NONE,
143            ..Self::portable()
144        }
145    }
146}
147
148impl Default for ExpandOptions {
149    fn default() -> Self {
150        Self::portable()
151    }
152}
153
154/// Structured input passed to a command-substitution callback.
155#[derive(Debug, Clone, PartialEq, Eq)]
156#[non_exhaustive]
157pub struct CommandContext<'a> {
158    /// Command text exactly as written between `$(` and `)`.
159    pub command: &'a str,
160    /// Whole `$(...)` reference span in the original source.
161    pub reference: Range<usize>,
162    /// Raw command payload span in the original source.
163    pub payload: Range<usize>,
164    /// Optional caller-supplied source identity; never inferred from a path.
165    pub source: Option<&'a str>,
166}
167
168/// An error returned by a command-substitution callback.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct CommandError {
171    message: String,
172}
173
174impl CommandError {
175    /// Create a callback error with a caller-provided message.
176    #[must_use]
177    pub fn new(message: impl Into<String>) -> Self {
178        Self {
179            message: message.into(),
180        }
181    }
182    /// The caller-provided message.
183    #[must_use]
184    pub fn message(&self) -> &str {
185        &self.message
186    }
187}
188
189impl fmt::Display for CommandError {
190    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
191        formatter.write_str(&self.message)
192    }
193}
194
195impl core::error::Error for CommandError {}
196
197/// Recorded outcome of one invoked command callback.
198#[derive(Debug, Clone, PartialEq, Eq)]
199#[non_exhaustive]
200pub enum CommandOutcome {
201    /// The callback returned this output.
202    Output(String),
203    /// The callback returned this error message.
204    Error(String),
205}
206
207/// Provenance for one invoked command callback.
208#[derive(Debug, Clone, PartialEq, Eq)]
209#[non_exhaustive]
210pub struct CommandCall {
211    /// Whole `$(...)` reference span in the original source.
212    pub reference: Range<usize>,
213    /// Raw command payload span in the original source.
214    pub payload: Range<usize>,
215    /// Caller-supplied source identity, if any.
216    pub source: Option<String>,
217    /// Recorded output or error.
218    pub outcome: CommandOutcome,
219}
220
221/// A resolution failure that retains command calls already made.
222#[derive(Debug, Clone, PartialEq, Eq)]
223#[non_exhaustive]
224pub struct ResolveFailure {
225    /// The structured resolution error.
226    pub error: ResolveError,
227    /// Command callbacks invoked before the failure, in invocation order.
228    pub commands: Vec<CommandCall>,
229}
230
231impl fmt::Display for ResolveFailure {
232    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
233        self.error.fmt(formatter)
234    }
235}
236
237impl core::error::Error for ResolveFailure {}
238
239/// Where one resolved variable value came from.
240#[derive(Debug, Clone, PartialEq, Eq)]
241#[non_exhaustive]
242pub enum ValueOrigin {
243    /// A participating declaration in this document.
244    File {
245        /// Snapshot-local entry identity.
246        entry: usize,
247        /// Binding key.
248        key: String,
249        /// Original value-token or line span.
250        span: Range<usize>,
251    },
252    /// The caller-supplied lookup.
253    Lookup,
254}
255
256/// One resolved value part.
257#[derive(Debug, Clone, PartialEq, Eq)]
258#[non_exhaustive]
259pub struct ResolvedSegment {
260    /// Exact text contributed by this part.
261    pub value: String,
262    /// Provenance and structural role.
263    pub kind: ResolvedSegmentKind,
264}
265
266/// The role of one resolved segment.
267#[derive(Debug, Clone, PartialEq, Eq)]
268#[non_exhaustive]
269pub enum ResolvedSegmentKind {
270    /// Literal decoded source text.
271    Literal {
272        /// Original-source region, when representable.
273        span: Range<usize>,
274    },
275    /// A substituted variable reference.
276    Variable {
277        /// Referenced name.
278        name: String,
279        /// Whole reference span in the original source.
280        reference: Range<usize>,
281        /// Variable operator, when present.
282        operator: Option<ReferenceOperator>,
283        /// Direct value origin; absent for an empty unresolved substitution.
284        origin: Option<ValueOrigin>,
285        /// Nested provenance of a file binding; empty for caller lookup.
286        parts: Vec<ResolvedSegment>,
287    },
288    /// An eligible reference left literal by [`Unresolved::Keep`].
289    Unresolved {
290        /// Referenced name.
291        name: String,
292        /// Whole reference span in the original source.
293        reference: Range<usize>,
294    },
295    /// Output of a caller-supplied command callback.
296    Command {
297        /// Whole `$(...)` reference span in the original source.
298        reference: Range<usize>,
299        /// Raw command payload span in the original source.
300        payload: Range<usize>,
301        /// Caller-supplied source identity, if any.
302        source: Option<String>,
303    },
304}
305
306/// One entry's resolved value and provenance.
307#[derive(Debug, Clone, PartialEq, Eq)]
308#[non_exhaustive]
309pub struct ResolvedEntry {
310    /// Snapshot-local entry identity from the originating document.
311    pub entry: usize,
312    /// Literal key.
313    pub key: String,
314    /// Original assignment state.
315    pub state: AssignmentState,
316    /// Resolved assigned value; `None` for a bare declaration.
317    pub value: Option<String>,
318    /// Effective literal lookup value after participation and bare-key policy.
319    pub effective_value: Option<String>,
320    /// Ordered provenance segments; concatenation equals `value`.
321    pub segments: Vec<ResolvedSegment>,
322}
323
324/// All resolved entries in source order.
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct ResolvedDocument {
327    entries: Vec<ResolvedEntry>,
328    commands: Vec<CommandCall>,
329}
330
331impl ResolvedDocument {
332    /// Resolved entries in source order.
333    #[must_use]
334    pub fn entries(&self) -> &[ResolvedEntry] {
335        &self.entries
336    }
337    /// Command callbacks invoked during resolution, in invocation order.
338    #[must_use]
339    pub fn commands(&self) -> &[CommandCall] {
340        &self.commands
341    }
342    /// Number of resolved entries.
343    #[must_use]
344    pub fn len(&self) -> usize {
345        self.entries.len()
346    }
347    /// Whether there are no entries.
348    #[must_use]
349    pub fn is_empty(&self) -> bool {
350        self.entries.is_empty()
351    }
352}
353
354/// A resolution failure.
355#[derive(Debug, Clone, PartialEq, Eq)]
356#[non_exhaustive]
357pub enum ResolveError {
358    /// An eligible variable had no value under [`Unresolved::Error`].
359    Unresolved {
360        /// Entry being resolved.
361        key: String,
362        /// Referenced name.
363        name: String,
364        /// Reference span in the original source.
365        span: Range<usize>,
366    },
367    /// An `:?` or `?` operator required a value.
368    Required {
369        /// Entry being resolved.
370        key: String,
371        /// Referenced name.
372        name: String,
373        /// Operator message.
374        message: String,
375        /// Reference span in the original source.
376        span: Range<usize>,
377    },
378    /// Reachable bindings formed a cycle.
379    Cycle {
380        /// Entry where the cycle was detected.
381        key: String,
382        /// Keys in cycle order, including the repeated final key.
383        chain: Vec<String>,
384    },
385    /// The configured depth limit was reached.
386    DepthExceeded {
387        /// Entry being resolved.
388        key: String,
389        /// Configured limit.
390        limit: usize,
391        /// Reference or entry span.
392        span: Range<usize>,
393    },
394    /// One resolved value exceeded the configured byte limit.
395    ValueTooLarge {
396        /// Entry being resolved.
397        key: String,
398        /// Configured limit.
399        limit: usize,
400        /// Entry value or line span.
401        span: Range<usize>,
402    },
403    /// A command callback failed.
404    Command {
405        /// Entry being resolved.
406        key: String,
407        /// Whole `$(...)` reference span.
408        reference: Range<usize>,
409        /// Callback error message.
410        message: String,
411    },
412}
413
414impl fmt::Display for ResolveError {
415    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
416        match self {
417            Self::Unresolved { key, name, .. } => {
418                write!(
419                    formatter,
420                    "unresolved variable {name} while resolving {key}"
421                )
422            }
423            Self::Required {
424                key, name, message, ..
425            } => write!(
426                formatter,
427                "required variable {name} is unset or empty while resolving {key}: {message}"
428            ),
429            Self::Cycle { key, chain } => {
430                write!(formatter, "reference cycle while resolving {key}: ")?;
431                for (index, name) in chain.iter().enumerate() {
432                    if index != 0 {
433                        formatter.write_str(" -> ")?;
434                    }
435                    formatter.write_str(name)?;
436                }
437                Ok(())
438            }
439            Self::DepthExceeded { key, limit, .. } => {
440                write!(
441                    formatter,
442                    "reference depth limit {limit} exceeded while resolving {key}"
443                )
444            }
445            Self::ValueTooLarge { key, limit, .. } => {
446                write!(formatter, "resolved value for {key} exceeds {limit} bytes")
447            }
448            Self::Command { key, message, .. } => {
449                write!(
450                    formatter,
451                    "command callback failed while resolving {key}: {message}"
452                )
453            }
454        }
455    }
456}
457
458impl core::error::Error for ResolveError {}
459
460impl Document<'_> {
461    /// Resolve eligible variable references with an explicit caller lookup.
462    ///
463    /// The lookup closure is consulted only when the selected order reaches it;
464    /// no process environment, file or command access occurs. Command forms and
465    /// ineligible references remain literal in this slice. Every substituted
466    /// variable records its direct file or caller origin and, for file bindings,
467    /// nested provenance.
468    ///
469    /// # Errors
470    /// Returns a structured error for required variables, unresolved variables
471    /// under [`Unresolved::Error`], reachable cycles, or configured limits.
472    ///
473    /// # Examples
474    ///
475    /// ```
476    /// use adotenv::{parse_strict, ExpandOptions};
477    ///
478    /// let doc = parse_strict("A=base\nB=${A}-suffix\n").unwrap();
479    /// let resolved = doc
480    ///     .resolve(|_| None, &ExpandOptions::portable())
481    ///     .unwrap();
482    /// assert_eq!(resolved.entries()[1].value.as_deref(), Some("base-suffix"));
483    /// ```
484    pub fn resolve<F>(
485        &self,
486        lookup: F,
487        options: &ExpandOptions,
488    ) -> Result<ResolvedDocument, ResolveError>
489    where
490        F: Fn(&str) -> Option<String>,
491    {
492        let mut resolver = Resolver {
493            document: self,
494            lookup,
495            options: *options,
496            bindings: BTreeMap::new(),
497            cache: BTreeMap::new(),
498            stack: Vec::new(),
499            commands: None,
500            source_identity: None,
501            command_calls: Vec::new(),
502        };
503        resolver.resolve_document()
504    }
505
506    /// Resolve with an explicit command-substitution callback.
507    ///
508    /// Commands are invoked only when both [`ReferenceRules::commands`] and this
509    /// callback permit them. The callback receives the raw command text, source
510    /// spans and the supplied source identity. Outputs are literal resolved data.
511    /// Every call is recorded; [`ResolveFailure::commands`] retains calls made
512    /// before a later error. No shell, environment or file access is implied.
513    ///
514    /// # Errors
515    /// Returns the same structured errors as [`Self::resolve`], wraps callback
516    /// failures, and retains prior command-call provenance in [`ResolveFailure`].
517    pub fn resolve_with_commands<F, C>(
518        &self,
519        lookup: F,
520        mut commands: C,
521        source_identity: Option<&str>,
522        options: &ExpandOptions,
523    ) -> Result<ResolvedDocument, ResolveFailure>
524    where
525        F: Fn(&str) -> Option<String>,
526        C: FnMut(CommandContext<'_>) -> Result<String, CommandError>,
527    {
528        let mut resolver = Resolver {
529            document: self,
530            lookup,
531            options: *options,
532            bindings: BTreeMap::new(),
533            cache: BTreeMap::new(),
534            stack: Vec::new(),
535            commands: Some(&mut commands),
536            source_identity: source_identity.map(str::to_string),
537            command_calls: Vec::new(),
538        };
539        match resolver.resolve_document() {
540            Ok(mut document) => {
541                document.commands = core::mem::take(&mut resolver.command_calls);
542                Ok(document)
543            }
544            Err(error) => Err(ResolveFailure {
545                error,
546                commands: core::mem::take(&mut resolver.command_calls),
547            }),
548        }
549    }
550}
551
552type CommandCallback<'a> = dyn FnMut(CommandContext<'_>) -> Result<String, CommandError> + 'a;
553
554struct Resolver<'a, 'src, F> {
555    document: &'a Document<'src>,
556    lookup: F,
557    options: ExpandOptions,
558    bindings: BTreeMap<&'a str, usize>,
559    cache: BTreeMap<usize, ResolvedEntry>,
560    stack: Vec<usize>,
561    commands: Option<&'a mut CommandCallback<'a>>,
562    source_identity: Option<String>,
563    command_calls: Vec<CommandCall>,
564}
565
566enum Bound {
567    Unset,
568    Value {
569        text: String,
570        parts: Vec<ResolvedSegment>,
571        origin: ValueOrigin,
572    },
573}
574
575impl<'a, F> Resolver<'a, '_, F>
576where
577    F: Fn(&str) -> Option<String>,
578{
579    fn resolve_document(&mut self) -> Result<ResolvedDocument, ResolveError> {
580        let entries = match self.options.scope {
581            ExpandScope::Sequential => {
582                let mut entries = Vec::new();
583                for entry in self.document.entries() {
584                    let entry_result = self.resolve_entry(entry.id())?;
585                    if entry.participates_in_lookup() {
586                        self.bindings.insert(entry.key(), entry.id());
587                    }
588                    entries.push(entry_result);
589                }
590                entries
591            }
592            ExpandScope::WholeFile => {
593                for entry in self.document.entries() {
594                    if entry.participates_in_lookup() {
595                        self.bindings.insert(entry.key(), entry.id());
596                    }
597                }
598                let mut entries = Vec::new();
599                for entry in self.document.entries() {
600                    entries.push(self.resolve_entry(entry.id())?);
601                }
602                entries
603            }
604        };
605        Ok(ResolvedDocument {
606            entries,
607            commands: Vec::new(),
608        })
609    }
610
611    fn resolve_entry(&mut self, id: usize) -> Result<ResolvedEntry, ResolveError> {
612        if let Some(cached) = self.cache.get(&id) {
613            return Ok(cached.clone());
614        }
615        let entry = self
616            .document
617            .entry(id)
618            .expect("resolved entry id belongs to the document");
619        if let Some(position) = self.stack.iter().position(|current| *current == id) {
620            let mut chain = self.stack[position..]
621                .iter()
622                .map(|current| {
623                    self.document
624                        .entry(*current)
625                        .expect("resolved stack id belongs to the document")
626                        .key()
627                        .to_string()
628                })
629                .collect::<Vec<_>>();
630            chain.push(entry.key().to_string());
631            return Err(ResolveError::Cycle {
632                key: entry.key().to_string(),
633                chain,
634            });
635        }
636        if self.stack.len() > self.options.max_depth {
637            return Err(ResolveError::DepthExceeded {
638                key: entry.key().to_string(),
639                limit: self.options.max_depth,
640                span: entry.line_span(),
641            });
642        }
643        self.stack.push(id);
644        let result = self.resolve_entry_value(entry);
645        self.stack.pop();
646        let resolved = result?;
647        self.cache.insert(id, resolved.clone());
648        Ok(resolved)
649    }
650
651    fn resolve_entry_value(&mut self, entry: Entry<'a>) -> Result<ResolvedEntry, ResolveError> {
652        let key = entry.key().to_string();
653        let state = entry.state();
654        let Some(value) = entry.value() else {
655            return Ok(ResolvedEntry {
656                entry: entry.id(),
657                key,
658                state,
659                value: None,
660                effective_value: effective_binding(entry, None),
661                segments: Vec::new(),
662            });
663        };
664        let references = entry.references();
665        let segments = self.resolve_segments(entry, value, 0..value.len(), None, &references)?;
666        let resolved = segments
667            .iter()
668            .map(|segment| segment.value.as_str())
669            .collect::<String>();
670        if resolved.len() > self.options.max_value_bytes {
671            return Err(ResolveError::ValueTooLarge {
672                key,
673                limit: self.options.max_value_bytes,
674                span: entry.value_span().unwrap_or_else(|| entry.line_span()),
675            });
676        }
677        Ok(ResolvedEntry {
678            entry: entry.id(),
679            effective_value: effective_binding(entry, Some(&resolved)),
680            key,
681            state,
682            value: Some(resolved),
683            segments,
684        })
685    }
686
687    fn resolve_segments(
688        &mut self,
689        entry: Entry<'a>,
690        decoded: &str,
691        range: Range<usize>,
692        parent: Option<&Reference>,
693        references: &[Reference],
694    ) -> Result<Vec<ResolvedSegment>, ResolveError> {
695        let mut segments = Vec::new();
696        let mut length = 0usize;
697        let mut cursor = range.start;
698        for reference in references {
699            if let Some(parent) = parent {
700                if reference.depth != parent.depth.saturating_add(1)
701                    || !reference.span_within(parent.argument.as_ref())
702                {
703                    continue;
704                }
705            } else if !reference.containers.is_empty() {
706                continue;
707            }
708            if !reference.is_eligible(self.options.rules) {
709                continue;
710            }
711            if reference.kind == ReferenceKind::Command && self.commands.is_none() {
712                continue;
713            }
714            if reference.depth > self.options.max_depth {
715                return Err(ResolveError::DepthExceeded {
716                    key: entry.key().to_string(),
717                    limit: self.options.max_depth,
718                    span: reference.span.clone(),
719                });
720            }
721            let Some(start) = decoded_start_for_raw(entry, reference.span.start) else {
722                continue;
723            };
724            let Some(end) = decoded_end_for_raw(entry, reference.span.end) else {
725                continue;
726            };
727            if start < range.start || end > range.end || start < cursor || start > end {
728                continue;
729            }
730            if start > cursor {
731                let literal = literal_segment(entry, decoded, cursor..start);
732                account(&mut length, &literal, self.options.max_value_bytes, entry)?;
733                segments.push(literal);
734            }
735            let replacement = match reference.kind {
736                ReferenceKind::Variable => {
737                    self.resolve_reference(entry, reference, decoded, start..end, references)?
738                }
739                ReferenceKind::Command => self.run_command(entry, reference)?,
740            };
741            for segment in replacement {
742                account(&mut length, &segment, self.options.max_value_bytes, entry)?;
743                segments.push(segment);
744            }
745            cursor = end;
746        }
747        if cursor < range.end {
748            let literal = literal_segment(entry, decoded, cursor..range.end);
749            account(&mut length, &literal, self.options.max_value_bytes, entry)?;
750            segments.push(literal);
751        }
752        Ok(segments)
753    }
754
755    fn run_command(
756        &mut self,
757        entry: Entry<'a>,
758        reference: &Reference,
759    ) -> Result<Vec<ResolvedSegment>, ResolveError> {
760        let payload = reference
761            .command
762            .clone()
763            .expect("eligible command has a payload");
764        let command = self.document.source()[payload.clone()].to_string();
765        let reference_span = reference.span.clone();
766        let source = self.source_identity.clone();
767        let result = {
768            let context = CommandContext {
769                command: &command,
770                reference: reference_span.clone(),
771                payload: payload.clone(),
772                source: source.as_deref(),
773            };
774            let callback = self
775                .commands
776                .as_mut()
777                .expect("command callback was checked before invocation");
778            callback(context)
779        };
780        match result {
781            Ok(output) => {
782                self.command_calls.push(CommandCall {
783                    reference: reference_span.clone(),
784                    payload: payload.clone(),
785                    source: source.clone(),
786                    outcome: CommandOutcome::Output(output.clone()),
787                });
788                Ok(vec![ResolvedSegment {
789                    value: output,
790                    kind: ResolvedSegmentKind::Command {
791                        reference: reference_span,
792                        payload,
793                        source,
794                    },
795                }])
796            }
797            Err(error) => {
798                let message = error.message().to_string();
799                self.command_calls.push(CommandCall {
800                    reference: reference_span.clone(),
801                    payload,
802                    source,
803                    outcome: CommandOutcome::Error(message.clone()),
804                });
805                Err(ResolveError::Command {
806                    key: entry.key().to_string(),
807                    reference: reference_span,
808                    message,
809                })
810            }
811        }
812    }
813
814    fn resolve_reference(
815        &mut self,
816        entry: Entry<'a>,
817        reference: &Reference,
818        decoded: &str,
819        decoded_span: Range<usize>,
820        references: &[Reference],
821    ) -> Result<Vec<ResolvedSegment>, ResolveError> {
822        let name_span = reference
823            .name
824            .as_ref()
825            .expect("eligible variable has a name");
826        let name = self.document.source()[name_span.clone()].to_string();
827        let bound = self.lookup_variable(&name)?;
828        let (set, empty, text, parts, origin) = match bound {
829            Some(Bound::Value {
830                text,
831                parts,
832                origin,
833            }) => (true, text.is_empty(), text, parts, Some(origin)),
834            _ => (false, true, String::new(), Vec::new(), None),
835        };
836        let variable = |text: String, parts: Vec<ResolvedSegment>, origin: Option<ValueOrigin>| {
837            ResolvedSegment {
838                value: text,
839                kind: ResolvedSegmentKind::Variable {
840                    name: name.clone(),
841                    reference: reference.span.clone(),
842                    operator: reference.operator,
843                    origin,
844                    parts,
845                },
846            }
847        };
848        match reference.operator {
849            None => {
850                if set {
851                    Ok(vec![variable(text, parts, origin)])
852                } else {
853                    self.unresolved(reference, decoded, decoded_span, &name)
854                }
855            }
856            Some(ReferenceOperator::DefaultIfUnset) => {
857                if set {
858                    Ok(vec![variable(text, parts, origin)])
859                } else {
860                    self.argument(entry, reference, decoded, references)
861                }
862            }
863            Some(ReferenceOperator::DefaultIfUnsetOrEmpty) => {
864                if set && !empty {
865                    Ok(vec![variable(text, parts, origin)])
866                } else {
867                    self.argument(entry, reference, decoded, references)
868                }
869            }
870            Some(ReferenceOperator::AlternativeIfSet) => {
871                if set {
872                    self.argument(entry, reference, decoded, references)
873                } else {
874                    Ok(Vec::new())
875                }
876            }
877            Some(ReferenceOperator::AlternativeIfSetAndNonEmpty) => {
878                if set && !empty {
879                    self.argument(entry, reference, decoded, references)
880                } else {
881                    Ok(Vec::new())
882                }
883            }
884            Some(ReferenceOperator::ErrorIfUnset) => {
885                if set {
886                    Ok(vec![variable(text, parts, origin)])
887                } else {
888                    Err(self.required(entry, reference, decoded, name))
889                }
890            }
891            Some(ReferenceOperator::ErrorIfUnsetOrEmpty) => {
892                if set && !empty {
893                    Ok(vec![variable(text, parts, origin)])
894                } else {
895                    Err(self.required(entry, reference, decoded, name))
896                }
897            }
898        }
899    }
900
901    fn unresolved(
902        &self,
903        reference: &Reference,
904        decoded: &str,
905        decoded_span: Range<usize>,
906        name: &str,
907    ) -> Result<Vec<ResolvedSegment>, ResolveError> {
908        Ok(match self.options.unresolved {
909            Unresolved::Keep => vec![ResolvedSegment {
910                value: decoded[decoded_span].to_string(),
911                kind: ResolvedSegmentKind::Unresolved {
912                    name: name.to_string(),
913                    reference: reference.span.clone(),
914                },
915            }],
916            Unresolved::Empty => Vec::new(),
917            Unresolved::Error => {
918                return Err(ResolveError::Unresolved {
919                    key: self.current_key(),
920                    name: name.to_string(),
921                    span: reference.span.clone(),
922                });
923            }
924        })
925    }
926
927    fn required(
928        &self,
929        entry: Entry<'a>,
930        reference: &Reference,
931        decoded: &str,
932        name: String,
933    ) -> ResolveError {
934        let message = reference
935            .argument
936            .as_ref()
937            .map_or_else(String::new, |argument| {
938                decoded_start_for_raw(entry, argument.start)
939                    .zip(decoded_end_for_raw(entry, argument.end))
940                    .map_or_else(
941                        || self.document.source()[argument.clone()].to_string(),
942                        |(start, end)| decoded[start..end].to_string(),
943                    )
944            });
945        ResolveError::Required {
946            key: entry.key().to_string(),
947            name,
948            message,
949            span: reference.span.clone(),
950        }
951    }
952
953    fn argument(
954        &mut self,
955        entry: Entry<'a>,
956        reference: &Reference,
957        decoded: &str,
958        references: &[Reference],
959    ) -> Result<Vec<ResolvedSegment>, ResolveError> {
960        let Some(argument) = reference.argument.as_ref() else {
961            return Ok(Vec::new());
962        };
963        let Some(start) = decoded_start_for_raw(entry, argument.start) else {
964            return Ok(vec![ResolvedSegment {
965                value: self.document.source()[argument.clone()].to_string(),
966                kind: ResolvedSegmentKind::Literal {
967                    span: argument.clone(),
968                },
969            }]);
970        };
971        let Some(end) = decoded_end_for_raw(entry, argument.end) else {
972            return Ok(vec![ResolvedSegment {
973                value: self.document.source()[argument.clone()].to_string(),
974                kind: ResolvedSegmentKind::Literal {
975                    span: argument.clone(),
976                },
977            }]);
978        };
979        self.resolve_segments(entry, decoded, start..end, Some(reference), references)
980    }
981
982    fn lookup_variable(&mut self, name: &str) -> Result<Option<Bound>, ResolveError> {
983        match self.options.lookup_order {
984            LookupOrder::CallerFirst => {
985                if let Some(value) = (self.lookup)(name) {
986                    return Ok(Some(Bound::Value {
987                        text: value,
988                        parts: Vec::new(),
989                        origin: ValueOrigin::Lookup,
990                    }));
991                }
992                self.file_binding(name)
993            }
994            LookupOrder::FileFirst => {
995                let file = self.file_binding(name)?;
996                if file.is_some() {
997                    return Ok(file);
998                }
999                Ok((self.lookup)(name).map(|value| Bound::Value {
1000                    text: value,
1001                    parts: Vec::new(),
1002                    origin: ValueOrigin::Lookup,
1003                }))
1004            }
1005        }
1006    }
1007
1008    fn file_binding(&mut self, name: &str) -> Result<Option<Bound>, ResolveError> {
1009        let Some(id) = self.bindings.get(name).copied() else {
1010            return Ok(None);
1011        };
1012        let entry = self
1013            .document
1014            .entry(id)
1015            .expect("binding id belongs to the document");
1016        if entry.value().is_none() {
1017            if entry.state() == AssignmentState::Bare
1018                && self.document.dialect().bare_keys == BareKeys::Empty
1019            {
1020                return Ok(Some(Bound::Value {
1021                    text: String::new(),
1022                    parts: Vec::new(),
1023                    origin: file_origin(entry),
1024                }));
1025            }
1026            return Ok(Some(Bound::Unset));
1027        }
1028        let resolved = self.resolve_entry(id)?;
1029        Ok(Some(Bound::Value {
1030            text: resolved.value.unwrap_or_default(),
1031            parts: resolved.segments,
1032            origin: file_origin(entry),
1033        }))
1034    }
1035
1036    fn current_key(&self) -> String {
1037        self.stack
1038            .last()
1039            .and_then(|id| self.document.entry(*id))
1040            .map_or_else(String::new, |entry| entry.key().to_string())
1041    }
1042}
1043
1044impl Reference {
1045    fn span_within(&self, parent: Option<&Range<usize>>) -> bool {
1046        parent.is_some_and(|parent| parent.start <= self.span.start && self.span.end <= parent.end)
1047    }
1048}
1049
1050fn effective_binding(entry: Entry<'_>, value: Option<&str>) -> Option<String> {
1051    if !entry.participates_in_lookup() {
1052        return None;
1053    }
1054    match value {
1055        Some(value) => Some(value.to_string()),
1056        None if entry.state() == AssignmentState::Bare
1057            && entry.document.dialect().bare_keys == BareKeys::Empty =>
1058        {
1059            Some(String::new())
1060        }
1061        None => None,
1062    }
1063}
1064
1065fn file_origin(entry: Entry<'_>) -> ValueOrigin {
1066    ValueOrigin::File {
1067        entry: entry.id(),
1068        key: entry.key().to_string(),
1069        span: entry.value_span().unwrap_or_else(|| entry.line_span()),
1070    }
1071}
1072
1073fn literal_segment(entry: Entry<'_>, decoded: &str, range: Range<usize>) -> ResolvedSegment {
1074    let span = entry
1075        .decoded_offset_to_raw(range.start)
1076        .ok()
1077        .zip(entry.decoded_offset_to_raw(range.end).ok())
1078        .map_or_else(
1079            || entry.value_span().unwrap_or_else(|| entry.line_span()),
1080            |(start, end)| start..end,
1081        );
1082    ResolvedSegment {
1083        value: decoded[range].to_string(),
1084        kind: ResolvedSegmentKind::Literal { span },
1085    }
1086}
1087
1088fn account(
1089    length: &mut usize,
1090    segment: &ResolvedSegment,
1091    limit: usize,
1092    entry: Entry<'_>,
1093) -> Result<(), ResolveError> {
1094    *length = length.saturating_add(segment.value.len());
1095    if *length > limit {
1096        return Err(ResolveError::ValueTooLarge {
1097            key: entry.key().to_string(),
1098            limit,
1099            span: entry.line_span(),
1100        });
1101    }
1102    Ok(())
1103}
1104
1105fn decoded_start_for_raw(entry: Entry<'_>, raw: usize) -> Option<usize> {
1106    let value = entry.value()?;
1107    let mut start = None;
1108    for (offset, _) in value.char_indices() {
1109        let mapped = entry.decoded_offset_to_raw(offset).ok()?;
1110        if mapped <= raw {
1111            start = Some(offset);
1112        } else {
1113            break;
1114        }
1115    }
1116    if entry.decoded_offset_to_raw(value.len()).ok()? <= raw {
1117        start = Some(value.len());
1118    }
1119    start
1120}
1121
1122fn decoded_end_for_raw(entry: Entry<'_>, raw: usize) -> Option<usize> {
1123    let value = entry.value()?;
1124    for (offset, _) in value.char_indices() {
1125        if entry.decoded_offset_to_raw(offset).ok()? >= raw {
1126            return Some(offset);
1127        }
1128    }
1129    Some(value.len())
1130}