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 3230E1FF141 for ; Fri, 16 Jan 2026 16:33:38 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 445291C2A4; Fri, 16 Jan 2026 16:33:36 +0100 (CET) From: Christoph Heiss To: pve-devel@lists.proxmox.com Date: Fri, 16 Jan 2026 16:33:06 +0100 Message-ID: <20260116153317.1146323-2-c.heiss@proxmox.com> X-Mailer: git-send-email 2.52.0 In-Reply-To: <20260116153317.1146323-1-c.heiss@proxmox.com> References: <20260116153317.1146323-1-c.heiss@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1768577558089 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.052 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 SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH proxmox 01/11] serde: implement ini serializer X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" The official WireGuard tooling wg(8) uses a (mostly) INI-like format for consuming configuration. E.g. `wg syncconf` will be used by in the future by the WireGuard fabric for applying changes to a particular WireGuard interface. One of the quirks of the INI format used by wg(8) are that there can be multiple sections with the same name, which is also explicitly supported by this serializer. Signed-off-by: Christoph Heiss --- Cargo.toml | 1 + proxmox-serde/Cargo.toml | 2 + proxmox-serde/debian/control | 4 + proxmox-serde/src/ini.rs | 901 +++++++++++++++++++++++++++++++++++ proxmox-serde/src/lib.rs | 3 + 5 files changed, 911 insertions(+) create mode 100644 proxmox-serde/src/ini.rs diff --git a/Cargo.toml b/Cargo.toml index 27a69afa..3cdad8d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,6 +114,7 @@ openssl = "0.10" pam-sys = "0.5" percent-encoding = "2.1" pin-utils = "0.1.0" +pretty_assertions = "1.4.1" proc-macro2 = "1.0" quick-xml = "0.36.1" quote = "1.0" diff --git a/proxmox-serde/Cargo.toml b/proxmox-serde/Cargo.toml index 78d733d0..d4f9fe43 100644 --- a/proxmox-serde/Cargo.toml +++ b/proxmox-serde/Cargo.toml @@ -20,6 +20,8 @@ proxmox-time.workspace = true [dev-dependencies] serde_json.workspace = true +pretty_assertions.workspace = true [features] perl = [] +ini-ser = [] diff --git a/proxmox-serde/debian/control b/proxmox-serde/debian/control index 62a5033a..d956d54b 100644 --- a/proxmox-serde/debian/control +++ b/proxmox-serde/debian/control @@ -36,15 +36,19 @@ Suggests: librust-proxmox-serde+serde-json-dev (= ${binary:Version}) Provides: librust-proxmox-serde+default-dev (= ${binary:Version}), + librust-proxmox-serde+ini-ser-dev (= ${binary:Version}), librust-proxmox-serde+perl-dev (= ${binary:Version}), librust-proxmox-serde-1-dev (= ${binary:Version}), librust-proxmox-serde-1+default-dev (= ${binary:Version}), + librust-proxmox-serde-1+ini-ser-dev (= ${binary:Version}), librust-proxmox-serde-1+perl-dev (= ${binary:Version}), librust-proxmox-serde-1.0-dev (= ${binary:Version}), librust-proxmox-serde-1.0+default-dev (= ${binary:Version}), + librust-proxmox-serde-1.0+ini-ser-dev (= ${binary:Version}), librust-proxmox-serde-1.0+perl-dev (= ${binary:Version}), librust-proxmox-serde-1.0.1-dev (= ${binary:Version}), librust-proxmox-serde-1.0.1+default-dev (= ${binary:Version}), + librust-proxmox-serde-1.0.1+ini-ser-dev (= ${binary:Version}), librust-proxmox-serde-1.0.1+perl-dev (= ${binary:Version}) Description: Serde formatting tools - Rust source code Source code for Debianized Rust crate "proxmox-serde" diff --git a/proxmox-serde/src/ini.rs b/proxmox-serde/src/ini.rs new file mode 100644 index 00000000..3141e6df --- /dev/null +++ b/proxmox-serde/src/ini.rs @@ -0,0 +1,901 @@ +//! 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 and struct variants are not supported. + +use std::{ + collections::BTreeMap, + fmt::{self, Display, Write}, + io, +}; + +use serde::{ + de, + ser::{self, Impossible, Serialize}, +}; + +/// Errors that can occur during INI serialization. +#[derive(Debug, PartialEq)] +pub enum Error { + Message(String), + UnsupportedType(&'static str), + ExpectedKey, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Message(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(msg: T) -> Self { + Error::Message(msg.to_string()) + } +} + +impl de::Error for Error { + fn custom(msg: T) -> Self { + Error::Message(msg.to_string()) + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + Self::Message(err.to_string()) + } +} + +impl From for Error { + fn from(err: fmt::Error) -> Self { + Self::Message(err.to_string()) + } +} + +/// Return type used throughout the serializer. +pub type Result = std::result::Result; + +/// Implements a serde serializer for transforming Rust values into the INI +/// format. +struct IniSerializer { + /// Last key observed during serialization + last_key: Option, + /// Key-value pairs on this level of the serialization tree + kvs: 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, + kvs: String::new(), + sections: BTreeMap::new(), + } + } + + /// Write out the serialized INI to a target implementing [`io::Write`]. + fn write(self, mut w: W) -> Result<()> { + write!(w, "{}", self.kvs)?; + + let mut is_first_byte = self.kvs.is_empty(); + self.sections.iter().try_for_each(|(name, sections)| { + Self::write_nested(&mut w, name, sections, &mut is_first_byte) + }) + } + + /// Internal, recursive method for writing out serialized INI to a target implementing [`io::Write`]. + fn write_nested( + w: &mut W, + name: &str, + sections: &[IniSerializer], + is_first_byte: &mut bool, + ) -> Result<()> { + for section in sections { + if !section.kvs.is_empty() { + if !*is_first_byte { + writeln!(w)?; + } + + write!(w, "[{name}]\n{}", section.kvs)?; + *is_first_byte = false; + } + + section.sections.iter().try_for_each(|(secname, secs)| { + Self::write_nested(w, &format!("{name}.{secname}"), secs, is_first_byte) + })?; + } + + Ok(()) + } + + /// Serializes a single key-value pair, expecting some previously picked up key. + fn serialize_value_as_string(&mut self, v: T) -> Result<()> { + let key = self.last_key.take().ok_or(Error::ExpectedKey)?; + writeln!(&mut self.kvs, "{key} = {v}")?; + Ok(()) + } +} + +/// 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)?; + + String::from_utf8(buf).map_err(|err| Error::Message(err.to_string())) +} + +impl<'a> ser::Serializer for &'a mut IniSerializer { + type Ok = (); + type Error = Error; + + type SerializeSeq = IniSeqSerializer<'a>; + type SerializeTuple = IniSeqSerializer<'a>; + type SerializeTupleStruct = IniSeqSerializer<'a>; + type SerializeTupleVariant = Impossible; + type SerializeMap = Self; + type SerializeStruct = Self; + type SerializeStructVariant = Impossible; + + fn serialize_bool(self, v: bool) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_i8(self, v: i8) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_i16(self, v: i16) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_i32(self, v: i32) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_i64(self, v: i64) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_u8(self, v: u8) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_u16(self, v: u16) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_u32(self, v: u32) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_u64(self, v: u64) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_f32(self, v: f32) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_f64(self, v: f64) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_char(self, v: char) -> Result { + self.serialize_value_as_string(v) + } + + fn serialize_str(self, v: &str) -> Result { + if self.last_key.is_none() { + self.last_key = Some(v.to_owned()); + Ok(()) + } else { + self.serialize_value_as_string(v) + } + } + + fn serialize_bytes(self, _: &[u8]) -> Result { + Err(Error::UnsupportedType("raw bytes")) + } + + fn serialize_none(self) -> Result { + self.serialize_value_as_string("") + } + + fn serialize_some(self, v: &T) -> Result + where + T: ?Sized + Serialize, + { + v.serialize(self) + } + + fn serialize_unit(self) -> Result { + self.last_key = None; + Ok(()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + self.last_key = None; + self.serialize_unit() + } + + 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) + } + + // Serializes as externally tagged representation. + 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.last_key.take(), 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)) + } + + // Serializes as externally tagged representation. + 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(self) + } + + 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")) + } +} + +impl ser::SerializeMap for &'_ mut IniSerializer { + type Ok = (); + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + let mut buf = String::new(); + self.last_key = None; + key.serialize(&mut IniValueSerializer::new(&mut buf, self))?; + self.last_key = Some(buf); + + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + let key = self.last_key.clone().ok_or(Error::ExpectedKey)?; + let mut buf = String::new(); + + if value + .serialize(&mut IniValueSerializer::new(&mut buf, self)) + .is_ok() + { + // Value serialized as a primitive type, so write it out + if !buf.is_empty() { + writeln!(self.kvs, "{buf}")?; + } + self.last_key = None; + } else { + // Otherwise its a struct or map, so do a recursive serialization + let mut serializer = IniSerializer::new(); + serializer.last_key = Some(key.clone()); + value.serialize(&mut serializer)?; + + self.sections.entry(key).or_default().push(serializer); + } + + Ok(()) + } + + fn end(self) -> Result<()> { + self.last_key = None; + Ok(()) + } +} + +impl ser::SerializeStruct for &'_ mut IniSerializer { + type Ok = (); + 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> { + name: Option, + ser: &'a mut IniSerializer, + kvs: String, +} + +impl<'a> IniSeqSerializer<'a> { + pub fn new(name: Option, ser: &'a mut IniSerializer) -> Self { + Self { + name, + ser, + kvs: String::new(), + } + } +} + +impl ser::SerializeSeq for IniSeqSerializer<'_> { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + let mut buf = String::new(); + if value + .serialize(&mut IniValueSerializer::new(&mut buf, self.ser)) + .is_ok() + { + // Value serialized as a primitive type, so write it out + if let Some(name) = &self.name.take() { + write!(self.kvs, "{name} = ")?; + } else { + write!(self.kvs, ", ")?; + } + write!(self.kvs, "{buf}")?; + } else if let Some(name) = &self.name { + // Otherwise its a struct or map, so do a recursive serialization + let mut serializer = IniSerializer::new(); + value.serialize(&mut serializer)?; + + self.ser + .sections + .entry(name.clone()) + .or_default() + .push(serializer) + } else { + return Err(Error::Message( + "got non-primitive value but no name!".into(), + )); + } + + Ok(()) + } + + fn end(self) -> Result<()> { + if !self.kvs.is_empty() { + Ok(writeln!(self.ser.kvs, "{}", self.kvs)?) + } else { + Ok(()) + } + } +} + +impl ser::SerializeTuple for IniSeqSerializer<'_> { + type Ok = (); + 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 = (); + 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) + } +} + +struct IniValueSerializer<'a, W: fmt::Write> { + writer: &'a mut W, + ser: &'a mut IniSerializer, +} + +impl<'a, W: fmt::Write> IniValueSerializer<'a, W> { + fn new(writer: &'a mut W, ser: &'a mut IniSerializer) -> Self { + Self { writer, ser } + } +} + +impl IniValueSerializer<'_, W> { + fn serialize_value(&mut self, v: T) -> Result<()> { + if let Some(key) = self.ser.last_key.take() { + write!(self.writer, "{key} = ")?; + } + + Ok(write!(self.writer, "{v}")?) + } +} + +impl<'a, W: fmt::Write> ser::Serializer for &'a mut IniValueSerializer<'a, W> { + type Ok = (); + type Error = Error; + + type SerializeSeq = IniSeqSerializer<'a>; + type SerializeTuple = IniSeqSerializer<'a>; + type SerializeTupleStruct = IniSeqSerializer<'a>; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_bool(self, v: bool) -> Result { + self.serialize_value(v) + } + + fn serialize_i8(self, v: i8) -> Result { + self.serialize_value(v) + } + + fn serialize_i16(self, v: i16) -> Result { + self.serialize_value(v) + } + + fn serialize_i32(self, v: i32) -> Result { + self.serialize_value(v) + } + + fn serialize_i64(self, v: i64) -> Result { + self.serialize_value(v) + } + + fn serialize_u8(self, v: u8) -> Result { + self.serialize_value(v) + } + + fn serialize_u16(self, v: u16) -> Result { + self.serialize_value(v) + } + + fn serialize_u32(self, v: u32) -> Result { + self.serialize_value(v) + } + + fn serialize_u64(self, v: u64) -> Result { + self.serialize_value(v) + } + + fn serialize_f32(self, v: f32) -> Result { + self.serialize_value(v) + } + + fn serialize_f64(self, v: f64) -> Result { + self.serialize_value(v) + } + + fn serialize_char(self, v: char) -> Result { + self.serialize_value(v) + } + + fn serialize_str(self, v: &str) -> Result { + self.serialize_value(v) + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + Err(Error::UnsupportedType("raw bytes")) + } + + fn serialize_none(self) -> Result { + self.serialize_value("") + } + + fn serialize_some(self, v: &T) -> Result + where + T: ?Sized + Serialize, + { + v.serialize(self) + } + + fn serialize_unit(self) -> Result { + self.serialize_value("") + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + self.serialize_unit() + } + + 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) + } + + // Serializes as externally tagged representation. + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Err(Error::UnsupportedType("newtype variant")) + } + + fn serialize_seq(self, _len: Option) -> Result { + Ok(IniSeqSerializer::new(self.ser.last_key.take(), self.ser)) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Ok(IniSeqSerializer::new(self.ser.last_key.take(), self.ser)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(IniSeqSerializer::new(self.ser.last_key.take(), self.ser)) + } + + // Serializes as externally tagged representation. + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::UnsupportedType("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("struct variant")) + } +} + +#[cfg(test)] +mod tests { + use std::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>, + } + + #[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, + } + + let serialized = to_string(&TopLevel { + a: 1, + nested: NestedStruct { + s: "foo", + x: 123.4567, + l: vec!["a", "b", "c"], + }, + none: None, + some: Some(42), + bytes: [1, 2, 3], + unit: (), + unit_struct: PhantomData, + unit_variant: Enum::A, + newtype_struct: NewtypeStruct(42), + }) + .unwrap(); + + eprintln!("{}", &serialized); + pretty_assertions::assert_eq!( + "a = 1 +none =\x20 +some = 42 +bytes = 1, 2, 3 +unit =\x20 +unit_struct =\x20 +unit_variant = A +newtype_struct = 42 + +[nested] +s = foo +x = 123.4567 +l = a, b, c +", + 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() { + let a = 1u32; + assert_eq!(to_string(&a), Err(Error::ExpectedKey)); + + #[derive(Serialize)] + enum Enum { + A(u32), + B(u32, f32), + C { a: u8, b: &'static str }, + } + + #[derive(Serialize)] + struct TopLevel { + x: Enum, + } + + assert_eq!( + to_string(&TopLevel { x: Enum::A(1) }), + Err(Error::UnsupportedType("enum newtype variant")) + ); + + assert_eq!( + to_string(&TopLevel { x: Enum::B(1, 2.) }), + Err(Error::UnsupportedType("enum tuple variant")) + ); + + assert_eq!( + to_string(&TopLevel { + x: Enum::C { + a: 100, + b: "foobar" + } + }), + Err(Error::UnsupportedType("enum struct variant")) + ); + } + + #[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, + ); + } +} diff --git a/proxmox-serde/src/lib.rs b/proxmox-serde/src/lib.rs index c16f4efb..28c3054d 100644 --- a/proxmox-serde/src/lib.rs +++ b/proxmox-serde/src/lib.rs @@ -11,6 +11,9 @@ pub mod json; #[cfg(feature = "perl")] pub mod perl; +#[cfg(feature = "ini-ser")] +pub mod ini; + /// Serialize Unix epoch (i64) as RFC3339. /// /// Usage example: -- 2.52.0 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel