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)]
5pub enum Syntax {
6    /// The crate's conservative, lossless default grammar.
7    Portable,
8    /// Node dotenv's default regular-expression grammar.
9    Node,
10    /// Python dotenv's stream grammar.
11    Python,
12    /// Compose's dotenv grammar, without interpolation.
13    Compose,
14    /// Docker's raw key/value-file grammar.
15    Docker,
16}
17
18/// Recognition of inline comments (whole-statement comments are always enabled).
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum InlineComments {
21    /// A hash preceded by configured whitespace.
22    Whitespace,
23    /// A hash preceded by a literal ASCII space within the value/header tail.
24    Space,
25    /// Any hash starts a comment.
26    AnyHash,
27    /// Whitespace-delimited values, but any hash after a bare key (Python).
28    Python,
29    /// Hashes in values are literal.
30    None,
31}
32
33/// Independently enabled double-quoted escape sequences.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct DoubleEscapes(u16);
36impl DoubleEscapes {
37    /// No recognized escapes.
38    pub const NONE: Self = Self(0);
39    /// `\\n`.
40    pub const LF: Self = Self(1);
41    /// `\\r`.
42    pub const CR: Self = Self(1 << 1);
43    /// `\\t`.
44    pub const TAB: Self = Self(1 << 2);
45    /// `\\\\`.
46    pub const BACKSLASH: Self = Self(1 << 3);
47    /// `\\"`.
48    pub const DOUBLE_QUOTE: Self = Self(1 << 4);
49    /// `\\$` (literal dollar, not interpolation).
50    pub const DOLLAR: Self = Self(1 << 5);
51    /// `\\'`.
52    pub const SINGLE_QUOTE: Self = Self(1 << 6);
53    /// `\\a`.
54    pub const BELL: Self = Self(1 << 7);
55    /// `\\b`.
56    pub const BACKSPACE: Self = Self(1 << 8);
57    /// `\\f`.
58    pub const FORM_FEED: Self = Self(1 << 9);
59    /// `\\v`.
60    pub const VERTICAL_TAB: Self = Self(1 << 10);
61    /// Compose's `\\0` followed by up to three octal digits.
62    pub const OCTAL: Self = Self(1 << 11);
63    /// Portable's newline, carriage return, tab, slash, quote and dollar escapes.
64    pub const PORTABLE: Self = Self(0x3f);
65    /// Node's newline and carriage return substitutions.
66    pub const NODE: Self = Self(3);
67    /// Python dotenv's recognized double-quoted escapes.
68    pub const PYTHON: Self = Self(0x7df);
69    /// Compose's recognized escapes, including its octal-prefix behavior.
70    pub const COMPOSE: Self = Self(0xfbf);
71    /// Enable every sequence enabled by either set.
72    #[must_use]
73    pub const fn union(self, other: Self) -> Self {
74        Self(self.0 | other.0)
75    }
76    /// Whether every sequence in `other` is enabled.
77    #[must_use]
78    pub const fn contains(self, other: Self) -> bool {
79        self.0 & other.0 == other.0
80    }
81}
82
83/// Handling of unrecognized double-quoted escapes.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub enum UnknownEscapes {
86    /// Keep both characters literally.
87    Preserve,
88    /// Keep both characters and issue a warning.
89    Warn,
90    /// Remove the backslash only.
91    DropBackslash,
92    /// Retain the binding but report an error.
93    Error,
94}
95/// Escapes recognized inside single quotes.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97pub enum SingleEscapes {
98    /// No single-quoted escapes.
99    None,
100    /// Decode escaped apostrophes.
101    Quote,
102    /// Decode escaped apostrophes and backslashes.
103    QuoteAndBackslash,
104}
105/// Colon assignment separators.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum ColonSeparator {
108    /// Only equals signs separate keys and values.
109    None,
110    /// A colon separates only when followed by configured whitespace.
111    Whitespace,
112    /// Any colon separates.
113    Any,
114}
115/// Validation of unquoted key bodies.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117pub enum KeySyntax {
118    /// ASCII letters/underscore followed by letters, digits or underscore.
119    Posix,
120    /// Nonempty ASCII letters, digits, underscore, dot or hyphen.
121    Portable,
122    /// Unicode 17 letters/numbers, underscore, dot or hyphen.
123    Unicode,
124    /// Nonempty, excluding Unicode whitespace and equals signs.
125    NonWhitespace,
126    /// Nonempty, excluding Python whitespace, equals signs and hashes.
127    Python,
128    /// Compose's Unicode letters/numbers, punctuation and horizontal whitespace.
129    Compose,
130    /// Nonempty, excluding literal ASCII space, tab and equals signs.
131    Docker,
132}
133/// Effective treatment of declarations with no assignment separator.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
135pub enum BareKeys {
136    /// Retain a participating declaration whose value is absent.
137    Preserve,
138    /// Retain syntax but exclude it from lookup.
139    Ignore,
140    /// Look up the declaration as an empty string without changing its syntax.
141    Empty,
142    /// Retain syntax and report an error.
143    Error,
144}
145/// Physical line recognition.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
147pub enum LineBreaks {
148    /// LF, CRLF and lone CR terminate lines.
149    Universal,
150    /// LF terminates lines; one final CR belongs to the line ending.
151    Lf,
152}
153/// Treatment of an initial UTF-8 byte-order mark.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
155pub enum BomPolicy {
156    /// Retain it as a separate prelude and diagnose it informationally.
157    Strip,
158    /// Treat it as ordinary statement text.
159    Keep,
160    /// Retain it as an invalid item and report an error.
161    Error,
162}
163/// Which participating duplicate declaration wins lookup.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165pub enum Duplicates {
166    /// The first declaration wins.
167    First,
168    /// The last declaration wins.
169    Last,
170}
171/// Handling of noncomment text following a closing quote.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub enum QuotedTail {
174    /// Reject the statement.
175    Error,
176    /// Keep the tail token but omit it from the decoded value.
177    Ignore,
178    /// Append the literal tail to the decoded quoted body.
179    Append,
180    /// Reinterpret the opening physical line as an unquoted value.
181    LiteralFallback,
182    /// Ignore the complete statement with a warning.
183    Skip,
184    /// End the item at the quote and parse the remainder as another statement.
185    NextStatement,
186}
187/// Independently enabled multiline quote forms.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189pub struct Multiline {
190    /// Permit line endings inside single quotes.
191    pub single: bool,
192    /// Permit line endings inside double quotes.
193    pub double: bool,
194    /// Permit line endings inside backticks.
195    pub backtick: bool,
196}
197impl Multiline {
198    /// Permit multiline content in every quote form.
199    pub const ALL: Self = Self {
200        single: true,
201        double: true,
202        backtick: true,
203    };
204    /// Forbid multiline content in every quote form.
205    pub const NONE: Self = Self {
206        single: false,
207        double: false,
208        backtick: false,
209    };
210}
211/// Whitespace classification used by syntax policies.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
213pub enum Whitespace {
214    /// ASCII space and tab.
215    Ascii,
216    /// Rust's Unicode `White_Space` classification.
217    Unicode,
218    /// ECMAScript whitespace, including BOM but excluding NEL.
219    EcmaScript,
220    /// Unicode `White_Space` plus U+001C through U+001F.
221    Python,
222    /// Compose's horizontal whitespace classification.
223    Compose,
224}
225/// Handling of malformed statements, excluding explicit policy violations.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
227pub enum MalformedLines {
228    /// Retain invalid syntax and report an error.
229    Error,
230    /// Retain ignored syntax and report a warning.
231    Ignore,
232}
233
234/// Complete, explicit literal parsing and lookup configuration.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
236// These orthogonal syntax switches are independently overrideable, not states.
237#[allow(clippy::struct_excessive_bools)]
238pub struct Dialect {
239    /// Statement grammar.
240    pub syntax: Syntax,
241    /// Unquoted inline comment recognition.
242    pub inline_comments: InlineComments,
243    /// Recognized double-quoted escapes.
244    pub double_escapes: DoubleEscapes,
245    /// Unrecognized double-quoted escape policy.
246    pub unknown_escapes: UnknownEscapes,
247    /// Single-quoted escape policy.
248    pub single_escapes: SingleEscapes,
249    /// Recognize backtick-quoted values.
250    pub backticks: bool,
251    /// Colon assignment separator policy.
252    pub colon: ColonSeparator,
253    /// Unquoted key validation.
254    pub keys: KeySyntax,
255    /// Bare declaration lookup/error policy.
256    pub bare_keys: BareKeys,
257    /// Keep all bytes after the separator as a literal value.
258    pub raw_values: bool,
259    /// Physical line recognition.
260    pub line_breaks: LineBreaks,
261    /// Leading byte-order mark policy.
262    pub bom: BomPolicy,
263    /// Winning duplicate declaration.
264    pub duplicates: Duplicates,
265    /// Noncomment text following a quote.
266    pub quoted_tail: QuotedTail,
267    /// Multiline quote permissions.
268    pub multiline: Multiline,
269    /// Whitespace classification.
270    pub whitespace: Whitespace,
271    /// Recognize the export prefix.
272    pub export: bool,
273    /// Permit single-quoted key tokens.
274    pub quoted_keys: bool,
275    /// Permit comments immediately after closing quotes.
276    pub quoted_comments: bool,
277    /// Normalize decoded CRLF and CR to LF, preserving original source maps.
278    pub normalize_newlines: bool,
279    /// Malformed statement policy.
280    pub malformed_lines: MalformedLines,
281    /// Exclude Node's special `__proto__` property from lookup.
282    pub node_object_keys: bool,
283    /// Maximum bytes per physical line before its final LF, including CR.
284    pub line_byte_limit: Option<usize>,
285}
286impl Dialect {
287    /// Conservative Portable syntax; the default configuration.
288    pub const PORTABLE: Self = Self {
289        syntax: Syntax::Portable,
290        inline_comments: InlineComments::Whitespace,
291        double_escapes: DoubleEscapes::PORTABLE,
292        unknown_escapes: UnknownEscapes::Warn,
293        single_escapes: SingleEscapes::None,
294        backticks: false,
295        colon: ColonSeparator::None,
296        keys: KeySyntax::Portable,
297        bare_keys: BareKeys::Preserve,
298        raw_values: false,
299        line_breaks: LineBreaks::Universal,
300        bom: BomPolicy::Strip,
301        duplicates: Duplicates::Last,
302        quoted_tail: QuotedTail::Error,
303        multiline: Multiline::ALL,
304        whitespace: Whitespace::Ascii,
305        export: true,
306        quoted_keys: false,
307        quoted_comments: false,
308        normalize_newlines: false,
309        malformed_lines: MalformedLines::Error,
310        node_object_keys: false,
311        line_byte_limit: None,
312    };
313    /// Node dotenv 18's default grammar and literal substitutions.
314    pub const NODE_DOTENV: Self = Self {
315        syntax: Syntax::Node,
316        whitespace: Whitespace::EcmaScript,
317        inline_comments: InlineComments::AnyHash,
318        double_escapes: DoubleEscapes::NODE,
319        unknown_escapes: UnknownEscapes::Preserve,
320        backticks: true,
321        colon: ColonSeparator::Whitespace,
322        bare_keys: BareKeys::Ignore,
323        quoted_tail: QuotedTail::LiteralFallback,
324        quoted_comments: true,
325        normalize_newlines: true,
326        malformed_lines: MalformedLines::Ignore,
327        node_object_keys: true,
328        ..Self::PORTABLE
329    };
330    /// Python dotenv 1.2's stream grammar, without interpolation.
331    pub const PYTHON_DOTENV: Self = Self {
332        syntax: Syntax::Python,
333        whitespace: Whitespace::Python,
334        inline_comments: InlineComments::Python,
335        double_escapes: DoubleEscapes::PYTHON,
336        unknown_escapes: UnknownEscapes::Preserve,
337        single_escapes: SingleEscapes::QuoteAndBackslash,
338        keys: KeySyntax::Python,
339        quoted_keys: true,
340        quoted_comments: true,
341        ..Self::PORTABLE
342    };
343    /// Compose dotenv's grammar and escapes, without interpolation or inheritance.
344    pub const COMPOSE: Self = Self {
345        syntax: Syntax::Compose,
346        whitespace: Whitespace::Compose,
347        inline_comments: InlineComments::Space,
348        double_escapes: DoubleEscapes::COMPOSE,
349        unknown_escapes: UnknownEscapes::Preserve,
350        single_escapes: SingleEscapes::Quote,
351        colon: ColonSeparator::Any,
352        keys: KeySyntax::Compose,
353        bare_keys: BareKeys::Ignore,
354        line_breaks: LineBreaks::Lf,
355        quoted_tail: QuotedTail::NextStatement,
356        quoted_comments: true,
357        ..Self::PORTABLE
358    };
359    /// Docker CLI's raw kvfile grammar, with no environment inheritance.
360    pub const DOCKER_RAW: Self = Self {
361        syntax: Syntax::Docker,
362        whitespace: Whitespace::Unicode,
363        inline_comments: InlineComments::None,
364        double_escapes: DoubleEscapes::NONE,
365        unknown_escapes: UnknownEscapes::Preserve,
366        keys: KeySyntax::Docker,
367        bare_keys: BareKeys::Ignore,
368        raw_values: true,
369        line_breaks: LineBreaks::Lf,
370        multiline: Multiline::NONE,
371        export: false,
372        line_byte_limit: Some(65_535),
373        ..Self::PORTABLE
374    };
375}
376impl Default for Dialect {
377    fn default() -> Self {
378        Self::PORTABLE
379    }
380}