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}