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 26C821FF144 for ; Tue, 24 Mar 2026 16:14:42 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 502F81617E; Tue, 24 Mar 2026 16:14:59 +0100 (CET) Date: Tue, 24 Mar 2026 16:14:50 +0100 From: Wolfgang Bumiller To: Christoph Heiss Subject: Re: [PATCH proxmox v2 1/8] serde: implement ini serializer Message-ID: References: <20260213143601.1424613-1-c.heiss@proxmox.com> <20260213143601.1424613-2-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: <20260213143601.1424613-2-c.heiss@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774365243518 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.083 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 RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 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 0.001 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 0.001 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: P6ZJGUDM3A7A24MBR7SUP53ERUWN2KCO X-Message-ID-Hash: P6ZJGUDM3A7A24MBR7SUP53ERUWN2KCO X-MailFrom: w.bumiller@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 CC: pve-devel@lists.proxmox.com X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On Fri, Feb 13, 2026 at 03:35:54PM +0100, Christoph Heiss wrote: > 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 > --- > Changes v1 -> v2: > * use correct version of the `pretty-assertions` crate > > 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 6ce4d5ec..f650a9f7 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" > 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>, I'm not convinced buffering nested serializer states like this makes sense. See below. > +} > + > +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)?; (could skip the macro and just call `.write_all`) > + > + 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; > + So this block from here ... > + 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) > + } ... to here could live in a macro ;-) > + > + 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. ↑ No, it refuses to serialize ;-) > + 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. ↑ same > + 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() ^ I find it a bit weird as we're effectively throwing away errors - also. What if the error did not in fact come from this being a struct or map? Also: `IniValueSerializer` refuses *maps* and structs, it only sequences. Since you're using your own error type, you should be able to distinguish them. > + { > + // 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)?; The design that the serializer either "returns" a OK or requires a retry with a different serializer is very awkward to me. Can we maybe encode this more into the types and state? Eg. the serializers could have an `Ok` type which is an enum of 'Simple' and 'Block'. `Simple` would be used for `{key} = {value}` and `Block` for `[{section}.{key}]\n{block}` for example (and could skip emitting the section lien if the block starts with one in case it was a nested struct)? Then again, I'm not sure how well the format is defined anyway and what it actually supports. Eg. what about happens when nesting structs and arrays more deeply? > + > + 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