Skip to main content

deser_ini/
ser.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3
4use deser_core::ext::Number;
5use deser_core::ser::{self, SerializeDriver, SerializeRef};
6use deser_core::{Atom, BytesFormat, Error, ErrorKind, Event, Serialize, State};
7
8use crate::parser::comment_start;
9use crate::{Continuation, InlineComments, Quotes, Syntax};
10
11/// Configures how values are serialized to INI files.
12///
13/// The value has to serialize to a map (for instance a struct or a map
14/// type).  Its entries with maps as values are written as sections, the
15/// others before the first section.  The values of sections cannot be maps
16/// (in git's syntax they can: they are written as subsections).  Sequences
17/// are written as repeated keys (`tags = a` `tags = b`), their elements
18/// cannot be maps or sequences.  Null values (like `None`) of map entries
19/// are skipped, null values in sequences are written as keys without
20/// value.  Empty maps are written as empty sections, empty sequences are
21/// not written.
22///
23/// The options describe the dialect the file is written for, they are the
24/// same as the ones of [`DeserializerConfig`](crate::DeserializerConfig)
25/// and the file reads back with the same options.  Values that the dialect
26/// cannot represent are an error, for instance values that start with
27/// whitespace without [`Quotes::Value`].  Values are quoted only if needed
28/// (whitespace at the start or end, comment characters), values with line
29/// breaks are written with continuation lines.
30///
31/// Numbers are written with the shortest text that reads back as the same
32/// value, booleans as `true` and `false` and bytes as base64 (or the
33/// [`BytesFormat`](deser_core::BytesFormat) of the context).
34///
35/// ```
36/// use std::collections::BTreeMap;
37/// use deser_ini::SerializerConfig;
38///
39/// let value = BTreeMap::from([
40///     ("tox", BTreeMap::from([("envlist", "py312, py313")])),
41///     ("testenv", BTreeMap::from([("commands", "pytest\nruff check")])),
42/// ]);
43/// assert_eq!(
44///     SerializerConfig::python().to_string(&value).unwrap(),
45///     "[testenv]\ncommands = pytest\n    ruff check\n\n[tox]\nenvlist = py312, py313\n"
46/// );
47/// ```
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SerializerConfig {
50    syntax: Syntax,
51    inline_comments: InlineComments,
52    colon_delimiter: bool,
53    continuation: Continuation,
54    quotes: Quotes,
55    context: deser_core::Context,
56}
57
58impl Default for SerializerConfig {
59    fn default() -> SerializerConfig {
60        SerializerConfig::new()
61    }
62}
63
64impl SerializerConfig {
65    /// Creates the default configuration (common INI files).
66    ///
67    /// The options are the ones of
68    /// [`DeserializerConfig::new`](crate::DeserializerConfig::new).
69    pub const fn new() -> SerializerConfig {
70        SerializerConfig {
71            syntax: Syntax::Ini,
72            inline_comments: InlineComments::AfterWhitespace,
73            colon_delimiter: true,
74            continuation: Continuation::Indented,
75            quotes: Quotes::Value,
76            context: deser_core::Context::new(),
77        }
78    }
79
80    /// Creates the configuration for the files of Python's `configparser`.
81    ///
82    /// See [`DeserializerConfig::python`](crate::DeserializerConfig::python).
83    pub const fn python() -> SerializerConfig {
84        SerializerConfig::builder()
85            .inline_comments(InlineComments::None)
86            .quotes(Quotes::None)
87            .build()
88    }
89
90    /// Creates the configuration for git's config files.
91    ///
92    /// See [`Syntax::Git`].
93    ///
94    /// ```
95    /// use std::collections::BTreeMap;
96    /// use deser_ini::SerializerConfig;
97    ///
98    /// let value = BTreeMap::from([(
99    ///     "remote",
100    ///     BTreeMap::from([("origin", BTreeMap::from([("url", "git@x:y.git")]))]),
101    /// )]);
102    /// assert_eq!(
103    ///     SerializerConfig::git().to_string(&value).unwrap(),
104    ///     "[remote \"origin\"]\n\turl = git@x:y.git\n"
105    /// );
106    /// ```
107    pub const fn git() -> SerializerConfig {
108        SerializerConfig::builder().syntax(Syntax::Git).build()
109    }
110
111    /// Returns a builder for the configuration (see [`SerializerConfigBuilder`]).
112    pub const fn builder() -> SerializerConfigBuilder {
113        SerializerConfigBuilder::new()
114    }
115
116    /// Returns a builder that starts with this configuration.
117    pub const fn into_builder(self) -> SerializerConfigBuilder {
118        SerializerConfigBuilder { value: self }
119    }
120
121    /// Sets the context the values are serialized in.
122    ///
123    /// The values of the context are the defaults of the extension values
124    /// of the state (see [`Context`](deser_core::Context)), for instance
125    /// the [`BytesFormat`](deser_core::BytesFormat).  The serializers and
126    /// writers created with the configuration use this context.  A context
127    /// set on the driver takes precedence.
128    pub fn set_context(&mut self, context: deser_core::Context) {
129        self.context = context;
130    }
131
132    /// Returns the context the values are serialized in.
133    pub fn context(&self) -> &deser_core::Context {
134        &self.context
135    }
136
137    /// Gives the context to a driver which has none.
138    #[inline]
139    fn apply_context(&self, driver: &mut SerializeDriver<'_>) {
140        if !self.context.is_empty() {
141            driver.set_default_context(self.context.clone());
142        }
143    }
144
145    /// Sets the syntax.
146    ///
147    /// The default is [`Syntax::Ini`].  With [`Syntax::Git`] the other
148    /// options (except for the context) are ignored.
149    pub const fn set_syntax(&mut self, syntax: Syntax) {
150        self.syntax = syntax;
151    }
152
153    /// Sets where comments start after values.
154    ///
155    /// The default is [`InlineComments::AfterWhitespace`], values that
156    /// would be read as comments are quoted.
157    pub const fn set_inline_comments(&mut self, comments: InlineComments) {
158        self.inline_comments = comments;
159    }
160
161    /// Sets if `:` separates keys and values (like `=`).
162    ///
163    /// The default is `true`, keys that contain `:` are an error.
164    pub const fn set_colon_delimiter(&mut self, yes: bool) {
165        self.colon_delimiter = yes;
166    }
167
168    /// Sets how values continue on the next lines.
169    ///
170    /// The default is [`Continuation::Indented`] which writes values with
171    /// line breaks with continuation lines.  Otherwise values with line
172    /// breaks are an error.
173    pub const fn set_continuation(&mut self, continuation: Continuation) {
174        self.continuation = continuation;
175    }
176
177    /// Sets if values can be quoted.
178    ///
179    /// The default is [`Quotes::Value`].  With [`Quotes::None`] values that
180    /// need quotes are an error.
181    pub const fn set_quotes(&mut self, quotes: Quotes) {
182        self.quotes = quotes;
183    }
184
185    /// Serializes the given value.
186    pub fn to_string<T: Serialize + ?Sized>(&self, value: &T) -> Result<String, Error> {
187        self.to_string_ref(SerializeRef::new(&value))
188    }
189
190    /// Serializes the given value with a configured driver.
191    ///
192    /// The callback is invoked with the driver before the serialization
193    /// starts, for instance to add [`Layer`](deser_core::ser::Layer)s.
194    pub fn to_string_with<F, T: Serialize + ?Sized>(
195        &self,
196        value: &T,
197        setup: F,
198    ) -> Result<String, Error>
199    where
200        F: FnOnce(&mut SerializeDriver<'_>),
201    {
202        let mut driver = SerializeDriver::new(&value);
203        setup(&mut driver);
204        self.apply_context(&mut driver);
205        self.serialize_driver(&mut driver)
206    }
207
208    /// Serializes a value whose type is erased (see
209    /// [`to_string`](Self::to_string)).
210    ///
211    /// This is not generic: the code that exists for every type only
212    /// erases it.
213    fn to_string_ref(&self, value: SerializeRef<'_>) -> Result<String, Error> {
214        let mut driver = SerializeDriver::from_ref(value);
215        self.apply_context(&mut driver);
216        self.serialize_driver(&mut driver)
217    }
218
219    /// Serializes the value of a driver.
220    pub(crate) fn serialize_driver(
221        &self,
222        driver: &mut SerializeDriver<'_>,
223    ) -> Result<String, Error> {
224        let mut writer = Writer {
225            config: self,
226            bytes: BytesFormat::of(driver.state()),
227            stack: Vec::new(),
228            out: String::new(),
229        };
230        driver.drive(|event, state| writer.event(event, state))?;
231        Ok(writer.out)
232    }
233}
234
235/// Builds a [`SerializerConfig`].
236///
237/// The methods have the names of the setters of [`SerializerConfig`] (without `set_`).
238#[derive(Debug, Clone)]
239#[must_use]
240pub struct SerializerConfigBuilder {
241    value: SerializerConfig,
242}
243
244impl SerializerConfigBuilder {
245    /// Creates a builder that starts with the default.
246    pub const fn new() -> SerializerConfigBuilder {
247        SerializerConfigBuilder {
248            value: SerializerConfig::new(),
249        }
250    }
251
252    /// Sets the syntax.
253    ///
254    /// See [`SerializerConfig::set_syntax`].
255    pub const fn syntax(mut self, syntax: Syntax) -> SerializerConfigBuilder {
256        self.value.set_syntax(syntax);
257        self
258    }
259
260    /// Sets where comments start after values.
261    ///
262    /// See [`SerializerConfig::set_inline_comments`].
263    pub const fn inline_comments(mut self, comments: InlineComments) -> SerializerConfigBuilder {
264        self.value.set_inline_comments(comments);
265        self
266    }
267
268    /// Sets if `:` separates keys and values (like `=`).
269    ///
270    /// See [`SerializerConfig::set_colon_delimiter`].
271    pub const fn colon_delimiter(mut self, yes: bool) -> SerializerConfigBuilder {
272        self.value.set_colon_delimiter(yes);
273        self
274    }
275
276    /// Sets how values continue on the next lines.
277    ///
278    /// See [`SerializerConfig::set_continuation`].
279    pub const fn continuation(mut self, continuation: Continuation) -> SerializerConfigBuilder {
280        self.value.set_continuation(continuation);
281        self
282    }
283
284    /// Sets if values can be quoted.
285    ///
286    /// See [`SerializerConfig::set_quotes`].
287    pub const fn quotes(mut self, quotes: Quotes) -> SerializerConfigBuilder {
288        self.value.set_quotes(quotes);
289        self
290    }
291
292    /// Sets the context the values are serialized in.
293    ///
294    /// See [`SerializerConfig::set_context`].
295    pub fn context(mut self, context: deser_core::Context) -> SerializerConfigBuilder {
296        self.value.set_context(context);
297        self
298    }
299
300    /// Returns the built [`SerializerConfig`].
301    pub const fn build(self) -> SerializerConfig {
302        // the value cannot be moved out of the builder in a const fn as the
303        // builder needs dropping (the context has a destructor)
304        // SAFETY: the value is read once and the builder is forgotten
305        let value = unsafe { core::ptr::read(&self.value) };
306        core::mem::forget(self);
307        value
308    }
309}
310
311impl Default for SerializerConfigBuilder {
312    fn default() -> SerializerConfigBuilder {
313        SerializerConfigBuilder::new()
314    }
315}
316
317/// Serializes values into INI files.
318///
319/// An INI file holds a single value, writing a second one fails.
320///
321/// ```
322/// use std::collections::BTreeMap;
323/// use deser_ini::Serializer;
324///
325/// let mut serializer = Serializer::new();
326/// serializer.serialize(&BTreeMap::from([("a", 1)])).unwrap();
327/// assert!(serializer.serialize(&BTreeMap::from([("b", 2)])).is_err());
328/// assert_eq!(serializer.finish(), "a = 1\n");
329/// ```
330///
331/// The serializer is also the stream serializer of INI files (see
332/// [`StreamSerializer`](ser::StreamSerializer)).  An INI file cannot be
333/// written in parts: the keys before the first section can come last in the
334/// value.  To write to a [`Write`](std::io::Write) use
335/// [`SerializerConfig::writer`].
336#[derive(Debug, Clone)]
337pub struct Serializer {
338    config: SerializerConfig,
339    out: String,
340    written: bool,
341}
342
343impl Default for Serializer {
344    fn default() -> Serializer {
345        Serializer::new()
346    }
347}
348
349impl Serializer {
350    /// Creates a serializer.
351    pub fn new() -> Serializer {
352        Serializer::with_config(SerializerConfig::new())
353    }
354
355    /// Creates a serializer with the given configuration.
356    pub fn with_config(config: SerializerConfig) -> Serializer {
357        Serializer {
358            config,
359            out: String::new(),
360            written: false,
361        }
362    }
363
364    /// Serializes a value.
365    ///
366    /// If the value fails to serialize, nothing is written.
367    pub fn serialize<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<(), Error> {
368        ser::Serializer::serialize(self, value)
369    }
370
371    /// Serializes a value with a configured driver.
372    ///
373    /// The callback is invoked with the driver before the value is
374    /// serialized, for instance to add [`Layer`](deser_core::ser::Layer)s.
375    pub fn serialize_with<F, T: Serialize + ?Sized>(
376        &mut self,
377        value: &T,
378        setup: F,
379    ) -> Result<(), Error>
380    where
381        F: FnOnce(&mut SerializeDriver<'_>),
382    {
383        ser::Serializer::serialize_with(self, value, setup)
384    }
385
386    /// Returns the configuration.
387    pub fn config(&self) -> &SerializerConfig {
388        &self.config
389    }
390
391    /// Returns the output written so far (that was not cleared).
392    pub fn as_str(&self) -> &str {
393        &self.out
394    }
395
396    /// Returns the output.
397    pub fn finish(self) -> String {
398        self.out
399    }
400}
401
402impl ser::Serializer for Serializer {
403    fn drive(&mut self, driver: &mut SerializeDriver<'_>) -> Result<(), Error> {
404        if !self.config.context.is_empty() {
405            driver.set_default_context(self.config.context.clone());
406        }
407        if self.written {
408            return Err(Error::new(
409                ErrorKind::InvalidState,
410                "an INI file holds a single value",
411            ));
412        }
413        let ini = self.config.serialize_driver(driver)?;
414        self.out.push_str(&ini);
415        self.written = true;
416        Ok(())
417    }
418}
419
420impl ser::StreamSerializer for Serializer {
421    fn output(&self) -> &[u8] {
422        self.out.as_bytes()
423    }
424
425    fn clear_output(&mut self) {
426        self.out.clear();
427    }
428}
429
430#[cfg(feature = "io")]
431impl SerializerConfig {
432    /// Creates a writer of an INI file (see
433    /// [`deser::io::Writer`](deser_core::io::Writer)).
434    ///
435    /// A stream holds a single file, writing a second value fails.  The file
436    /// is written with a single write once it's complete.
437    pub fn writer<W: std::io::Write>(&self, writer: W) -> deser_core::io::Writer<W, Serializer> {
438        deser_core::io::Writer::new(writer, Serializer::with_config(self.clone()))
439    }
440
441    /// Serializes a value to a writer.
442    ///
443    /// See [`to_writer`].
444    pub fn to_writer<W: std::io::Write, T: Serialize + ?Sized>(
445        &self,
446        writer: W,
447        value: &T,
448    ) -> Result<(), Error> {
449        self.writer(writer).write(value)
450    }
451}
452
453/// Serializes a value to a writer.
454///
455/// The file is written with a single write once it's complete.
456///
457/// ```
458/// use std::collections::BTreeMap;
459///
460/// let mut out = Vec::new();
461/// deser_ini::to_writer(&mut out, &BTreeMap::from([("a", 1)])).unwrap();
462/// assert_eq!(out, b"a = 1\n");
463/// ```
464#[cfg(feature = "io")]
465pub fn to_writer<W: std::io::Write, T: Serialize + ?Sized>(
466    writer: W,
467    value: &T,
468) -> Result<(), Error> {
469    SerializerConfig::new().to_writer(writer, value)
470}
471
472/// Serializes a value to an INI file.
473///
474/// This uses the default [`SerializerConfig`], see there for more
475/// information.
476///
477/// ```
478/// #[derive(deser::Serialize)]
479/// struct Config {
480///     debug: bool,
481///     server: Server,
482/// }
483///
484/// #[derive(deser::Serialize)]
485/// struct Server {
486///     host: &'static str,
487///     greeting: &'static str,
488/// }
489///
490/// let config = Config {
491///     debug: false,
492///     server: Server { host: "localhost", greeting: " hello ; world" },
493/// };
494/// assert_eq!(
495///     deser_ini::to_string(&config).unwrap(),
496///     "debug = false\n\n[server]\nhost = localhost\ngreeting = \" hello ; world\"\n"
497/// );
498/// ```
499pub fn to_string<T: Serialize + ?Sized>(value: &T) -> Result<String, Error> {
500    SerializerConfig::new().to_string(value)
501}
502
503/// A map that is being written: the root, a section or a subsection.
504struct Table {
505    /// The name of a section.
506    name: String,
507    /// 0 for the root, 1 for sections and 2 for subsections.
508    depth: usize,
509    /// The header (with line break).
510    header: String,
511    /// The lines of the keys.
512    body: String,
513    /// The sections or subsections in the table.
514    tables: String,
515    /// The key of the next value.
516    key: Option<String>,
517    /// The names of the keys and tables (lowercased like git reads them in
518    /// git's config files, except for subsections), with whether they are
519    /// tables.
520    names: HashMap<String, bool>,
521}
522
523enum Frame {
524    Table(Table),
525    /// A sequence (written as repeated key).
526    Seq(String),
527}
528
529/// Writes the events of a value.
530struct Writer<'c> {
531    config: &'c SerializerConfig,
532    /// How bytes are written (from the state).
533    bytes: BytesFormat,
534    stack: Vec<Frame>,
535    out: String,
536}
537
538impl Writer<'_> {
539    fn event(&mut self, event: Event, _state: &State) -> Result<(), Error> {
540        /// What the event is for.
541        enum Position {
542            Start,
543            Key,
544            Value,
545            Element,
546        }
547
548        let position = match self.stack.last() {
549            None => Position::Start,
550            Some(Frame::Table(table)) if table.key.is_none() => Position::Key,
551            Some(Frame::Table(_)) => Position::Value,
552            Some(Frame::Seq(_)) => Position::Element,
553        };
554        match position {
555            Position::Start => match event {
556                Event::MapStart(_) => self.stack.push(Frame::Table(Table {
557                    name: String::new(),
558                    depth: 0,
559                    header: String::new(),
560                    body: String::new(),
561                    tables: String::new(),
562                    key: None,
563                    names: HashMap::new(),
564                })),
565                Event::Atom(Atom::Null) => {}
566                _ => {
567                    return Err(Error::new(
568                        ErrorKind::UnsupportedType,
569                        "INI files hold maps (like structs)",
570                    ));
571                }
572            },
573            Position::Key => match event {
574                Event::Atom(ref atom) => {
575                    let key = self.key_text(atom)?.into_owned();
576                    self.top_table().key = Some(key);
577                }
578                Event::MapEnd => self.end_table(),
579                _ => return Err(unsupported_key()),
580            },
581            Position::Value => {
582                let key = self.top_table().key.take().unwrap_or_default();
583                match event {
584                    Event::Atom(ref atom) => {
585                        if let Some(value) = self.value_text(atom)? {
586                            let mut line = String::new();
587                            self.write_entry(&mut line, &key, Some(&value))?;
588                            self.claim_name(&key, false, false)?;
589                            self.top_table().body.push_str(&line);
590                        }
591                    }
592                    Event::SeqStart(_) => {
593                        self.check_key(&key)?;
594                        self.claim_name(&key, false, false)?;
595                        self.stack.push(Frame::Seq(key));
596                    }
597                    Event::MapStart(_) => self.start_table(key)?,
598                    Event::MapEnd | Event::SeqEnd => {
599                        return Err(Error::new(ErrorKind::InvalidState, "unexpected end event"));
600                    }
601                }
602            }
603            Position::Element => match event {
604                Event::SeqEnd => {
605                    self.stack.pop();
606                }
607                Event::Atom(ref atom) => {
608                    let key = match self.stack.last() {
609                        Some(Frame::Seq(key)) => key.clone(),
610                        _ => unreachable!(),
611                    };
612                    let value = self.value_text(atom)?;
613                    let mut line = String::new();
614                    self.write_entry(&mut line, &key, value.as_deref())?;
615                    let len = self.stack.len();
616                    match self.stack[len - 2] {
617                        Frame::Table(ref mut table) => table.body.push_str(&line),
618                        Frame::Seq(_) => unreachable!("sequences are in tables"),
619                    }
620                }
621                Event::MapStart(_) | Event::SeqStart(_) => {
622                    return Err(Error::new(
623                        ErrorKind::UnsupportedType,
624                        "INI files cannot hold sequences of maps or sequences",
625                    ));
626                }
627                Event::MapEnd => {
628                    return Err(Error::new(ErrorKind::InvalidState, "unexpected end event"));
629                }
630            },
631        }
632        Ok(())
633    }
634
635    /// Returns the table on the top of the stack.
636    fn top_table(&mut self) -> &mut Table {
637        match self.stack.last_mut() {
638            Some(Frame::Table(table)) => table,
639            _ => unreachable!("values of maps are written into tables"),
640        }
641    }
642
643    /// Starts a section or subsection.
644    fn start_table(&mut self, key: String) -> Result<(), Error> {
645        let parent = self.top_table();
646        let depth = parent.depth + 1;
647        let git = self.config.syntax == Syntax::Git;
648        let header = match depth {
649            1 => {
650                self.check_section(&key)?;
651                format!("[{}]\n", key)
652            }
653            2 if git => {
654                if key.contains(['\n', '\0']) {
655                    return Err(Error::new(
656                        ErrorKind::UnsupportedType,
657                        format!("subsection {:?} cannot be written", key),
658                    ));
659                }
660                let mut header = format!("[{} \"", self.top_table().name);
661                for c in key.chars() {
662                    if c == '\\' || c == '"' {
663                        header.push('\\');
664                    }
665                    header.push(c);
666                }
667                header.push_str("\"]\n");
668                header
669            }
670            _ => {
671                return Err(Error::new(
672                    ErrorKind::UnsupportedType,
673                    if git {
674                        "subsections of git's config files cannot hold maps"
675                    } else {
676                        "sections of INI files cannot hold maps"
677                    },
678                ));
679            }
680        };
681        self.claim_name(&key, true, depth == 2)?;
682        self.stack.push(Frame::Table(Table {
683            name: key,
684            depth,
685            header,
686            body: String::new(),
687            tables: String::new(),
688            key: None,
689            names: HashMap::new(),
690        }));
691        Ok(())
692    }
693
694    /// Ends the table on the top of the stack.
695    fn end_table(&mut self) {
696        let table = match self.stack.pop() {
697            Some(Frame::Table(table)) => table,
698            _ => unreachable!("tables are ended by MapEnd"),
699        };
700        // tables are separated by empty lines (also from the keys before
701        // them, which can be written after them)
702        let mut text = String::new();
703        if table.depth > 0 && (!table.body.is_empty() || table.tables.is_empty()) {
704            // a section that only has subsections needs no header
705            text.push_str(&table.header);
706        }
707        text.push_str(&table.body);
708        if !table.body.is_empty() && !table.tables.is_empty() {
709            text.push('\n');
710        }
711        text.push_str(&table.tables);
712        if table.depth == 0 {
713            self.out.push_str(&text);
714            return;
715        }
716        let parent = self.top_table();
717        if !parent.tables.is_empty() {
718            parent.tables.push('\n');
719        }
720        parent.tables.push_str(&text);
721    }
722
723    /// Records the name of a key or table in the table on the top of the
724    /// stack.
725    ///
726    /// A key and a section cannot have the same name, they would be read
727    /// back as the same value.  In git's config files no two keys can have
728    /// the same name, and the names of keys and sections are case
729    /// insensitive (`a` and `A` are the same name).  The names of
730    /// subsections are case sensitive.
731    fn claim_name(&mut self, name: &str, table: bool, case_sensitive: bool) -> Result<(), Error> {
732        let git = self.config.syntax == Syntax::Git;
733        let name = if case_sensitive || !git {
734            name.to_string()
735        } else {
736            name.to_ascii_lowercase()
737        };
738        match self.top_table().names.insert(name, table) {
739            None => Ok(()),
740            // repeated keys and sections of INI files are merged
741            Some(was_table) if !git && was_table == table => Ok(()),
742            Some(_) => Err(Error::new(
743                ErrorKind::UnsupportedType,
744                if git {
745                    "different keys have the same name in git's config files"
746                } else {
747                    "a key and a section have the same name"
748                },
749            )),
750        }
751    }
752
753    /// Checks if a key can be written.
754    fn check_key(&self, key: &str) -> Result<(), Error> {
755        let valid = match self.config.syntax {
756            Syntax::Git => {
757                key.starts_with(|c: char| c.is_ascii_alphabetic())
758                    && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
759            }
760            // a byte order mark at the start of the file is skipped
761            Syntax::Ini => {
762                !key.is_empty()
763                    && key.trim_matches([' ', '\t']) == key
764                    && !key.starts_with(['[', ';', '#', '\u{feff}'])
765                    && !key.contains(['=', '\n', '\r'])
766                    && !(self.config.colon_delimiter && key.contains(':'))
767            }
768        };
769        if valid {
770            Ok(())
771        } else {
772            Err(Error::new(
773                ErrorKind::UnsupportedType,
774                format!("key {:?} cannot be written", key),
775            ))
776        }
777    }
778
779    /// Checks if the name of a section can be written.
780    fn check_section(&self, name: &str) -> Result<(), Error> {
781        let valid = match self.config.syntax {
782            Syntax::Git => {
783                !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
784            }
785            // the first `]` that is followed by nothing or a comment
786            // ends the name when it's read
787            Syntax::Ini => {
788                !name.contains(['\n', '\r'])
789                    && name.match_indices(']').all(|(pos, _)| {
790                        !name[pos + 1..]
791                            .trim_start_matches([' ', '\t'])
792                            .starts_with([';', '#'])
793                    })
794            }
795        };
796        if valid {
797            Ok(())
798        } else {
799            Err(Error::new(
800                ErrorKind::UnsupportedType,
801                format!("section {:?} cannot be written", name),
802            ))
803        }
804    }
805
806    /// Writes the line of a key (without value for `None`).
807    fn write_entry(&self, out: &mut String, key: &str, value: Option<&str>) -> Result<(), Error> {
808        self.check_key(key)?;
809        let git = self.config.syntax == Syntax::Git;
810        if git {
811            out.push('\t');
812        }
813        out.push_str(key);
814        if let Some(value) = value {
815            out.push_str(" =");
816            let value = if git {
817                git_value(value)
818            } else {
819                self.ini_value(value)?
820            };
821            // `;` only starts a comment after whitespace
822            if !value.is_empty() && !value.starts_with(';') {
823                out.push(' ');
824            }
825            out.push_str(&value);
826        }
827        out.push('\n');
828        Ok(())
829    }
830
831    /// Returns `true` if a value of a single line does not read back
832    /// without quotes.
833    fn needs_quotes(&self, value: &str) -> bool {
834        // a value that starts with `;` is written without whitespace in
835        // front, so it's not a comment
836        value.trim_matches([' ', '\t']) != value
837            || comment_start(value, self.config.inline_comments) < value.len()
838            || (self.config.quotes == Quotes::Value && value.starts_with(['"', '\'']))
839            || (self.config.continuation == Continuation::Backslash && value.ends_with('\\'))
840    }
841
842    /// Returns how a value is written in INI files.
843    fn ini_value<'v>(&self, value: &'v str) -> Result<Cow<'v, str>, Error> {
844        if value.contains(['\n', '\r']) {
845            return self.multiline_value(value).map(Cow::Owned);
846        }
847        if !self.needs_quotes(value) {
848            return Ok(Cow::Borrowed(value));
849        }
850        if self.config.quotes == Quotes::None {
851            return Err(Error::new(
852                ErrorKind::UnsupportedType,
853                format!("value {:?} cannot be written without quotes", value),
854            ));
855        }
856        let mut out = String::with_capacity(value.len() + 2);
857        out.push('"');
858        for c in value.chars() {
859            if c == '\\' || c == '"' {
860                out.push('\\');
861            }
862            out.push(c);
863        }
864        out.push('"');
865        Ok(Cow::Owned(out))
866    }
867
868    /// Writes a value with line breaks as continuation lines.
869    fn multiline_value(&self, value: &str) -> Result<String, Error> {
870        let lines: Vec<&str> = value.split('\n').collect();
871        let valid = self.config.continuation == Continuation::Indented
872            && !value.contains('\r')
873            && !lines[0].is_empty()
874            && !lines[lines.len() - 1].is_empty()
875            && lines.iter().all(|line| {
876                line.is_empty()
877                    || (!self.needs_quotes(line)
878                        && !line.starts_with(['#', ';'])
879                        && !(self.config.quotes == Quotes::Value && line.starts_with(['"', '\''])))
880            });
881        if !valid {
882            return Err(Error::new(
883                ErrorKind::UnsupportedType,
884                format!("value {:?} cannot be written as continuation lines", value),
885            ));
886        }
887        let mut out = String::from(lines[0]);
888        for line in &lines[1..] {
889            out.push('\n');
890            if !line.is_empty() {
891                out.push_str("    ");
892                out.push_str(line);
893            }
894        }
895        Ok(out)
896    }
897
898    /// Returns the text of a map key.
899    fn key_text<'a>(&self, atom: &'a Atom<'_>) -> Result<Cow<'a, str>, Error> {
900        match atom {
901            Atom::Null | Atom::Bytes(_) => Err(unsupported_key()),
902            atom => self.value_text(atom)?.ok_or_else(unsupported_key),
903        }
904    }
905
906    /// Returns the text of a value, `None` for null.
907    fn value_text<'a>(&self, atom: &'a Atom<'_>) -> Result<Option<Cow<'a, str>>, Error> {
908        Ok(Some(match *atom {
909            Atom::Null => return Ok(None),
910            // values whose type was inferred from text are written as value
911            Atom::Implicit(ref value) => {
912                return Ok(self
913                    .value_text(&value.value().to_atom())?
914                    .map(|text| Cow::Owned(text.into_owned())));
915            }
916            Atom::Bool(value) => Cow::Borrowed(if value { "true" } else { "false" }),
917            Atom::Str(ref value) | Atom::Lexical(ref value) => Cow::Borrowed(&**value),
918            Atom::Char(value) => Cow::Owned(value.to_string()),
919            Atom::U64(value) => Cow::Owned(value.to_string()),
920            Atom::I64(value) => Cow::Owned(value.to_string()),
921            Atom::F32(value) => Cow::Owned(zmij::Buffer::new().format(value).into()),
922            Atom::F64(value) => Cow::Owned(zmij::Buffer::new().format(value).into()),
923            Atom::Bytes(ref bytes) => {
924                let format = bytes.fallback.copied().unwrap_or(self.bytes);
925                Cow::Owned(
926                    format
927                        .encode(bytes)
928                        .or_else(|| BytesFormat::BASE64.encode(bytes))
929                        .unwrap_or_default(),
930                )
931            }
932            Atom::Ext(ref ext) => {
933                if let Some(number) = ext.downcast_value_ref::<Number>() {
934                    // numbers keep their text
935                    Cow::Owned(number.as_str().to_string())
936                } else if let Some(value) = ext.downcast_ref::<u128>() {
937                    Cow::Owned(value.to_string())
938                } else if let Some(value) = ext.downcast_ref::<i128>() {
939                    Cow::Owned(value.to_string())
940                } else {
941                    match ext.fallback() {
942                        Atom::Ext(_) => {
943                            return Err(Error::new(
944                                ErrorKind::UnsupportedType,
945                                format!("INI files do not support {}", ext.name()),
946                            ));
947                        }
948                        fallback => match self.value_text(&fallback)? {
949                            Some(text) => Cow::Owned(text.into_owned()),
950                            None => return Ok(None),
951                        },
952                    }
953                }
954            }
955            _ => {
956                return Err(Error::new(
957                    ErrorKind::UnsupportedType,
958                    format!("INI files do not support {}", atom.name()),
959                ));
960            }
961        }))
962    }
963}
964
965/// Returns how a value is written in git's config files.
966///
967/// Values with whitespace at the start or end or with comment characters
968/// are quoted.  Backslashes, quotes and line breaks, tabs and backspaces are
969/// escaped.
970fn git_value(value: &str) -> Cow<'_, str> {
971    let quote = value.starts_with([' ', '\t'])
972        || value.ends_with([' ', '\t'])
973        || value.contains([';', '#', '\r']);
974    if !quote && !value.contains(['\\', '"', '\n', '\t', '\x08']) {
975        return Cow::Borrowed(value);
976    }
977    let mut out = String::with_capacity(value.len() + 2);
978    if quote {
979        out.push('"');
980    }
981    for c in value.chars() {
982        match c {
983            '\\' => out.push_str("\\\\"),
984            '"' => out.push_str("\\\""),
985            '\n' => out.push_str("\\n"),
986            '\t' => out.push_str("\\t"),
987            '\x08' => out.push_str("\\b"),
988            c => out.push(c),
989        }
990    }
991    if quote {
992        out.push('"');
993    }
994    Cow::Owned(out)
995}
996
997#[cold]
998fn unsupported_key() -> Error {
999    Error::new(
1000        ErrorKind::UnsupportedType,
1001        "keys of INI files must be strings, numbers or booleans",
1002    )
1003}