Skip to main content

adotenv/
dialect.rs

1//! Explicit literal syntax configuration. Presets perform no expansion or I/O.
2
3/// The statement grammar used to recognize bindings.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5#[non_exhaustive]
6pub enum Syntax {
7    /// The crate's conservative, lossless default grammar.
8    Portable,
9    /// Node dotenv's default regular-expression grammar.
10    Node,
11    /// Python dotenv's stream grammar.
12    Python,
13    /// Compose's dotenv grammar, without interpolation.
14    Compose,
15    /// Docker's raw key/value-file grammar.
16    Docker,
17}
18
19/// Recognition of inline comments (whole-statement comments are always enabled).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum InlineComments {
23    /// A hash preceded by configured whitespace.
24    Whitespace,
25    /// A hash preceded by a literal ASCII space within the value/header tail.
26    Space,
27    /// Any hash starts a comment.
28    AnyHash,
29    /// Whitespace-delimited values, but any hash after a bare key (Python).
30    Python,
31    /// Hashes in values are literal.
32    None,
33}
34
35/// Independently enabled double-quoted escape sequences.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub struct DoubleEscapes(u16);
38impl DoubleEscapes {
39    /// No recognized escapes.
40    pub const NONE: Self = Self(0);
41    /// `\\n`.
42    pub const LF: Self = Self(1);
43    /// `\\r`.
44    pub const CR: Self = Self(1 << 1);
45    /// `\\t`.
46    pub const TAB: Self = Self(1 << 2);
47    /// `\\\\`.
48    pub const BACKSLASH: Self = Self(1 << 3);
49    /// `\\"`.
50    pub const DOUBLE_QUOTE: Self = Self(1 << 4);
51    /// `\\$` (literal dollar, not interpolation).
52    pub const DOLLAR: Self = Self(1 << 5);
53    /// `\\'`.
54    pub const SINGLE_QUOTE: Self = Self(1 << 6);
55    /// `\\a`.
56    pub const BELL: Self = Self(1 << 7);
57    /// `\\b`.
58    pub const BACKSPACE: Self = Self(1 << 8);
59    /// `\\f`.
60    pub const FORM_FEED: Self = Self(1 << 9);
61    /// `\\v`.
62    pub const VERTICAL_TAB: Self = Self(1 << 10);
63    /// Compose's `\\0` followed by up to three octal digits.
64    pub const OCTAL: Self = Self(1 << 11);
65    /// Portable's newline, carriage return, tab, slash, quote and dollar escapes.
66    pub const PORTABLE: Self = Self(0x3f);
67    /// Node's newline and carriage return substitutions.
68    pub const NODE: Self = Self(3);
69    /// Python dotenv's recognized double-quoted escapes.
70    pub const PYTHON: Self = Self(0x7df);
71    /// Compose's recognized escapes, including its octal-prefix behavior.
72    pub const COMPOSE: Self = Self(0xfbf);
73    /// Enable every sequence enabled by either set.
74    #[must_use]
75    pub const fn union(self, other: Self) -> Self {
76        Self(self.0 | other.0)
77    }
78    /// Whether every sequence in `other` is enabled.
79    #[must_use]
80    pub const fn contains(self, other: Self) -> bool {
81        self.0 & other.0 == other.0
82    }
83}
84
85/// Handling of unrecognized double-quoted escapes.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87#[non_exhaustive]
88pub enum UnknownEscapes {
89    /// Keep both characters literally.
90    Preserve,
91    /// Keep both characters and issue a warning.
92    Warn,
93    /// Remove the backslash only.
94    DropBackslash,
95    /// Retain the binding but report an error.
96    Error,
97}
98/// Escapes recognized inside single quotes.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100#[non_exhaustive]
101pub enum SingleEscapes {
102    /// No single-quoted escapes.
103    None,
104    /// Decode escaped apostrophes.
105    Quote,
106    /// Decode escaped apostrophes and backslashes.
107    QuoteAndBackslash,
108}
109/// Colon assignment separators.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
111#[non_exhaustive]
112pub enum ColonSeparator {
113    /// Only equals signs separate keys and values.
114    None,
115    /// A colon separates only when followed by configured whitespace.
116    Whitespace,
117    /// Any colon separates.
118    Any,
119}
120/// Validation of unquoted key bodies.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122#[non_exhaustive]
123pub enum KeySyntax {
124    /// ASCII letters/underscore followed by letters, digits or underscore.
125    Posix,
126    /// Nonempty ASCII letters, digits, underscore, dot or hyphen.
127    Portable,
128    /// Unicode 17 letters/numbers, underscore, dot or hyphen.
129    Unicode,
130    /// Nonempty, excluding Unicode whitespace and equals signs.
131    NonWhitespace,
132    /// Nonempty, excluding Python whitespace, equals signs and hashes.
133    Python,
134    /// Compose's Unicode letters/numbers, punctuation and horizontal whitespace.
135    Compose,
136    /// Nonempty, excluding literal ASCII space, tab and equals signs.
137    Docker,
138}
139/// Effective treatment of declarations with no assignment separator.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141#[non_exhaustive]
142pub enum BareKeys {
143    /// Retain a participating declaration whose value is absent.
144    Preserve,
145    /// Retain syntax but exclude it from lookup.
146    Ignore,
147    /// Look up the declaration as an empty string without changing its syntax.
148    Empty,
149    /// Retain syntax and report an error.
150    Error,
151}
152/// Physical line recognition.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
154#[non_exhaustive]
155pub enum LineBreaks {
156    /// LF, CRLF and lone CR terminate lines.
157    Universal,
158    /// LF terminates lines; one final CR belongs to the line ending.
159    Lf,
160}
161/// Treatment of an initial UTF-8 byte-order mark.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
163#[non_exhaustive]
164pub enum BomPolicy {
165    /// Retain it as a separate prelude and diagnose it informationally.
166    Strip,
167    /// Treat it as ordinary statement text.
168    Keep,
169    /// Retain it as an invalid item and report an error.
170    Error,
171}
172/// Which participating duplicate declaration wins lookup.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174#[non_exhaustive]
175pub enum Duplicates {
176    /// The first declaration wins.
177    First,
178    /// The last declaration wins.
179    Last,
180}
181/// Handling of noncomment text following a closing quote.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183#[non_exhaustive]
184pub enum QuotedTail {
185    /// Reject the statement.
186    Error,
187    /// Keep the tail token but omit it from the decoded value.
188    Ignore,
189    /// Append the literal tail to the decoded quoted body.
190    Append,
191    /// Reinterpret the opening physical line as an unquoted value.
192    LiteralFallback,
193    /// Ignore the complete statement with a warning.
194    Skip,
195    /// End the item at the quote and parse the remainder as another statement.
196    NextStatement,
197}
198/// Independently enabled multiline quote forms.
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
200#[non_exhaustive]
201pub struct Multiline {
202    /// Permit line endings inside single quotes.
203    pub single: bool,
204    /// Permit line endings inside double quotes.
205    pub double: bool,
206    /// Permit line endings inside backticks.
207    pub backtick: bool,
208}
209impl Multiline {
210    /// Permit or forbid line endings inside single quotes.
211    #[must_use]
212    pub const fn with_single(mut self, value: bool) -> Self {
213        self.single = value;
214        self
215    }
216    /// Permit or forbid line endings inside double quotes.
217    #[must_use]
218    pub const fn with_double(mut self, value: bool) -> Self {
219        self.double = value;
220        self
221    }
222    /// Permit or forbid line endings inside backticks.
223    #[must_use]
224    pub const fn with_backtick(mut self, value: bool) -> Self {
225        self.backtick = value;
226        self
227    }
228}
229impl Multiline {
230    /// Permit multiline content in every quote form.
231    pub const ALL: Self = Self {
232        single: true,
233        double: true,
234        backtick: true,
235    };
236    /// Forbid multiline content in every quote form.
237    pub const NONE: Self = Self {
238        single: false,
239        double: false,
240        backtick: false,
241    };
242}
243/// Whitespace classification used by syntax policies.
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
245#[non_exhaustive]
246pub enum Whitespace {
247    /// ASCII space and tab.
248    Ascii,
249    /// Rust's Unicode `White_Space` classification.
250    Unicode,
251    /// ECMAScript whitespace, including BOM but excluding NEL.
252    EcmaScript,
253    /// Unicode `White_Space` plus U+001C through U+001F.
254    Python,
255    /// Compose's horizontal whitespace classification.
256    Compose,
257}
258/// Handling of malformed statements, excluding explicit policy violations.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
260#[non_exhaustive]
261pub enum MalformedLines {
262    /// Retain invalid syntax and report an error.
263    Error,
264    /// Retain ignored syntax and report a warning.
265    Ignore,
266}
267
268/// Complete, explicit literal parsing and lookup configuration.
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
270// These orthogonal syntax switches are independently overrideable, not states.
271#[allow(clippy::struct_excessive_bools)]
272#[non_exhaustive]
273pub struct Dialect {
274    /// Statement grammar.
275    pub syntax: Syntax,
276    /// Unquoted inline comment recognition.
277    pub inline_comments: InlineComments,
278    /// Recognized double-quoted escapes.
279    pub double_escapes: DoubleEscapes,
280    /// Unrecognized double-quoted escape policy.
281    pub unknown_escapes: UnknownEscapes,
282    /// Single-quoted escape policy.
283    pub single_escapes: SingleEscapes,
284    /// Recognize backtick-quoted values.
285    pub backticks: bool,
286    /// Colon assignment separator policy.
287    pub colon: ColonSeparator,
288    /// Unquoted key validation.
289    pub keys: KeySyntax,
290    /// Bare declaration lookup/error policy.
291    pub bare_keys: BareKeys,
292    /// Keep all bytes after the separator as a literal value.
293    pub raw_values: bool,
294    /// Physical line recognition.
295    pub line_breaks: LineBreaks,
296    /// Leading byte-order mark policy.
297    pub bom: BomPolicy,
298    /// Winning duplicate declaration.
299    pub duplicates: Duplicates,
300    /// Noncomment text following a quote.
301    pub quoted_tail: QuotedTail,
302    /// Multiline quote permissions.
303    pub multiline: Multiline,
304    /// Whitespace classification.
305    pub whitespace: Whitespace,
306    /// Recognize the export prefix.
307    pub export: bool,
308    /// Permit single-quoted key tokens.
309    pub quoted_keys: bool,
310    /// Permit comments immediately after closing quotes.
311    pub quoted_comments: bool,
312    /// Normalize decoded CRLF and CR to LF, preserving original source maps.
313    pub normalize_newlines: bool,
314    /// Malformed statement policy.
315    pub malformed_lines: MalformedLines,
316    /// Exclude Node's special `__proto__` property from lookup.
317    pub node_object_keys: bool,
318    /// Maximum bytes per physical line before its final LF, including CR.
319    pub line_byte_limit: Option<usize>,
320}
321impl Dialect {
322    /// Conservative Portable syntax; the default configuration.
323    pub const PORTABLE: Self = Self {
324        syntax: Syntax::Portable,
325        inline_comments: InlineComments::Whitespace,
326        double_escapes: DoubleEscapes::PORTABLE,
327        unknown_escapes: UnknownEscapes::Warn,
328        single_escapes: SingleEscapes::None,
329        backticks: false,
330        colon: ColonSeparator::None,
331        keys: KeySyntax::Portable,
332        bare_keys: BareKeys::Preserve,
333        raw_values: false,
334        line_breaks: LineBreaks::Universal,
335        bom: BomPolicy::Strip,
336        duplicates: Duplicates::Last,
337        quoted_tail: QuotedTail::Error,
338        multiline: Multiline::ALL,
339        whitespace: Whitespace::Ascii,
340        export: true,
341        quoted_keys: false,
342        quoted_comments: false,
343        normalize_newlines: false,
344        malformed_lines: MalformedLines::Error,
345        node_object_keys: false,
346        line_byte_limit: None,
347    };
348    /// Node dotenv 18's default grammar and literal substitutions.
349    pub const NODE_DOTENV: Self = Self {
350        syntax: Syntax::Node,
351        whitespace: Whitespace::EcmaScript,
352        inline_comments: InlineComments::AnyHash,
353        double_escapes: DoubleEscapes::NODE,
354        unknown_escapes: UnknownEscapes::Preserve,
355        backticks: true,
356        colon: ColonSeparator::Whitespace,
357        bare_keys: BareKeys::Ignore,
358        quoted_tail: QuotedTail::LiteralFallback,
359        quoted_comments: true,
360        normalize_newlines: true,
361        malformed_lines: MalformedLines::Ignore,
362        node_object_keys: true,
363        ..Self::PORTABLE
364    };
365    /// Python dotenv 1.2's stream grammar, without interpolation.
366    pub const PYTHON_DOTENV: Self = Self {
367        syntax: Syntax::Python,
368        whitespace: Whitespace::Python,
369        inline_comments: InlineComments::Python,
370        double_escapes: DoubleEscapes::PYTHON,
371        unknown_escapes: UnknownEscapes::Preserve,
372        single_escapes: SingleEscapes::QuoteAndBackslash,
373        keys: KeySyntax::Python,
374        quoted_keys: true,
375        quoted_comments: true,
376        ..Self::PORTABLE
377    };
378    /// Compose dotenv's grammar and escapes, without interpolation or inheritance.
379    pub const COMPOSE: Self = Self {
380        syntax: Syntax::Compose,
381        whitespace: Whitespace::Compose,
382        inline_comments: InlineComments::Space,
383        double_escapes: DoubleEscapes::COMPOSE,
384        unknown_escapes: UnknownEscapes::Preserve,
385        single_escapes: SingleEscapes::Quote,
386        colon: ColonSeparator::Any,
387        keys: KeySyntax::Compose,
388        bare_keys: BareKeys::Ignore,
389        line_breaks: LineBreaks::Lf,
390        quoted_tail: QuotedTail::NextStatement,
391        quoted_comments: true,
392        ..Self::PORTABLE
393    };
394    /// Docker CLI's raw kvfile grammar, with no environment inheritance.
395    pub const DOCKER_RAW: Self = Self {
396        syntax: Syntax::Docker,
397        whitespace: Whitespace::Unicode,
398        inline_comments: InlineComments::None,
399        double_escapes: DoubleEscapes::NONE,
400        unknown_escapes: UnknownEscapes::Preserve,
401        keys: KeySyntax::Docker,
402        bare_keys: BareKeys::Ignore,
403        raw_values: true,
404        line_breaks: LineBreaks::Lf,
405        multiline: Multiline::NONE,
406        export: false,
407        line_byte_limit: Some(65_535),
408        ..Self::PORTABLE
409    };
410}
411impl Dialect {
412    /// Use the given statement grammar.
413    #[must_use]
414    pub const fn with_syntax(mut self, syntax: Syntax) -> Self {
415        self.syntax = syntax;
416        self
417    }
418    /// Use the given unquoted inline-comment policy.
419    #[must_use]
420    pub const fn with_inline_comments(mut self, value: InlineComments) -> Self {
421        self.inline_comments = value;
422        self
423    }
424    /// Use the given double-quoted escape set.
425    #[must_use]
426    pub const fn with_double_escapes(mut self, value: DoubleEscapes) -> Self {
427        self.double_escapes = value;
428        self
429    }
430    /// Use the given unknown-escape policy.
431    #[must_use]
432    pub const fn with_unknown_escapes(mut self, value: UnknownEscapes) -> Self {
433        self.unknown_escapes = value;
434        self
435    }
436    /// Use the given single-quoted escape policy.
437    #[must_use]
438    pub const fn with_single_escapes(mut self, value: SingleEscapes) -> Self {
439        self.single_escapes = value;
440        self
441    }
442    /// Enable or disable backtick-quoted values.
443    #[must_use]
444    pub const fn with_backticks(mut self, value: bool) -> Self {
445        self.backticks = value;
446        self
447    }
448    /// Use the given colon-separator policy.
449    #[must_use]
450    pub const fn with_colon(mut self, value: ColonSeparator) -> Self {
451        self.colon = value;
452        self
453    }
454    /// Use the given unquoted key syntax.
455    #[must_use]
456    pub const fn with_keys(mut self, value: KeySyntax) -> Self {
457        self.keys = value;
458        self
459    }
460    /// Use the given bare-key policy.
461    #[must_use]
462    pub const fn with_bare_keys(mut self, value: BareKeys) -> Self {
463        self.bare_keys = value;
464        self
465    }
466    /// Enable or disable raw value retention.
467    #[must_use]
468    pub const fn with_raw_values(mut self, value: bool) -> Self {
469        self.raw_values = value;
470        self
471    }
472    /// Use the given physical line-break policy.
473    #[must_use]
474    pub const fn with_line_breaks(mut self, value: LineBreaks) -> Self {
475        self.line_breaks = value;
476        self
477    }
478    /// Use the given byte-order-mark policy.
479    #[must_use]
480    pub const fn with_bom(mut self, value: BomPolicy) -> Self {
481        self.bom = value;
482        self
483    }
484    /// Use the given duplicate lookup policy.
485    #[must_use]
486    pub const fn with_duplicates(mut self, value: Duplicates) -> Self {
487        self.duplicates = value;
488        self
489    }
490    /// Use the given quoted-tail policy.
491    #[must_use]
492    pub const fn with_quoted_tail(mut self, value: QuotedTail) -> Self {
493        self.quoted_tail = value;
494        self
495    }
496    /// Use the given multiline quote permissions.
497    #[must_use]
498    pub const fn with_multiline(mut self, value: Multiline) -> Self {
499        self.multiline = value;
500        self
501    }
502    /// Use the given whitespace classification.
503    #[must_use]
504    pub const fn with_whitespace(mut self, value: Whitespace) -> Self {
505        self.whitespace = value;
506        self
507    }
508    /// Enable or disable the export prefix.
509    #[must_use]
510    pub const fn with_export(mut self, value: bool) -> Self {
511        self.export = value;
512        self
513    }
514    /// Enable or disable quoted key tokens.
515    #[must_use]
516    pub const fn with_quoted_keys(mut self, value: bool) -> Self {
517        self.quoted_keys = value;
518        self
519    }
520    /// Enable or disable comments after closing quotes.
521    #[must_use]
522    pub const fn with_quoted_comments(mut self, value: bool) -> Self {
523        self.quoted_comments = value;
524        self
525    }
526    /// Enable or disable decoded newline normalization.
527    #[must_use]
528    pub const fn with_normalize_newlines(mut self, value: bool) -> Self {
529        self.normalize_newlines = value;
530        self
531    }
532    /// Use the given malformed-line policy.
533    #[must_use]
534    pub const fn with_malformed_lines(mut self, value: MalformedLines) -> Self {
535        self.malformed_lines = value;
536        self
537    }
538    /// Enable or disable Node object-key exclusions.
539    #[must_use]
540    pub const fn with_node_object_keys(mut self, value: bool) -> Self {
541        self.node_object_keys = value;
542        self
543    }
544    /// Set the optional per-line byte limit.
545    #[must_use]
546    pub const fn with_line_byte_limit(mut self, value: Option<usize>) -> Self {
547        self.line_byte_limit = value;
548        self
549    }
550}
551impl Default for Dialect {
552    fn default() -> Self {
553        Self::PORTABLE
554    }
555}