Skip to main content

deser_ini/
de.rs

1use std::borrow::Cow;
2
3use deser_core::Text;
4use deser_core::de::{
5    self, Deserialize, DeserializeDriver, DuplicateKeys, LexicalRules, deserialize_value,
6};
7use deser_core::{Atom, ContainerShape, Error, ErrorKind, Event, Source, TrackLocations};
8
9use crate::parser::{self, Document, NodeKind, Range};
10use crate::{Continuation, InlineComments, Quotes, Syntax};
11
12/// Configures how INI files are deserialized.
13///
14/// The default ([`new`](Self::new)) reads the INI files that are common
15/// today: `;` and `#` comments, `=` and `:` delimiters, comments after
16/// values (` ; comment`), values continued on indented lines, quoted values
17/// and keys without values.  The presets [`python`](Self::python) and
18/// [`git`](Self::git) read the dialects of Python's `configparser` and of
19/// git.  The configuration is independent of the input so it can be created
20/// once (even as a constant) and used for many inputs.
21///
22/// ```
23/// use std::collections::BTreeMap;
24/// use deser_ini::{DeserializerConfig, Quotes};
25///
26/// const CONFIG: DeserializerConfig =
27///     DeserializerConfig::builder().quotes(Quotes::None).build();
28/// let value: BTreeMap<String, BTreeMap<String, String>> =
29///     CONFIG.from_str("[a]\nb = \"c\"").unwrap();
30/// assert_eq!(value["a"]["b"], "\"c\"");
31/// ```
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct DeserializerConfig {
34    pub(crate) syntax: Syntax,
35    pub(crate) inline_comments: InlineComments,
36    pub(crate) colon_delimiter: bool,
37    pub(crate) continuation: Continuation,
38    pub(crate) quotes: Quotes,
39    pub(crate) allow_no_value: bool,
40    pub(crate) lowercase_names: bool,
41
42    context: deser_core::Context,
43}
44
45impl Default for DeserializerConfig {
46    fn default() -> DeserializerConfig {
47        DeserializerConfig::new()
48    }
49}
50
51impl DeserializerConfig {
52    /// Creates the default configuration (common INI files).
53    ///
54    /// * [`Syntax::Ini`]
55    /// * [`InlineComments::AfterWhitespace`]
56    /// * `=` and `:` are delimiters
57    /// * [`Continuation::Indented`]
58    /// * [`Quotes::Value`]
59    /// * keys without values are allowed
60    /// * names keep their case
61    pub const fn new() -> DeserializerConfig {
62        DeserializerConfig {
63            syntax: Syntax::Ini,
64            inline_comments: InlineComments::AfterWhitespace,
65            colon_delimiter: true,
66            continuation: Continuation::Indented,
67            quotes: Quotes::Value,
68            allow_no_value: true,
69            lowercase_names: false,
70
71            context: deser_core::Context::new(),
72        }
73    }
74
75    /// Creates the configuration for the files of Python's `configparser`.
76    ///
77    /// These are `setup.cfg`, `tox.ini`, `pytest.ini` and the like.  This is
78    /// the default but without inline comments and quotes, like
79    /// `RawConfigParser(strict=False, allow_no_value=True,
80    /// allow_unnamed_section=True, interpolation=None)` with keys that keep
81    /// their case.
82    ///
83    /// ```
84    /// use std::collections::BTreeMap;
85    /// use deser_ini::DeserializerConfig;
86    ///
87    /// let value: BTreeMap<String, BTreeMap<String, String>> =
88    ///     DeserializerConfig::python()
89    ///         .from_str("[tox]\nenvlist = py312 ; py313\n")
90    ///         .unwrap();
91    /// assert_eq!(value["tox"]["envlist"], "py312 ; py313");
92    /// ```
93    pub const fn python() -> DeserializerConfig {
94        DeserializerConfig::builder()
95            .inline_comments(InlineComments::None)
96            .quotes(Quotes::None)
97            .build()
98    }
99
100    /// Creates the configuration for git's config files.
101    ///
102    /// This is [`Syntax::Git`] which reads `.gitconfig`, `.git/config` and
103    /// `.gitmodules` like git does.
104    ///
105    /// ```
106    /// use std::collections::BTreeMap;
107    /// use deser_ini::DeserializerConfig;
108    ///
109    /// type Config = BTreeMap<String, BTreeMap<String, BTreeMap<String, String>>>;
110    /// let config: Config = DeserializerConfig::git()
111    ///     .from_str("[remote \"origin\"]\n\turl = https://example.com/x.git\n")
112    ///     .unwrap();
113    /// assert_eq!(config["remote"]["origin"]["url"], "https://example.com/x.git");
114    /// ```
115    pub const fn git() -> DeserializerConfig {
116        DeserializerConfig::builder().syntax(Syntax::Git).build()
117    }
118
119    /// Returns a builder for the configuration (see [`DeserializerConfigBuilder`]).
120    pub const fn builder() -> DeserializerConfigBuilder {
121        DeserializerConfigBuilder::new()
122    }
123
124    /// Returns a builder that starts with this configuration.
125    pub const fn into_builder(self) -> DeserializerConfigBuilder {
126        DeserializerConfigBuilder { value: self }
127    }
128
129    /// Sets the context the values are deserialized in.
130    ///
131    /// The values of the context are the defaults of the extension values
132    /// of the state (see [`Context`](deser_core::Context)), for instance
133    /// the variants of open enums.  The deserializers and readers created
134    /// with the configuration use this context.  A context set
135    /// on the driver takes precedence.
136    pub fn set_context(&mut self, context: deser_core::Context) {
137        self.context = context;
138    }
139
140    /// Returns the configuration without its context (for the frames of
141    /// streams, which get the context of the stream).
142    pub(crate) fn without_context(&self) -> DeserializerConfig {
143        let mut config = self.clone();
144        config.context = deser_core::Context::default();
145        config
146    }
147
148    /// Returns the context the values are deserialized in.
149    pub fn context(&self) -> &deser_core::Context {
150        &self.context
151    }
152
153    /// Sets the syntax.
154    ///
155    /// The default is [`Syntax::Ini`].  With [`Syntax::Git`] the other
156    /// options (except for the context) are ignored.
157    pub const fn set_syntax(&mut self, syntax: Syntax) {
158        self.syntax = syntax;
159    }
160
161    /// Returns the syntax.
162    pub const fn syntax(&self) -> Syntax {
163        self.syntax
164    }
165
166    /// Sets where comments start after values.
167    ///
168    /// The default is [`InlineComments::AfterWhitespace`].  Lines that
169    /// start with `;` or `#` are always comments.
170    pub const fn set_inline_comments(&mut self, comments: InlineComments) {
171        self.inline_comments = comments;
172    }
173
174    /// Returns where comments start after values.
175    pub const fn inline_comments(&self) -> InlineComments {
176        self.inline_comments
177    }
178
179    /// Sets if `:` separates keys and values (like `=`).
180    ///
181    /// The default is `true`, the first `=` or `:` of a line separates the
182    /// key and the value (`url = http://x` is the key `url`).
183    pub const fn set_colon_delimiter(&mut self, yes: bool) {
184        self.colon_delimiter = yes;
185    }
186
187    /// Returns if `:` separates keys and values.
188    pub const fn colon_delimiter(&self) -> bool {
189        self.colon_delimiter
190    }
191
192    /// Sets how values continue on the next lines.
193    ///
194    /// The default is [`Continuation::Indented`].
195    pub const fn set_continuation(&mut self, continuation: Continuation) {
196        self.continuation = continuation;
197    }
198
199    /// Returns how values continue on the next lines.
200    pub const fn continuation(&self) -> Continuation {
201        self.continuation
202    }
203
204    /// Sets how quoted values are read.
205    ///
206    /// The default is [`Quotes::Value`].
207    pub const fn set_quotes(&mut self, quotes: Quotes) {
208        self.quotes = quotes;
209    }
210
211    /// Returns how quoted values are read.
212    pub const fn quotes(&self) -> Quotes {
213        self.quotes
214    }
215
216    /// Sets if keys can be given without value.
217    ///
218    /// The default is `true`: a line with a key but no delimiter (like
219    /// `skip-name-resolve` in MySQL's configuration) is the key with a null
220    /// value.  Optionals are `None` and the
221    /// [`Flag`](deser_core::adapters::Flag) adapter is `true` for it.  If
222    /// `false`, such lines are an error.
223    pub const fn set_allow_no_value(&mut self, yes: bool) {
224        self.allow_no_value = yes;
225    }
226
227    /// Returns if keys can be given without value.
228    pub const fn allow_no_value(&self) -> bool {
229        self.allow_no_value
230    }
231
232    /// Sets if the names of sections and keys are lowercased.
233    ///
234    /// The default is `false`.  Only ASCII letters are lowercased.  This
235    /// makes names case insensitive, like they are for Windows and Python's
236    /// `configparser` (which lowercases keys).
237    pub const fn set_lowercase_names(&mut self, yes: bool) {
238        self.lowercase_names = yes;
239    }
240
241    /// Returns if the names of sections and keys are lowercased.
242    pub const fn lowercase_names(&self) -> bool {
243        self.lowercase_names
244    }
245
246    /// Deserializes a value from an INI file.
247    ///
248    /// See [`from_str`](crate::from_str).
249    pub fn from_str<'de, T: Deserialize<'de>>(&self, s: &'de str) -> Result<T, Error> {
250        deserialize_value(|driver| self.drive_str(s, driver))
251    }
252
253    /// The part of [`from_str`](Self::from_str) that does not depend on the type
254    /// of the value, it exists once.
255    fn drive_str<'de>(
256        &self,
257        s: &'de str,
258        driver: &mut DeserializeDriver<'_, 'de>,
259    ) -> Result<(), Error> {
260        de::Deserializer::drive(
261            &mut Deserializer::from_str_with_config(s, self.clone()),
262            driver,
263        )
264    }
265
266    /// Deserializes a value from an INI file in a byte slice.
267    ///
268    /// See [`from_slice`](crate::from_slice).
269    pub fn from_slice<'de, T: Deserialize<'de>>(&self, bytes: &'de [u8]) -> Result<T, Error> {
270        deserialize_value(|driver| self.drive_slice(bytes, driver))
271    }
272
273    /// The part of [`from_slice`](Self::from_slice) that does not depend on the type
274    /// of the value, it exists once.
275    fn drive_slice<'de>(
276        &self,
277        bytes: &'de [u8],
278        driver: &mut DeserializeDriver<'_, 'de>,
279    ) -> Result<(), Error> {
280        de::Deserializer::drive(
281            &mut Deserializer::from_slice_with_config(bytes, self.clone()),
282            driver,
283        )
284    }
285}
286
287/// Builds a [`DeserializerConfig`].
288///
289/// The methods have the names of the setters of [`DeserializerConfig`] (without `set_`).
290#[derive(Debug, Clone)]
291#[must_use]
292pub struct DeserializerConfigBuilder {
293    value: DeserializerConfig,
294}
295
296impl DeserializerConfigBuilder {
297    /// Creates a builder that starts with the default.
298    pub const fn new() -> DeserializerConfigBuilder {
299        DeserializerConfigBuilder {
300            value: DeserializerConfig::new(),
301        }
302    }
303
304    /// Sets the syntax.
305    ///
306    /// See [`DeserializerConfig::set_syntax`].
307    pub const fn syntax(mut self, syntax: Syntax) -> DeserializerConfigBuilder {
308        self.value.set_syntax(syntax);
309        self
310    }
311
312    /// Sets where comments start after values.
313    ///
314    /// See [`DeserializerConfig::set_inline_comments`].
315    pub const fn inline_comments(mut self, comments: InlineComments) -> DeserializerConfigBuilder {
316        self.value.set_inline_comments(comments);
317        self
318    }
319
320    /// Sets if `:` separates keys and values (like `=`).
321    ///
322    /// See [`DeserializerConfig::set_colon_delimiter`].
323    pub const fn colon_delimiter(mut self, yes: bool) -> DeserializerConfigBuilder {
324        self.value.set_colon_delimiter(yes);
325        self
326    }
327
328    /// Sets how values continue on the next lines.
329    ///
330    /// See [`DeserializerConfig::set_continuation`].
331    pub const fn continuation(mut self, continuation: Continuation) -> DeserializerConfigBuilder {
332        self.value.set_continuation(continuation);
333        self
334    }
335
336    /// Sets how quoted values are read.
337    ///
338    /// See [`DeserializerConfig::set_quotes`].
339    pub const fn quotes(mut self, quotes: Quotes) -> DeserializerConfigBuilder {
340        self.value.set_quotes(quotes);
341        self
342    }
343
344    /// Sets if keys can be given without value.
345    ///
346    /// See [`DeserializerConfig::set_allow_no_value`].
347    pub const fn allow_no_value(mut self, yes: bool) -> DeserializerConfigBuilder {
348        self.value.set_allow_no_value(yes);
349        self
350    }
351
352    /// Sets if the names of sections and keys are lowercased.
353    ///
354    /// See [`DeserializerConfig::set_lowercase_names`].
355    pub const fn lowercase_names(mut self, yes: bool) -> DeserializerConfigBuilder {
356        self.value.set_lowercase_names(yes);
357        self
358    }
359
360    /// Sets the context the values are deserialized in.
361    ///
362    /// See [`DeserializerConfig::set_context`].
363    pub fn context(mut self, context: deser_core::Context) -> DeserializerConfigBuilder {
364        self.value.set_context(context);
365        self
366    }
367
368    /// Returns the built [`DeserializerConfig`].
369    pub const fn build(self) -> DeserializerConfig {
370        // the value cannot be moved out of the builder in a const fn as the
371        // builder needs dropping (the context has a destructor)
372        // SAFETY: the value is read once and the builder is forgotten
373        let value = unsafe { core::ptr::read(&self.value) };
374        core::mem::forget(self);
375        value
376    }
377}
378
379impl Default for DeserializerConfigBuilder {
380    fn default() -> DeserializerConfigBuilder {
381        DeserializerConfigBuilder::new()
382    }
383}
384
385/// Deserializes INI files.
386///
387/// Most of the time the [`from_str`](crate::from_str) and
388/// [`from_slice`](crate::from_slice) functions (or the methods of the same
389/// name on [`DeserializerConfig`]) are all that is needed.  The deserializer
390/// is useful to configure the driver, for instance to add layers, or to
391/// update a value (see [`update`](deser_core::de::Deserializer::update)):
392///
393/// ```
394/// use deser_path::{Path, PathLayer};
395/// use deser_ini::Deserializer;
396///
397/// #[derive(Debug, deser::Deserialize)]
398/// struct Config {
399///     server: Server,
400/// }
401///
402/// #[derive(Debug, deser::Deserialize)]
403/// struct Server {
404///     port: u16,
405/// }
406///
407/// let err = Deserializer::from_str("[server]\nport = http\n")
408///     .deserialize_with::<Config, _>(|driver| {
409///         driver.push_layer(PathLayer::new())
410///     })
411///     .unwrap_err();
412/// assert_eq!(err.message(), "invalid value \"http\", expected u16");
413/// assert_eq!(err.attachment::<Path>().unwrap().to_string(), "server.port");
414/// assert_eq!(err.line(), Some(2));
415/// ```
416pub struct Deserializer<'a> {
417    input: &'a str,
418    /// An error that is reported instead of parsing (invalid UTF-8).
419    error: Option<Error>,
420    config: DeserializerConfig,
421}
422
423impl<'a> Deserializer<'a> {
424    /// Creates a new deserializer for a string.
425    #[allow(clippy::should_implement_trait)]
426    pub fn from_str(input: &'a str) -> Deserializer<'a> {
427        Deserializer::from_str_with_config(input, DeserializerConfig::new())
428    }
429
430    /// Creates a new deserializer for a string with the given configuration.
431    pub fn from_str_with_config(input: &'a str, config: DeserializerConfig) -> Deserializer<'a> {
432        Deserializer {
433            input,
434            error: None,
435            config,
436        }
437    }
438
439    /// Creates a new deserializer for a byte slice.
440    ///
441    /// The input must be UTF-8 (a byte order mark is skipped), otherwise
442    /// deserializing fails.
443    pub fn from_slice(input: &'a [u8]) -> Deserializer<'a> {
444        Deserializer::from_slice_with_config(input, DeserializerConfig::new())
445    }
446
447    /// Creates a new deserializer for a byte slice with the given
448    /// configuration.
449    pub fn from_slice_with_config(input: &'a [u8], config: DeserializerConfig) -> Deserializer<'a> {
450        match std::str::from_utf8(input) {
451            Ok(input) => Deserializer::from_str_with_config(input, config),
452            Err(err) => Deserializer {
453                input: "",
454                error: Some(Error::with_offset(
455                    ErrorKind::Syntax,
456                    "input is not valid UTF-8",
457                    err.valid_up_to(),
458                )),
459                config,
460            },
461        }
462    }
463
464    /// Returns the configuration.
465    pub fn config(&self) -> &DeserializerConfig {
466        &self.config
467    }
468
469    /// Deserializes the input.
470    ///
471    /// To configure the deserialization (for instance to add layers) use
472    /// [`deserialize_with`](Self::deserialize_with).
473    pub fn deserialize<T: Deserialize<'a>>(&mut self) -> Result<T, Error> {
474        de::Deserializer::deserialize(self)
475    }
476
477    /// Deserializes the input with a configured driver.
478    ///
479    /// The callback is invoked with the driver before the value is
480    /// deserialized, for instance to add [`Layer`](deser_core::de::Layer)s.
481    pub fn deserialize_with<T, F>(&mut self, setup: F) -> Result<T, Error>
482    where
483        T: Deserialize<'a>,
484        F: FnOnce(&mut DeserializeDriver<'_, 'a>),
485    {
486        de::Deserializer::deserialize_with(self, setup)
487    }
488
489    /// Parses the input and feeds the events into the given driver.
490    ///
491    /// The whole input is parsed before the first event is emitted, so
492    /// malformed input is reported before any value is deserialized.  Keys
493    /// and values that are not changed (by quotes, escapes, continuation
494    /// lines or lowercasing) are passed on borrowed from the input (see
495    /// [`emit_borrowed`](DeserializeDriver::emit_borrowed)).
496    ///
497    /// The context of the configuration is given to the driver (values that
498    /// the context of the driver has take precedence, see
499    /// [`DeserializeDriver::set_default_context`]).
500    pub fn drive(&mut self, driver: &mut DeserializeDriver<'_, 'a>) -> Result<(), Error> {
501        if !self.config.context.is_empty() {
502            driver.set_default_context(self.config.context.clone());
503        }
504        if let Some(err) = self.error.take() {
505            return Err(err);
506        }
507        let doc = parser::parse(self.input, &self.config).map_err(|mut err| {
508            err.resolve_position(self.input.as_bytes());
509            err
510        })?;
511        let state = driver.state_mut();
512        if TrackLocations::of(state) {
513            Source(self.input.into()).set(state);
514        }
515        // the last value of repeated keys is used and everything is text
516        // unless the context says otherwise
517        DuplicateKeys::Last.set_default(state);
518        LexicalRules::LENIENT.set_default(state);
519        emit(&doc, self.input.len(), driver).map_err(|mut err| {
520            err.resolve_position(self.input.as_bytes());
521            err
522        })
523    }
524}
525
526impl<'a> de::Deserializer<'a> for Deserializer<'a> {
527    fn drive(&mut self, driver: &mut DeserializeDriver<'_, 'a>) -> Result<(), Error> {
528        Deserializer::drive(self, driver)
529    }
530}
531
532/// Returns the shape of a table.
533///
534/// Tables are multimaps, the keys are given once per value.
535fn table_shape(doc: &Document<'_>, children: &[usize]) -> ContainerShape {
536    let len = children
537        .iter()
538        .map(|&child| doc.nodes[child].values.len().max(1))
539        .sum();
540    let mut shape = ContainerShape::with_len(len);
541    shape.set_multimap(true);
542    shape
543}
544
545/// Emits the events of the document.
546fn emit<'a>(
547    doc: &Document<'a>,
548    input_len: usize,
549    driver: &mut DeserializeDriver<'_, 'a>,
550) -> Result<(), Error> {
551    struct Frame {
552        node: usize,
553        pos: usize,
554    }
555
556    let root = &doc.nodes[0];
557    emit_at(
558        driver,
559        Event::MapStart(table_shape(doc, &root.children)),
560        root.range,
561    )?;
562    let mut stack = vec![Frame { node: 0, pos: 0 }];
563    while let Some(frame) = stack.last_mut() {
564        let table = &doc.nodes[frame.node];
565        let Some(&child) = table.children.get(frame.pos) else {
566            let range = if frame.node == 0 {
567                (input_len, input_len)
568            } else {
569                table.range
570            };
571            stack.pop();
572            emit_at(driver, Event::MapEnd, range)?;
573            continue;
574        };
575        frame.pos += 1;
576        let node = &doc.nodes[child];
577        match node.kind {
578            NodeKind::Key => {
579                // the key is emitted for every value (tables are multimaps)
580                for (value, range) in &node.values {
581                    emit_text(driver, &node.name, node.range)?;
582                    match *value {
583                        Some(ref value) => emit_text(driver, value, *range)?,
584                        None => emit_at(driver, Atom::Null, *range)?,
585                    }
586                }
587            }
588            NodeKind::Table => {
589                emit_text(driver, &node.name, node.range)?;
590                emit_at(
591                    driver,
592                    Event::MapStart(table_shape(doc, &node.children)),
593                    node.range,
594                )?;
595                stack.push(Frame {
596                    node: child,
597                    pos: 0,
598                });
599            }
600        }
601    }
602    Ok(())
603}
604
605/// Emits an event with a byte range.
606#[inline]
607fn emit_at<'e, E: Into<Event<'e>>>(
608    driver: &mut DeserializeDriver<'_, '_>,
609    event: E,
610    range: Range,
611) -> Result<(), Error> {
612    driver.state_mut().set_input_range(range.0, range.1);
613    driver.emit(event)
614}
615
616/// Emits text as lexical atom, borrowed if it's a slice of the input.
617// the text is a `Cow` as borrowed text is passed on for `'a`
618#[allow(clippy::ptr_arg)]
619#[inline]
620fn emit_text<'a>(
621    driver: &mut DeserializeDriver<'_, 'a>,
622    text: &Cow<'a, str>,
623    range: Range,
624) -> Result<(), Error> {
625    driver.state_mut().set_input_range(range.0, range.1);
626    match *text {
627        Cow::Borrowed(text) => driver.emit_borrowed(Atom::Lexical(Text::borrowed(text))),
628        Cow::Owned(ref text) => driver.emit(Atom::Lexical(Text::borrowed(text.as_str()))),
629    }
630}