use std::borrow::Cow;
use std::collections::HashMap;
use deser_core::ext::Number;
use deser_core::ser::{self, SerializeDriver, SerializeRef};
use deser_core::{Atom, BytesFormat, Error, ErrorKind, Event, Serialize, State};
use crate::parser::comment_start;
use crate::{Continuation, InlineComments, Quotes, Syntax};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SerializerConfig {
syntax: Syntax,
inline_comments: InlineComments,
colon_delimiter: bool,
continuation: Continuation,
quotes: Quotes,
context: deser_core::Context,
}
impl Default for SerializerConfig {
fn default() -> SerializerConfig {
SerializerConfig::new()
}
}
impl SerializerConfig {
pub const fn new() -> SerializerConfig {
SerializerConfig {
syntax: Syntax::Ini,
inline_comments: InlineComments::AfterWhitespace,
colon_delimiter: true,
continuation: Continuation::Indented,
quotes: Quotes::Value,
context: deser_core::Context::new(),
}
}
pub const fn python() -> SerializerConfig {
SerializerConfig::builder()
.inline_comments(InlineComments::None)
.quotes(Quotes::None)
.build()
}
pub const fn git() -> SerializerConfig {
SerializerConfig::builder().syntax(Syntax::Git).build()
}
pub const fn builder() -> SerializerConfigBuilder {
SerializerConfigBuilder::new()
}
pub const fn into_builder(self) -> SerializerConfigBuilder {
SerializerConfigBuilder { value: self }
}
pub fn set_context(&mut self, context: deser_core::Context) {
self.context = context;
}
pub fn context(&self) -> &deser_core::Context {
&self.context
}
#[inline]
fn apply_context(&self, driver: &mut SerializeDriver<'_>) {
if !self.context.is_empty() {
driver.set_default_context(self.context.clone());
}
}
pub const fn set_syntax(&mut self, syntax: Syntax) {
self.syntax = syntax;
}
pub const fn set_inline_comments(&mut self, comments: InlineComments) {
self.inline_comments = comments;
}
pub const fn set_colon_delimiter(&mut self, yes: bool) {
self.colon_delimiter = yes;
}
pub const fn set_continuation(&mut self, continuation: Continuation) {
self.continuation = continuation;
}
pub const fn set_quotes(&mut self, quotes: Quotes) {
self.quotes = quotes;
}
pub fn to_string<T: Serialize + ?Sized>(&self, value: &T) -> Result<String, Error> {
self.to_string_ref(SerializeRef::new(&value))
}
pub fn to_string_with<F, T: Serialize + ?Sized>(
&self,
value: &T,
setup: F,
) -> Result<String, Error>
where
F: FnOnce(&mut SerializeDriver<'_>),
{
let mut driver = SerializeDriver::new(&value);
setup(&mut driver);
self.apply_context(&mut driver);
self.serialize_driver(&mut driver)
}
fn to_string_ref(&self, value: SerializeRef<'_>) -> Result<String, Error> {
let mut driver = SerializeDriver::from_ref(value);
self.apply_context(&mut driver);
self.serialize_driver(&mut driver)
}
pub(crate) fn serialize_driver(
&self,
driver: &mut SerializeDriver<'_>,
) -> Result<String, Error> {
let mut writer = Writer {
config: self,
bytes: BytesFormat::of(driver.state()),
stack: Vec::new(),
out: String::new(),
};
driver.drive(|event, state| writer.event(event, state))?;
Ok(writer.out)
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct SerializerConfigBuilder {
value: SerializerConfig,
}
impl SerializerConfigBuilder {
pub const fn new() -> SerializerConfigBuilder {
SerializerConfigBuilder {
value: SerializerConfig::new(),
}
}
pub const fn syntax(mut self, syntax: Syntax) -> SerializerConfigBuilder {
self.value.set_syntax(syntax);
self
}
pub const fn inline_comments(mut self, comments: InlineComments) -> SerializerConfigBuilder {
self.value.set_inline_comments(comments);
self
}
pub const fn colon_delimiter(mut self, yes: bool) -> SerializerConfigBuilder {
self.value.set_colon_delimiter(yes);
self
}
pub const fn continuation(mut self, continuation: Continuation) -> SerializerConfigBuilder {
self.value.set_continuation(continuation);
self
}
pub const fn quotes(mut self, quotes: Quotes) -> SerializerConfigBuilder {
self.value.set_quotes(quotes);
self
}
pub fn context(mut self, context: deser_core::Context) -> SerializerConfigBuilder {
self.value.set_context(context);
self
}
pub const fn build(self) -> SerializerConfig {
let value = unsafe { core::ptr::read(&self.value) };
core::mem::forget(self);
value
}
}
impl Default for SerializerConfigBuilder {
fn default() -> SerializerConfigBuilder {
SerializerConfigBuilder::new()
}
}
#[derive(Debug, Clone)]
pub struct Serializer {
config: SerializerConfig,
out: String,
written: bool,
}
impl Default for Serializer {
fn default() -> Serializer {
Serializer::new()
}
}
impl Serializer {
pub fn new() -> Serializer {
Serializer::with_config(SerializerConfig::new())
}
pub fn with_config(config: SerializerConfig) -> Serializer {
Serializer {
config,
out: String::new(),
written: false,
}
}
pub fn serialize<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<(), Error> {
ser::Serializer::serialize(self, value)
}
pub fn serialize_with<F, T: Serialize + ?Sized>(
&mut self,
value: &T,
setup: F,
) -> Result<(), Error>
where
F: FnOnce(&mut SerializeDriver<'_>),
{
ser::Serializer::serialize_with(self, value, setup)
}
pub fn config(&self) -> &SerializerConfig {
&self.config
}
pub fn as_str(&self) -> &str {
&self.out
}
pub fn finish(self) -> String {
self.out
}
}
impl ser::Serializer for Serializer {
fn drive(&mut self, driver: &mut SerializeDriver<'_>) -> Result<(), Error> {
if !self.config.context.is_empty() {
driver.set_default_context(self.config.context.clone());
}
if self.written {
return Err(Error::new(
ErrorKind::InvalidState,
"an INI file holds a single value",
));
}
let ini = self.config.serialize_driver(driver)?;
self.out.push_str(&ini);
self.written = true;
Ok(())
}
}
impl ser::StreamSerializer for Serializer {
fn output(&self) -> &[u8] {
self.out.as_bytes()
}
fn clear_output(&mut self) {
self.out.clear();
}
}
#[cfg(feature = "io")]
impl SerializerConfig {
pub fn writer<W: std::io::Write>(&self, writer: W) -> deser_core::io::Writer<W, Serializer> {
deser_core::io::Writer::new(writer, Serializer::with_config(self.clone()))
}
pub fn to_writer<W: std::io::Write, T: Serialize + ?Sized>(
&self,
writer: W,
value: &T,
) -> Result<(), Error> {
self.writer(writer).write(value)
}
}
#[cfg(feature = "io")]
pub fn to_writer<W: std::io::Write, T: Serialize + ?Sized>(
writer: W,
value: &T,
) -> Result<(), Error> {
SerializerConfig::new().to_writer(writer, value)
}
pub fn to_string<T: Serialize + ?Sized>(value: &T) -> Result<String, Error> {
SerializerConfig::new().to_string(value)
}
struct Table {
name: String,
depth: usize,
header: String,
body: String,
tables: String,
key: Option<String>,
names: HashMap<String, bool>,
}
enum Frame {
Table(Table),
Seq(String),
}
struct Writer<'c> {
config: &'c SerializerConfig,
bytes: BytesFormat,
stack: Vec<Frame>,
out: String,
}
impl Writer<'_> {
fn event(&mut self, event: Event, _state: &State) -> Result<(), Error> {
enum Position {
Start,
Key,
Value,
Element,
}
let position = match self.stack.last() {
None => Position::Start,
Some(Frame::Table(table)) if table.key.is_none() => Position::Key,
Some(Frame::Table(_)) => Position::Value,
Some(Frame::Seq(_)) => Position::Element,
};
match position {
Position::Start => match event {
Event::MapStart(_) => self.stack.push(Frame::Table(Table {
name: String::new(),
depth: 0,
header: String::new(),
body: String::new(),
tables: String::new(),
key: None,
names: HashMap::new(),
})),
Event::Atom(Atom::Null) => {}
_ => {
return Err(Error::new(
ErrorKind::UnsupportedType,
"INI files hold maps (like structs)",
));
}
},
Position::Key => match event {
Event::Atom(ref atom) => {
let key = self.key_text(atom)?.into_owned();
self.top_table().key = Some(key);
}
Event::MapEnd => self.end_table(),
_ => return Err(unsupported_key()),
},
Position::Value => {
let key = self.top_table().key.take().unwrap_or_default();
match event {
Event::Atom(ref atom) => {
if let Some(value) = self.value_text(atom)? {
let mut line = String::new();
self.write_entry(&mut line, &key, Some(&value))?;
self.claim_name(&key, false, false)?;
self.top_table().body.push_str(&line);
}
}
Event::SeqStart(_) => {
self.check_key(&key)?;
self.claim_name(&key, false, false)?;
self.stack.push(Frame::Seq(key));
}
Event::MapStart(_) => self.start_table(key)?,
Event::MapEnd | Event::SeqEnd => {
return Err(Error::new(ErrorKind::InvalidState, "unexpected end event"));
}
}
}
Position::Element => match event {
Event::SeqEnd => {
self.stack.pop();
}
Event::Atom(ref atom) => {
let key = match self.stack.last() {
Some(Frame::Seq(key)) => key.clone(),
_ => unreachable!(),
};
let value = self.value_text(atom)?;
let mut line = String::new();
self.write_entry(&mut line, &key, value.as_deref())?;
let len = self.stack.len();
match self.stack[len - 2] {
Frame::Table(ref mut table) => table.body.push_str(&line),
Frame::Seq(_) => unreachable!("sequences are in tables"),
}
}
Event::MapStart(_) | Event::SeqStart(_) => {
return Err(Error::new(
ErrorKind::UnsupportedType,
"INI files cannot hold sequences of maps or sequences",
));
}
Event::MapEnd => {
return Err(Error::new(ErrorKind::InvalidState, "unexpected end event"));
}
},
}
Ok(())
}
fn top_table(&mut self) -> &mut Table {
match self.stack.last_mut() {
Some(Frame::Table(table)) => table,
_ => unreachable!("values of maps are written into tables"),
}
}
fn start_table(&mut self, key: String) -> Result<(), Error> {
let parent = self.top_table();
let depth = parent.depth + 1;
let git = self.config.syntax == Syntax::Git;
let header = match depth {
1 => {
self.check_section(&key)?;
format!("[{}]\n", key)
}
2 if git => {
if key.contains(['\n', '\0']) {
return Err(Error::new(
ErrorKind::UnsupportedType,
format!("subsection {:?} cannot be written", key),
));
}
let mut header = format!("[{} \"", self.top_table().name);
for c in key.chars() {
if c == '\\' || c == '"' {
header.push('\\');
}
header.push(c);
}
header.push_str("\"]\n");
header
}
_ => {
return Err(Error::new(
ErrorKind::UnsupportedType,
if git {
"subsections of git's config files cannot hold maps"
} else {
"sections of INI files cannot hold maps"
},
));
}
};
self.claim_name(&key, true, depth == 2)?;
self.stack.push(Frame::Table(Table {
name: key,
depth,
header,
body: String::new(),
tables: String::new(),
key: None,
names: HashMap::new(),
}));
Ok(())
}
fn end_table(&mut self) {
let table = match self.stack.pop() {
Some(Frame::Table(table)) => table,
_ => unreachable!("tables are ended by MapEnd"),
};
let mut text = String::new();
if table.depth > 0 && (!table.body.is_empty() || table.tables.is_empty()) {
text.push_str(&table.header);
}
text.push_str(&table.body);
if !table.body.is_empty() && !table.tables.is_empty() {
text.push('\n');
}
text.push_str(&table.tables);
if table.depth == 0 {
self.out.push_str(&text);
return;
}
let parent = self.top_table();
if !parent.tables.is_empty() {
parent.tables.push('\n');
}
parent.tables.push_str(&text);
}
fn claim_name(&mut self, name: &str, table: bool, case_sensitive: bool) -> Result<(), Error> {
let git = self.config.syntax == Syntax::Git;
let name = if case_sensitive || !git {
name.to_string()
} else {
name.to_ascii_lowercase()
};
match self.top_table().names.insert(name, table) {
None => Ok(()),
Some(was_table) if !git && was_table == table => Ok(()),
Some(_) => Err(Error::new(
ErrorKind::UnsupportedType,
if git {
"different keys have the same name in git's config files"
} else {
"a key and a section have the same name"
},
)),
}
}
fn check_key(&self, key: &str) -> Result<(), Error> {
let valid = match self.config.syntax {
Syntax::Git => {
key.starts_with(|c: char| c.is_ascii_alphabetic())
&& key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
}
Syntax::Ini => {
!key.is_empty()
&& key.trim_matches([' ', '\t']) == key
&& !key.starts_with(['[', ';', '#', '\u{feff}'])
&& !key.contains(['=', '\n', '\r'])
&& !(self.config.colon_delimiter && key.contains(':'))
}
};
if valid {
Ok(())
} else {
Err(Error::new(
ErrorKind::UnsupportedType,
format!("key {:?} cannot be written", key),
))
}
}
fn check_section(&self, name: &str) -> Result<(), Error> {
let valid = match self.config.syntax {
Syntax::Git => {
!name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
}
Syntax::Ini => {
!name.contains(['\n', '\r'])
&& name.match_indices(']').all(|(pos, _)| {
!name[pos + 1..]
.trim_start_matches([' ', '\t'])
.starts_with([';', '#'])
})
}
};
if valid {
Ok(())
} else {
Err(Error::new(
ErrorKind::UnsupportedType,
format!("section {:?} cannot be written", name),
))
}
}
fn write_entry(&self, out: &mut String, key: &str, value: Option<&str>) -> Result<(), Error> {
self.check_key(key)?;
let git = self.config.syntax == Syntax::Git;
if git {
out.push('\t');
}
out.push_str(key);
if let Some(value) = value {
out.push_str(" =");
let value = if git {
git_value(value)
} else {
self.ini_value(value)?
};
if !value.is_empty() && !value.starts_with(';') {
out.push(' ');
}
out.push_str(&value);
}
out.push('\n');
Ok(())
}
fn needs_quotes(&self, value: &str) -> bool {
value.trim_matches([' ', '\t']) != value
|| comment_start(value, self.config.inline_comments) < value.len()
|| (self.config.quotes == Quotes::Value && value.starts_with(['"', '\'']))
|| (self.config.continuation == Continuation::Backslash && value.ends_with('\\'))
}
fn ini_value<'v>(&self, value: &'v str) -> Result<Cow<'v, str>, Error> {
if value.contains(['\n', '\r']) {
return self.multiline_value(value).map(Cow::Owned);
}
if !self.needs_quotes(value) {
return Ok(Cow::Borrowed(value));
}
if self.config.quotes == Quotes::None {
return Err(Error::new(
ErrorKind::UnsupportedType,
format!("value {:?} cannot be written without quotes", value),
));
}
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for c in value.chars() {
if c == '\\' || c == '"' {
out.push('\\');
}
out.push(c);
}
out.push('"');
Ok(Cow::Owned(out))
}
fn multiline_value(&self, value: &str) -> Result<String, Error> {
let lines: Vec<&str> = value.split('\n').collect();
let valid = self.config.continuation == Continuation::Indented
&& !value.contains('\r')
&& !lines[0].is_empty()
&& !lines[lines.len() - 1].is_empty()
&& lines.iter().all(|line| {
line.is_empty()
|| (!self.needs_quotes(line)
&& !line.starts_with(['#', ';'])
&& !(self.config.quotes == Quotes::Value && line.starts_with(['"', '\''])))
});
if !valid {
return Err(Error::new(
ErrorKind::UnsupportedType,
format!("value {:?} cannot be written as continuation lines", value),
));
}
let mut out = String::from(lines[0]);
for line in &lines[1..] {
out.push('\n');
if !line.is_empty() {
out.push_str(" ");
out.push_str(line);
}
}
Ok(out)
}
fn key_text<'a>(&self, atom: &'a Atom<'_>) -> Result<Cow<'a, str>, Error> {
match atom {
Atom::Null | Atom::Bytes(_) => Err(unsupported_key()),
atom => self.value_text(atom)?.ok_or_else(unsupported_key),
}
}
fn value_text<'a>(&self, atom: &'a Atom<'_>) -> Result<Option<Cow<'a, str>>, Error> {
Ok(Some(match *atom {
Atom::Null => return Ok(None),
Atom::Implicit(ref value) => {
return Ok(self
.value_text(&value.value().to_atom())?
.map(|text| Cow::Owned(text.into_owned())));
}
Atom::Bool(value) => Cow::Borrowed(if value { "true" } else { "false" }),
Atom::Str(ref value) | Atom::Lexical(ref value) => Cow::Borrowed(&**value),
Atom::Char(value) => Cow::Owned(value.to_string()),
Atom::U64(value) => Cow::Owned(value.to_string()),
Atom::I64(value) => Cow::Owned(value.to_string()),
Atom::F32(value) => Cow::Owned(zmij::Buffer::new().format(value).into()),
Atom::F64(value) => Cow::Owned(zmij::Buffer::new().format(value).into()),
Atom::Bytes(ref bytes) => {
let format = bytes.fallback.copied().unwrap_or(self.bytes);
Cow::Owned(
format
.encode(bytes)
.or_else(|| BytesFormat::BASE64.encode(bytes))
.unwrap_or_default(),
)
}
Atom::Ext(ref ext) => {
if let Some(number) = ext.downcast_value_ref::<Number>() {
Cow::Owned(number.as_str().to_string())
} else if let Some(value) = ext.downcast_ref::<u128>() {
Cow::Owned(value.to_string())
} else if let Some(value) = ext.downcast_ref::<i128>() {
Cow::Owned(value.to_string())
} else {
match ext.fallback() {
Atom::Ext(_) => {
return Err(Error::new(
ErrorKind::UnsupportedType,
format!("INI files do not support {}", ext.name()),
));
}
fallback => match self.value_text(&fallback)? {
Some(text) => Cow::Owned(text.into_owned()),
None => return Ok(None),
},
}
}
}
_ => {
return Err(Error::new(
ErrorKind::UnsupportedType,
format!("INI files do not support {}", atom.name()),
));
}
}))
}
}
fn git_value(value: &str) -> Cow<'_, str> {
let quote = value.starts_with([' ', '\t'])
|| value.ends_with([' ', '\t'])
|| value.contains([';', '#', '\r']);
if !quote && !value.contains(['\\', '"', '\n', '\t', '\x08']) {
return Cow::Borrowed(value);
}
let mut out = String::with_capacity(value.len() + 2);
if quote {
out.push('"');
}
for c in value.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\x08' => out.push_str("\\b"),
c => out.push(c),
}
}
if quote {
out.push('"');
}
Cow::Owned(out)
}
#[cold]
fn unsupported_key() -> Error {
Error::new(
ErrorKind::UnsupportedType,
"keys of INI files must be strings, numbers or booleans",
)
}