From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 03E9B1FF141 for ; Mon, 30 Mar 2026 20:29:25 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 1EB0B6FD6; Mon, 30 Mar 2026 20:29:48 +0200 (CEST) From: Christoph Heiss To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox v3 1/8] ini: add crate for INI serialization Date: Mon, 30 Mar 2026 20:28:35 +0200 Message-ID: <20260330182856.2401050-2-c.heiss@proxmox.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260330182856.2401050-1-c.heiss@proxmox.com> References: <20260330182856.2401050-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774895292930 X-SPAM-LEVEL: Spam detection results: 0 AWL -1.445 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_SHORT 0.001 Use of a URL Shortener for very short URL RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 1 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 1 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 1 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: JVCODJAO7JE3IUC2KKCVTU5VWCCWSOIO X-Message-ID-Hash: JVCODJAO7JE3IUC2KKCVTU5VWCCWSOIO X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This is needed for serializing WireGuard configurations, to allow consumption by the official wg(8) tooling. It uses a pretty standard INI-like format. One of the "quirks" of the INI format used is that multiple sections the same name are supported, which is also supported here. E.g. `wg syncconf` will be used by in the future by the WireGuard fabric for applying changes to a particular WireGuard interface. Signed-off-by: Christoph Heiss --- Changes v2 -> v3: * move to dedicated `proxmox-ini` crate * rework map serialization to avoid nested serializer instances, instead serializing all values directly in place (thanks Wolfgang!) * make better use of custom error type to indicate whether we last serialized a primitive type or a section * expand tests, esp. around nested lists and structs Changes v1 -> v2: * use correct version of the `pretty-assertions` crate Cargo.toml | 2 + proxmox-ini/Cargo.toml | 18 + proxmox-ini/debian/changelog | 5 + proxmox-ini/debian/control | 32 + proxmox-ini/debian/copyright | 18 + proxmox-ini/debian/debcargo.toml | 7 + proxmox-ini/src/lib.rs | 991 +++++++++++++++++++++++++++++++ 7 files changed, 1073 insertions(+) create mode 100644 proxmox-ini/Cargo.toml create mode 100644 proxmox-ini/debian/changelog create mode 100644 proxmox-ini/debian/control create mode 100644 proxmox-ini/debian/copyright create mode 100644 proxmox-ini/debian/debcargo.toml create mode 100644 proxmox-ini/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 02ff7f81..4617bbab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "proxmox-http", "proxmox-http-error", "proxmox-human-byte", + "proxmox-ini", "proxmox-io", "proxmox-lang", "proxmox-ldap", @@ -117,6 +118,7 @@ openssl = "0.10" pam-sys = "0.5" percent-encoding = "2.1" pin-utils = "0.1.0" +pretty_assertions = "1.4" proc-macro2 = "1.0" quick-xml = "0.36.1" quote = "1.0" diff --git a/proxmox-ini/Cargo.toml b/proxmox-ini/Cargo.toml new file mode 100644 index 00000000..07af66c4 --- /dev/null +++ b/proxmox-ini/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "proxmox-ini" +description = "INI format support for serde" +version = "0.1.0" + +authors.workspace = true +edition.workspace = true +exclude.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +serde.workspace = true + +[dev-dependencies] +pretty_assertions.workspace = true +serde = { workspace = true, features = ["derive"] } diff --git a/proxmox-ini/debian/changelog b/proxmox-ini/debian/changelog new file mode 100644 index 00000000..064f3e2f --- /dev/null +++ b/proxmox-ini/debian/changelog @@ -0,0 +1,5 @@ +rust-proxmox-ini (0.1.0-1) unstable; urgency=medium + + * Initial release. + + -- Proxmox Support Team Fri, 27 Mar 2026 12:03:43 +0100 diff --git a/proxmox-ini/debian/control b/proxmox-ini/debian/control new file mode 100644 index 00000000..4a724570 --- /dev/null +++ b/proxmox-ini/debian/control @@ -0,0 +1,32 @@ +Source: rust-proxmox-ini +Section: rust +Priority: optional +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo +Build-Depends-Arch: cargo:native , + rustc:native , + libstd-rust-dev , + librust-serde-1+default-dev +Maintainer: Proxmox Support Team +Standards-Version: 4.7.2 +Vcs-Git: git://git.proxmox.com/git/proxmox.git +Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com +X-Cargo-Crate: proxmox-ini + +Package: librust-proxmox-ini-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-serde-1+default-dev +Provides: + librust-proxmox-ini+default-dev (= ${binary:Version}), + librust-proxmox-ini-0-dev (= ${binary:Version}), + librust-proxmox-ini-0+default-dev (= ${binary:Version}), + librust-proxmox-ini-0.1-dev (= ${binary:Version}), + librust-proxmox-ini-0.1+default-dev (= ${binary:Version}), + librust-proxmox-ini-0.1.0-dev (= ${binary:Version}), + librust-proxmox-ini-0.1.0+default-dev (= ${binary:Version}) +Description: INI format support for serde - Rust source code + Source code for Debianized Rust crate "proxmox-ini" diff --git a/proxmox-ini/debian/copyright b/proxmox-ini/debian/copyright new file mode 100644 index 00000000..01138fa0 --- /dev/null +++ b/proxmox-ini/debian/copyright @@ -0,0 +1,18 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: + * +Copyright: 2026 Proxmox Server Solutions GmbH +License: AGPL-3.0-or-later + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + . + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + . + You should have received a copy of the GNU Affero General Public License along + with this program. If not, see . diff --git a/proxmox-ini/debian/debcargo.toml b/proxmox-ini/debian/debcargo.toml new file mode 100644 index 00000000..b7864cdb --- /dev/null +++ b/proxmox-ini/debian/debcargo.toml @@ -0,0 +1,7 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team " + +[source] +vcs_git = "git://git.proxmox.com/git/proxmox.git" +vcs_browser = "https://git.proxmox.com/?p=proxmox.git" diff --git a/proxmox-ini/src/lib.rs b/proxmox-ini/src/lib.rs new file mode 100644 index 00000000..921a92e8 --- /dev/null +++ b/proxmox-ini/src/lib.rs @@ -0,0 +1,991 @@ +//! Implements a serde serializer for the INI file format. +//! +//! Nested structs/maps are supported and use the widely used variant of using dots as hierarchy +//! separators. +//! +//! Newtype variants, tuple variants, struct variants and raw bytes are not supported. + +#![forbid(unsafe_code, missing_docs)] + +use std::{ + collections::BTreeMap, + fmt::{self, Display, Write}, + io, +}; + +use serde::ser::{self, Impossible, Serialize}; + +#[derive(Debug, PartialEq)] +/// Errors that can occur during INI serialization. +pub enum Error { + /// Some error that occurred elsewhere. + Generic(String), + /// Error during I/O. + Io(String), + /// Encountered an unsupported data type during serialization. + UnsupportedType(&'static str), + /// A key was expected at this point during serialization, but a value was received. + ExpectedKey, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Generic(s) => write!(f, "{s}"), + Error::Io(s) => write!(f, "{s}"), + Error::UnsupportedType(s) => write!(f, "unsupported data type: {s}"), + Error::ExpectedKey => write!(f, "expected key"), + } + } +} + +impl std::error::Error for Error {} + +impl ser::Error for Error { + fn custom(err: T) -> Self { + Error::Generic(err.to_string()) + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + Self::Io(err.to_string()) + } +} + +impl From for Error { + fn from(err: fmt::Error) -> Self { + Self::Io(err.to_string()) + } +} + +impl From for Error { + fn from(err: std::string::FromUtf8Error) -> Self { + Self::Generic(err.to_string()) + } +} + +/// Return type used throughout the serializer. +pub type Result = std::result::Result; + +#[derive(Clone, Copy, Debug, PartialEq)] +/// Type of serialized value. +enum SerializedType { + /// Last serialized value was a key-value pair, ie. `key = value`. + Simple, + /// Last serialized was a section. + Section, +} + +/// Implements a serde serializer for transforming Rust values into the INI +/// format. +#[derive(Debug)] +struct IniSerializer { + /// Last key observed during serialization + last_key: Option, + /// Already serialized key-value pairs on this level of the serialization tree + buf: String, + /// Nested sections under this part of the tree. Multiple sections with the + /// same name are allowed. + sections: BTreeMap>, +} + +impl IniSerializer { + /// Creates a new INI serializer. + fn new() -> Self { + IniSerializer { + last_key: None, + buf: String::new(), + sections: BTreeMap::new(), + } + } + + /// Write out the serialized INI to a target implementing [`io::Write`]. + fn write(self, mut w: W) -> Result<()> { + w.write_all(self.buf.as_bytes())?; + if !self.buf.is_empty() && !self.sections.is_empty() { + w.write_all(b"\n")?; + } + + for (index, (name, values)) in self.sections.iter().enumerate() { + for (nested_idx, section) in values.iter().enumerate() { + write!(w, "[{name}]\n{section}")?; + + if nested_idx < values.len() - 1 { + w.write_all(b"\n")?; + } + } + + if index < self.sections.len() - 1 { + w.write_all(b"\n")?; + } + } + + Ok(()) + } + + /// Serializes a value using its [`Display`] implementation. + /// If a key is known for this value, it's prepended to the output and forgotten. + fn serialize_as_display(&mut self, v: T) -> Result { + if let Some(key) = self.last_key.take() { + writeln!(self.buf, "{key} = {v}")?; + } else { + self.buf += &v.to_string(); + } + Ok(SerializedType::Simple) + } +} + +/// Serialize the given data structure as INI into an I/O stream. +pub fn to_writer(writer: W, value: &T) -> Result<()> +where + W: io::Write, + T: ?Sized + Serialize, +{ + let mut ser = IniSerializer::new(); + value.serialize(&mut ser)?; + ser.write(writer) +} + +/// Serialize the given data structure as INI into a string. +pub fn to_string(value: &T) -> Result +where + T: ?Sized + Serialize, +{ + let mut buf = Vec::new(); + to_writer(&mut buf, value)?; + + Ok(String::from_utf8(buf)?) +} + +macro_rules! forward_to_display { + ($name:ident($ty:ty), $($rest:tt)* ) => { + fn $name(self, v: $ty) -> Result { + self.serialize_as_display(&v) + } + + forward_to_display! { $($rest)* } + }; + () => {}; +} + +impl<'a> ser::Serializer for &'a mut IniSerializer { + type Ok = SerializedType; + type Error = Error; + + type SerializeSeq = IniSeqSerializer<'a>; + type SerializeTuple = IniSeqSerializer<'a>; + type SerializeTupleStruct = IniSeqSerializer<'a>; + type SerializeTupleVariant = Impossible; + type SerializeMap = IniMapSerializer<'a>; + type SerializeStruct = IniMapSerializer<'a>; + type SerializeStructVariant = Impossible; + + forward_to_display! { + serialize_bool(bool), + serialize_i8(i8), + serialize_i16(i16), + serialize_i32(i32), + serialize_i64(i64), + serialize_u8(u8), + serialize_u16(u16), + serialize_u32(u32), + serialize_u64(u64), + serialize_f32(f32), + serialize_f64(f64), + serialize_char(char), + serialize_str(&str), + } + + fn serialize_bytes(self, _: &[u8]) -> Result { + Err(Error::UnsupportedType("raw bytes")) + } + + fn serialize_none(self) -> Result { + self.last_key = None; + Ok(Self::Ok::Simple) + } + + fn serialize_some(self, v: &T) -> Result + where + T: ?Sized + Serialize, + { + v.serialize(self) + } + + fn serialize_unit(self) -> Result { + self.serialize_none() + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + self.serialize_none() + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + self.serialize_str(variant) + } + + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Err(Error::UnsupportedType("enum newtype variant")) + } + + fn serialize_seq(self, _len: Option) -> Result { + Ok(IniSeqSerializer::new(self)) + } + + fn serialize_tuple(self, len: usize) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::UnsupportedType("enum tuple variant")) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(IniMapSerializer { + ser: self, + last_key: None, + }) + } + + fn serialize_struct(self, _name: &'static str, len: usize) -> Result { + self.serialize_map(Some(len)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::UnsupportedType("enum struct variant")) + } +} + +struct IniMapSerializer<'a> { + /// Root serializer. + ser: &'a mut IniSerializer, + /// Last serialized key observed at this level. + last_key: Option, +} + +impl<'a> ser::SerializeMap for IniMapSerializer<'a> { + type Ok = SerializedType; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + let mut s = String::new(); + key.serialize(IniKeySerializer::new(&mut s))?; + + self.last_key = Some(s); + self.ser.last_key = self.last_key.clone(); + + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + let mut serializer = IniSerializer::new(); + serializer.last_key = self.ser.last_key.clone(); + + let key = self.last_key.clone().ok_or(Error::ExpectedKey)?; + + match value.serialize(&mut serializer)? { + SerializedType::Simple => { + // Value serialized as a primitive type, we can just write that out + + self.ser.buf += &serializer.buf; + } + SerializedType::Section => { + dbg!(&serializer.buf, &serializer.sections); + + if !serializer.buf.is_empty() { + // First, add all top-level entries from the map into a new section, + // in case we serialized a map + self.ser + .sections + .entry(key.clone()) + .or_default() + .push(serializer.buf); + } else if let Some(mut values) = serializer.sections.remove(&key) { + // Otherwise we serialized a sequence of maps, append all of them under the current + // name + self.ser + .sections + .entry(key.clone()) + .or_default() + .append(&mut values); + } + + // .. and finally, append all other nested sections + for (name, mut values) in serializer.sections { + self.ser + .sections + .entry(format!("{key}.{name}")) + .or_default() + .append(&mut values); + } + } + } + + Ok(()) + } + + fn end(self) -> Result { + Ok(Self::Ok::Section) + } +} + +impl<'a> ser::SerializeStruct for IniMapSerializer<'a> { + type Ok = SerializedType; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + ser::SerializeMap::serialize_key(self, key)?; + ser::SerializeMap::serialize_value(self, value) + } + + fn end(self) -> Result { + ser::SerializeMap::end(self) + } +} + +struct IniSeqSerializer<'a> { + /// Root serializer. + ser: &'a mut IniSerializer, + /// Whether at least one element has been serialized yet. + first: bool, + /// Whether we saw at least one section in the past. + has_sections: bool, +} + +impl<'a> IniSeqSerializer<'a> { + pub fn new(ser: &'a mut IniSerializer) -> Self { + Self { + ser, + first: true, + has_sections: false, + } + } +} + +impl ser::SerializeSeq for IniSeqSerializer<'_> { + type Ok = SerializedType; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + // As we (at least, for now) don't support enum newtype variants, types which serialize to + // either a primitive type or a section type cannot be represested. + + let mut serializer = IniSerializer::new(); + let key = self.ser.last_key.clone().ok_or(Error::ExpectedKey)?; + + match value.serialize(&mut serializer)? { + SerializedType::Simple => { + // Value serialized as a primitive type, so write it out + if !self.first { + write!(self.ser.buf, ", {}", serializer.buf)?; + } else { + write!(self.ser.buf, "{key} = {}", serializer.buf)?; + self.first = false; + } + Ok(()) + } + SerializedType::Section => { + self.has_sections = true; + + self.ser + .sections + .entry(key.clone()) + .or_default() + .push(serializer.buf); + + for (name, mut values) in serializer.sections { + self.ser + .sections + .entry(format!("{key}.{name}")) + .or_default() + .append(&mut values); + } + + Ok(()) + } + } + } + + fn end(self) -> Result { + if self.has_sections { + Ok(Self::Ok::Section) + } else { + if !self.first { + self.ser.buf.push('\n'); + } + Ok(Self::Ok::Simple) + } + } +} + +impl ser::SerializeTuple for IniSeqSerializer<'_> { + type Ok = SerializedType; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + ser::SerializeSeq::end(self) + } +} + +impl ser::SerializeTupleStruct for IniSeqSerializer<'_> { + type Ok = SerializedType; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + ser::SerializeSeq::end(self) + } +} + +/// Slimmed down serializer which just supports serializing single values to the given writer and +/// no compound values. +/// +/// Used for serializing keys to their string representation. +struct IniKeySerializer<'a, W: fmt::Write> { + /// Target to write any serialized value to. + writer: &'a mut W, +} + +impl<'a, W: fmt::Write> IniKeySerializer<'a, W> { + fn new(writer: &'a mut W) -> Self { + Self { writer } + } +} + +macro_rules! forward_to_writer_as_str { + ($name:ident($ty:ty), $($rest:tt)* ) => { + fn $name(self, v: $ty) -> Result { + self.writer.write_str(&v.to_string())?; + Ok(()) + } + + forward_to_writer_as_str! { $($rest)* } + }; + () => {}; +} + +impl<'a, W: fmt::Write> ser::Serializer for IniKeySerializer<'a, W> { + type Ok = (); + type Error = Error; + + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + forward_to_writer_as_str! { + serialize_bool(bool), + serialize_i8(i8), + serialize_i16(i16), + serialize_i32(i32), + serialize_i64(i64), + serialize_u8(u8), + serialize_u16(u16), + serialize_u32(u32), + serialize_u64(u64), + serialize_f32(f32), + serialize_f64(f64), + serialize_char(char), + serialize_str(&str), + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + Err(Error::UnsupportedType("raw bytes")) + } + + fn serialize_none(self) -> Result { + Ok(()) + } + + fn serialize_some(self, v: &T) -> Result + where + T: ?Sized + Serialize, + { + v.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Ok(()) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + self.serialize_str(variant) + } + + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Err(Error::UnsupportedType("nested newtype variant")) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(Error::UnsupportedType("nested sequence")) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(Error::UnsupportedType("nested tuple")) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(Error::UnsupportedType("nested tuple struct")) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::UnsupportedType("nested tuple variant")) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(Error::UnsupportedType("nested maps")) + } + + fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { + Err(Error::UnsupportedType("nested structs")) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::UnsupportedType("nested struct variant")) + } +} + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, ffi::CString, marker::PhantomData}; + + use super::{to_string, Error}; + use serde::Serialize; + + #[test] + fn all_supported_types() { + #[derive(Serialize)] + struct NestedStruct { + s: &'static str, + x: f64, + l: Vec<&'static str>, + c: char, + } + + #[derive(Serialize)] + enum Enum { + A, + } + + #[derive(Serialize)] + struct NewtypeStruct(u8); + + #[derive(Serialize)] + struct TopLevel { + a: u32, + nested: NestedStruct, + none: Option, + some: Option, + bytes: [u8; 3], + unit: (), + unit_struct: PhantomData, + unit_variant: Enum, + newtype_struct: NewtypeStruct, + list: Vec, + empty_list: Vec, + one_item_list: Vec, + tuple: (u32, &'static str), + } + + let serialized = to_string(&TopLevel { + a: 1, + nested: NestedStruct { + s: "foo", + x: 123.4567, + l: vec!["a", "b", "c"], + c: 'Y', + }, + none: None, + some: Some(42), + bytes: [1, 2, 3], + unit: (), + unit_struct: PhantomData, + unit_variant: Enum::A, + newtype_struct: NewtypeStruct(42), + list: vec![100, 200, 300], + empty_list: Vec::new(), + one_item_list: vec![42], + tuple: (123, "bar"), + }) + .unwrap(); + + pretty_assertions::assert_eq!( + "a = 1 +some = 42 +bytes = 1, 2, 3 +unit_variant = A +newtype_struct = 42 +list = 100, 200, 300 +one_item_list = 42 +tuple = 123, bar + +[nested] +s = foo +x = 123.4567 +l = a, b, c +c = Y +", + serialized, + ); + } + + #[test] + fn two_levels_nested() { + #[derive(Serialize)] + struct SecondLevel { + x: u32, + } + + #[derive(Serialize)] + struct FirstLevel { + b: f32, + second_level: SecondLevel, + } + + #[derive(Serialize)] + struct NestedStruct { + s: &'static str, + } + + #[derive(Serialize)] + struct TopLevel { + a: u32, + nested: NestedStruct, + first_level: FirstLevel, + } + + let serialized = to_string(&TopLevel { + a: 1, + nested: NestedStruct { s: "foo" }, + first_level: FirstLevel { + b: 12.3, + second_level: SecondLevel { x: 100 }, + }, + }) + .unwrap(); + + pretty_assertions::assert_eq!( + "a = 1 + +[first_level] +b = 12.3 + +[first_level.second_level] +x = 100 + +[nested] +s = foo +", + serialized, + ); + } + + #[test] + fn no_top_level_kvs() { + #[derive(Serialize)] + struct NestedStruct { + s: &'static str, + } + + #[derive(Serialize)] + struct TopLevel { + a: NestedStruct, + b: NestedStruct, + } + + let serialized = to_string(&TopLevel { + a: NestedStruct { s: "foo" }, + b: NestedStruct { s: "bar" }, + }) + .unwrap(); + + pretty_assertions::assert_eq!( + "[a] +s = foo + +[b] +s = bar +", + serialized, + ); + } + + #[test] + fn unsupported_datatypes() { + #[derive(Serialize)] + enum Enum { + A(u32), + B(u32, f32), + C { a: u8, b: &'static str }, + } + + #[derive(Serialize)] + struct TopLevel { + x: Enum, + } + + #[derive(Serialize)] + struct RawBytes { + s: CString, + } + + assert_eq!( + Err(Error::UnsupportedType("enum newtype variant")), + to_string(&TopLevel { x: Enum::A(1) }), + ); + + assert_eq!( + Err(Error::UnsupportedType("enum tuple variant")), + to_string(&TopLevel { x: Enum::B(1, 2.) }), + ); + + assert_eq!( + Err(Error::UnsupportedType("enum struct variant")), + to_string(&TopLevel { + x: Enum::C { + a: 100, + b: "foobar" + } + }), + ); + + assert_eq!( + Err(Error::UnsupportedType("raw bytes")), + to_string(&RawBytes { + s: CString::new("baz").unwrap(), + }) + ); + } + + #[test] + fn multiple_sections_with_same_name() { + #[derive(Serialize)] + struct NestedStruct { + x: u32, + } + + #[derive(Serialize)] + struct TopLevel { + a: u32, + nested: Vec, + } + + let serialized = to_string(&TopLevel { + a: 42, + nested: vec![ + NestedStruct { x: 1 }, + NestedStruct { x: 2 }, + NestedStruct { x: 3 }, + ], + }) + .unwrap(); + + pretty_assertions::assert_eq!( + "a = 42 + +[nested] +x = 1 + +[nested] +x = 2 + +[nested] +x = 3 +", + serialized, + ); + } + + #[test] + fn unsupported_nested_lists() { + #[derive(Serialize)] + struct TopLevel { + x: Vec>, + } + + assert_eq!( + Err(Error::ExpectedKey), + to_string(&TopLevel { + x: vec![vec![1, 2], vec![3, 4]], + }), + ); + } + + #[test] + fn empty_struct_should_produce_nothing() { + #[derive(Serialize)] + struct Empty {} + + #[derive(Serialize)] + struct TopLevel { + empty: Empty, + } + + let serialized = to_string(&TopLevel { empty: Empty {} }).unwrap(); + pretty_assertions::assert_eq!("", serialized); + } + + #[test] + fn deeply_nested() { + #[derive(Serialize)] + struct ThirdLevel { + x: u32, + } + + #[derive(Serialize)] + struct SecondLevel { + third: ThirdLevel, + } + + #[derive(Serialize)] + struct FirstLevel { + second: SecondLevel, + } + + #[derive(Serialize)] + struct TopLevel { + first: FirstLevel, + } + + let serialized = to_string(&TopLevel { + first: FirstLevel { + second: SecondLevel { + third: ThirdLevel { x: 1 }, + }, + }, + }) + .unwrap(); + + pretty_assertions::assert_eq!( + r#"[first.second.third] +x = 1 +"#, + serialized + ); + } + + #[test] + fn ints_as_keys() { + let mut map = BTreeMap::new(); + map.insert(1u32, "one"); + map.insert(2, "two"); + + pretty_assertions::assert_eq!( + r#"1 = one +2 = two +"#, + to_string(&map).unwrap() + ); + } +} -- 2.53.0