public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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




  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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal