From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox 01/11] serde: implement ini serializer
Date: Fri, 16 Jan 2026 16:33:06 +0100 [thread overview]
Message-ID: <20260116153317.1146323-2-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260116153317.1146323-1-c.heiss@proxmox.com>
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>
---
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<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>>,
+}
+
+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)?;
+
+ 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>;
+
+ 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)
+ }
+
+ 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.
+ 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.
+ 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()
+ {
+ // 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<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
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2026-01-16 15:33 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
2026-01-16 15:33 ` Christoph Heiss [this message]
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 02/11] serde: add base64 module for byte arrays Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 03/11] network-types: add ServiceEndpoint type as host/port tuple abstraction Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 04/11] schema: provide integer schema for node ports Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 05/11] schema: api-types: add ed25519 base64 encoded key schema Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 06/11] wireguard: init configuration support crate Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 07/11] wireguard: implement api for PublicKey Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 08/11] wireguard: make per-peer preshared key optional Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 09/11] sdn-types: add wireguard-specific PersistentKeepalive api type Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 10/11] ve-config: fabric: refactor fabric config entry impl using macro Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 11/11] ve-config: sdn: fabrics: add wireguard section config types 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=20260116153317.1146323-2-c.heiss@proxmox.com \
--to=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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.