From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: Christoph Heiss <c.heiss@proxmox.com>
Cc: pve-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox v2 1/8] serde: implement ini serializer
Date: Tue, 24 Mar 2026 16:14:50 +0100 [thread overview]
Message-ID: <q6ddnyasb72exvv7jbkw3qj265kwugxbuk2yk5va47dlgkvmvz@y3merl3y5qea> (raw)
In-Reply-To: <20260213143601.1424613-2-c.heiss@proxmox.com>
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 <c.heiss@proxmox.com>
> ---
> 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<T: fmt::Display>(msg: T) -> Self {
> + Error::Message(msg.to_string())
> + }
> +}
> +
> +impl de::Error for Error {
> + fn custom<T: fmt::Display>(msg: T) -> Self {
> + Error::Message(msg.to_string())
> + }
> +}
> +
> +impl From<io::Error> for Error {
> + fn from(err: io::Error) -> Self {
> + Self::Message(err.to_string())
> + }
> +}
> +
> +impl From<fmt::Error> for Error {
> + fn from(err: fmt::Error) -> Self {
> + Self::Message(err.to_string())
> + }
> +}
> +
> +/// Return type used throughout the serializer.
> +pub type Result<T> = std::result::Result<T, Error>;
> +
> +/// Implements a serde serializer for transforming Rust values into the INI
> +/// format.
> +struct IniSerializer {
> + /// Last key observed during serialization
> + last_key: Option<String>,
> + /// 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<String, Vec<IniSerializer>>,
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<W: io::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: io::Write>(
> + 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<T: Display>(&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<W, T>(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<T>(value: &T) -> Result<String>
> +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<Self::Ok, Self::Error>;
> + type SerializeMap = Self;
> + type SerializeStruct = Self;
> + type SerializeStructVariant = Impossible<Self::Ok, Self::Error>;
> +
So this block from here ...
> + fn serialize_bool(self, v: bool) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_i8(self, v: i8) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_i16(self, v: i16) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_i32(self, v: i32) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_i64(self, v: i64) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_u8(self, v: u8) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_u16(self, v: u16) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_u32(self, v: u32) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_u64(self, v: u64) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_f32(self, v: f32) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_f64(self, v: f64) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
> +
> + fn serialize_char(self, v: char) -> Result<Self::Ok> {
> + self.serialize_value_as_string(v)
> + }
... to here could live in a macro ;-)
> +
> + fn serialize_str(self, v: &str) -> Result<Self::Ok> {
> + 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<Self::Ok> {
> + Err(Error::UnsupportedType("raw bytes"))
> + }
> +
> + fn serialize_none(self) -> Result<Self::Ok> {
> + self.serialize_value_as_string("")
> + }
> +
> + fn serialize_some<T>(self, v: &T) -> Result<Self::Ok>
> + where
> + T: ?Sized + Serialize,
> + {
> + v.serialize(self)
> + }
> +
> + fn serialize_unit(self) -> Result<Self::Ok> {
> + self.last_key = None;
> + Ok(())
> + }
> +
> + fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok> {
> + self.last_key = None;
> + self.serialize_unit()
> + }
> +
> + fn serialize_unit_variant(
> + self,
> + _name: &'static str,
> + _variant_index: u32,
> + variant: &'static str,
> + ) -> Result<Self::Ok> {
> + self.serialize_str(variant)
> + }
> +
> + fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<Self::Ok>
> + where
> + T: ?Sized + Serialize,
> + {
> + value.serialize(self)
> + }
> +
> + // Serializes as externally tagged representation.
↑ No, it refuses to serialize ;-)
> + fn serialize_newtype_variant<T>(
> + self,
> + _name: &'static str,
> + _variant_index: u32,
> + _variant: &'static str,
> + _value: &T,
> + ) -> Result<Self::Ok>
> + where
> + T: ?Sized + Serialize,
> + {
> + Err(Error::UnsupportedType("enum newtype variant"))
> + }
> +
> + fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
> + Ok(IniSeqSerializer::new(self.last_key.take(), self))
> + }
> +
> + fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
> + self.serialize_seq(Some(len))
> + }
> +
> + fn serialize_tuple_struct(
> + self,
> + _name: &'static str,
> + len: usize,
> + ) -> Result<Self::SerializeTupleStruct> {
> + 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<Self::SerializeTupleVariant> {
> + Err(Error::UnsupportedType("enum tuple variant"))
> + }
> +
> + fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
> + Ok(self)
> + }
> +
> + fn serialize_struct(self, _name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
> + self.serialize_map(Some(len))
> + }
> +
> + fn serialize_struct_variant(
> + self,
> + _name: &'static str,
> + _variant_index: u32,
> + _variant: &'static str,
> + _len: usize,
> + ) -> Result<Self::SerializeStructVariant> {
> + Err(Error::UnsupportedType("enum struct variant"))
> + }
> +}
> +
> +impl ser::SerializeMap for &'_ mut IniSerializer {
> + type Ok = ();
> + type Error = Error;
> +
> + fn serialize_key<T>(&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<T>(&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<T>(&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<String>,
> + ser: &'a mut IniSerializer,
> + kvs: String,
> +}
> +
> +impl<'a> IniSeqSerializer<'a> {
> + pub fn new(name: Option<String>, ser: &'a mut IniSerializer) -> Self {
> + Self {
> + name,
> + ser,
> + kvs: String::new(),
> + }
> + }
> +}
> +
> +impl ser::SerializeSeq for IniSeqSerializer<'_> {
> + type Ok = ();
> + type Error = Error;
> +
> + fn serialize_element<T>(&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<T>(&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<T>(&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<W: fmt::Write> IniValueSerializer<'_, W> {
> + fn serialize_value<T: Display>(&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<Self::Ok, Self::Error>;
> + type SerializeMap = Impossible<Self::Ok, Self::Error>;
> + type SerializeStruct = Impossible<Self::Ok, Self::Error>;
> + type SerializeStructVariant = Impossible<Self::Ok, Self::Error>;
> +
> + fn serialize_bool(self, v: bool) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_i8(self, v: i8) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_i16(self, v: i16) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_i32(self, v: i32) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_i64(self, v: i64) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_u8(self, v: u8) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_u16(self, v: u16) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_u32(self, v: u32) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_u64(self, v: u64) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_f32(self, v: f32) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_f64(self, v: f64) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_char(self, v: char) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_str(self, v: &str) -> Result<Self::Ok> {
> + self.serialize_value(v)
> + }
> +
> + fn serialize_bytes(self, _v: &[u8]) -> Result<Self::Ok> {
> + Err(Error::UnsupportedType("raw bytes"))
> + }
> +
> + fn serialize_none(self) -> Result<Self::Ok> {
> + self.serialize_value("")
> + }
> +
> + fn serialize_some<T>(self, v: &T) -> Result<Self::Ok>
> + where
> + T: ?Sized + Serialize,
> + {
> + v.serialize(self)
> + }
> +
> + fn serialize_unit(self) -> Result<Self::Ok> {
> + self.serialize_value("")
> + }
> +
> + fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok> {
> + self.serialize_unit()
> + }
> +
> + fn serialize_unit_variant(
> + self,
> + _name: &'static str,
> + _variant_index: u32,
> + variant: &'static str,
> + ) -> Result<Self::Ok> {
> + self.serialize_str(variant)
> + }
> +
> + fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<Self::Ok>
> + where
> + T: ?Sized + Serialize,
> + {
> + value.serialize(self)
> + }
> +
> + // Serializes as externally tagged representation.
> + fn serialize_newtype_variant<T>(
> + self,
> + _name: &'static str,
> + _variant_index: u32,
> + _variant: &'static str,
> + _value: &T,
> + ) -> Result<Self::Ok>
> + where
> + T: ?Sized + Serialize,
> + {
> + Err(Error::UnsupportedType("newtype variant"))
> + }
> +
> + fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
> + Ok(IniSeqSerializer::new(self.ser.last_key.take(), self.ser))
> + }
> +
> + fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple> {
> + Ok(IniSeqSerializer::new(self.ser.last_key.take(), self.ser))
> + }
> +
> + fn serialize_tuple_struct(
> + self,
> + _name: &'static str,
> + _len: usize,
> + ) -> Result<Self::SerializeTupleStruct> {
> + 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<Self::SerializeTupleVariant> {
> + Err(Error::UnsupportedType("tuple variant"))
> + }
> +
> + fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
> + Err(Error::UnsupportedType("nested maps"))
> + }
> +
> + fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
> + Err(Error::UnsupportedType("nested structs"))
> + }
> +
> + fn serialize_struct_variant(
> + self,
> + _name: &'static str,
> + _variant_index: u32,
> + _variant: &'static str,
> + _len: usize,
> + ) -> Result<Self::SerializeStructVariant> {
> + 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<i32>,
> + some: Option<i32>,
> + bytes: [u8; 3],
> + unit: (),
> + unit_struct: PhantomData<u32>,
> + 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<NestedStruct>,
> + }
> +
> + 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
next prev parent reply other threads:[~2026-03-24 15:14 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-13 14:35 [PATCH proxmox v2 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
2026-02-13 14:35 ` [PATCH proxmox v2 1/8] serde: implement ini serializer Christoph Heiss
2026-03-24 11:13 ` Thomas Lamprecht
2026-03-24 13:08 ` Wolfgang Bumiller
2026-03-25 11:40 ` Christoph Heiss
2026-03-25 12:17 ` Thomas Lamprecht
2026-03-25 18:02 ` Christoph Heiss
2026-03-24 15:14 ` Wolfgang Bumiller [this message]
2026-02-13 14:35 ` [PATCH proxmox v2 2/8] serde: add base64 module for byte arrays Christoph Heiss
2026-03-24 12:57 ` Wolfgang Bumiller
2026-02-13 14:35 ` [PATCH proxmox v2 3/8] network-types: add ServiceEndpoint type as host/port tuple abstraction Christoph Heiss
2026-02-13 14:35 ` [PATCH proxmox v2 4/8] schema: provide integer schema for node ports Christoph Heiss
2026-02-13 14:35 ` [PATCH proxmox v2 5/8] schema: api-types: add ed25519 base64 encoded key schema Christoph Heiss
2026-02-13 14:35 ` [PATCH proxmox v2 6/8] wireguard: init configuration support crate Christoph Heiss
2026-02-13 14:36 ` [PATCH proxmox v2 7/8] wireguard: implement api for PublicKey Christoph Heiss
2026-02-13 14:36 ` [PATCH proxmox v2 8/8] wireguard: make per-peer preshared key optional Christoph Heiss
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=q6ddnyasb72exvv7jbkw3qj265kwugxbuk2yk5va47dlgkvmvz@y3merl3y5qea \
--to=w.bumiller@proxmox.com \
--cc=c.heiss@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox