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#[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 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 pub const fn python() -> SerializerConfig {
84 SerializerConfig::builder()
85 .inline_comments(InlineComments::None)
86 .quotes(Quotes::None)
87 .build()
88 }
89
90 pub const fn git() -> SerializerConfig {
108 SerializerConfig::builder().syntax(Syntax::Git).build()
109 }
110
111 pub const fn builder() -> SerializerConfigBuilder {
113 SerializerConfigBuilder::new()
114 }
115
116 pub const fn into_builder(self) -> SerializerConfigBuilder {
118 SerializerConfigBuilder { value: self }
119 }
120
121 pub fn set_context(&mut self, context: deser_core::Context) {
129 self.context = context;
130 }
131
132 pub fn context(&self) -> &deser_core::Context {
134 &self.context
135 }
136
137 #[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 pub const fn set_syntax(&mut self, syntax: Syntax) {
150 self.syntax = syntax;
151 }
152
153 pub const fn set_inline_comments(&mut self, comments: InlineComments) {
158 self.inline_comments = comments;
159 }
160
161 pub const fn set_colon_delimiter(&mut self, yes: bool) {
165 self.colon_delimiter = yes;
166 }
167
168 pub const fn set_continuation(&mut self, continuation: Continuation) {
174 self.continuation = continuation;
175 }
176
177 pub const fn set_quotes(&mut self, quotes: Quotes) {
182 self.quotes = quotes;
183 }
184
185 pub fn to_string<T: Serialize + ?Sized>(&self, value: &T) -> Result<String, Error> {
187 self.to_string_ref(SerializeRef::new(&value))
188 }
189
190 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 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 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#[derive(Debug, Clone)]
239#[must_use]
240pub struct SerializerConfigBuilder {
241 value: SerializerConfig,
242}
243
244impl SerializerConfigBuilder {
245 pub const fn new() -> SerializerConfigBuilder {
247 SerializerConfigBuilder {
248 value: SerializerConfig::new(),
249 }
250 }
251
252 pub const fn syntax(mut self, syntax: Syntax) -> SerializerConfigBuilder {
256 self.value.set_syntax(syntax);
257 self
258 }
259
260 pub const fn inline_comments(mut self, comments: InlineComments) -> SerializerConfigBuilder {
264 self.value.set_inline_comments(comments);
265 self
266 }
267
268 pub const fn colon_delimiter(mut self, yes: bool) -> SerializerConfigBuilder {
272 self.value.set_colon_delimiter(yes);
273 self
274 }
275
276 pub const fn continuation(mut self, continuation: Continuation) -> SerializerConfigBuilder {
280 self.value.set_continuation(continuation);
281 self
282 }
283
284 pub const fn quotes(mut self, quotes: Quotes) -> SerializerConfigBuilder {
288 self.value.set_quotes(quotes);
289 self
290 }
291
292 pub fn context(mut self, context: deser_core::Context) -> SerializerConfigBuilder {
296 self.value.set_context(context);
297 self
298 }
299
300 pub const fn build(self) -> SerializerConfig {
302 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#[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 pub fn new() -> Serializer {
352 Serializer::with_config(SerializerConfig::new())
353 }
354
355 pub fn with_config(config: SerializerConfig) -> Serializer {
357 Serializer {
358 config,
359 out: String::new(),
360 written: false,
361 }
362 }
363
364 pub fn serialize<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<(), Error> {
368 ser::Serializer::serialize(self, value)
369 }
370
371 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 pub fn config(&self) -> &SerializerConfig {
388 &self.config
389 }
390
391 pub fn as_str(&self) -> &str {
393 &self.out
394 }
395
396 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 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 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#[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
472pub fn to_string<T: Serialize + ?Sized>(value: &T) -> Result<String, Error> {
500 SerializerConfig::new().to_string(value)
501}
502
503struct Table {
505 name: String,
507 depth: usize,
509 header: String,
511 body: String,
513 tables: String,
515 key: Option<String>,
517 names: HashMap<String, bool>,
521}
522
523enum Frame {
524 Table(Table),
525 Seq(String),
527}
528
529struct Writer<'c> {
531 config: &'c SerializerConfig,
532 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 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 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 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 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 let mut text = String::new();
703 if table.depth > 0 && (!table.body.is_empty() || table.tables.is_empty()) {
704 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 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 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 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 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 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 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 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 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 fn needs_quotes(&self, value: &str) -> bool {
834 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 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 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 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 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 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 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
965fn 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}