all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox 2/5] proxmox-fixed-string: extract FixedString into own crate
Date: Thu, 20 Nov 2025 15:50:10 +0100	[thread overview]
Message-ID: <20251120145031.550340-3-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20251120145031.550340-1-s.hanreich@proxmox.com>

As a preparation for using FixedString in pbs-api-types, extract the
type into its own crate, so it can be shared between pbs-api-types and
pve-api-types. This code is taken directly from the pve-api-types
implementation without any functional changes.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 Cargo.toml                                |   2 +
 proxmox-fixed-string/Cargo.toml           |  14 ++
 proxmox-fixed-string/debian/changelog     |   5 +
 proxmox-fixed-string/debian/control       |  34 +++
 proxmox-fixed-string/debian/copyright     |  18 ++
 proxmox-fixed-string/debian/debcargo.toml |   7 +
 proxmox-fixed-string/src/lib.rs           | 274 ++++++++++++++++++++++
 7 files changed, 354 insertions(+)
 create mode 100644 proxmox-fixed-string/Cargo.toml
 create mode 100644 proxmox-fixed-string/debian/changelog
 create mode 100644 proxmox-fixed-string/debian/control
 create mode 100644 proxmox-fixed-string/debian/copyright
 create mode 100644 proxmox-fixed-string/debian/debcargo.toml
 create mode 100644 proxmox-fixed-string/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index b4691a2d..c07795f3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ members = [
     "proxmox-config-digest",
     "proxmox-daemon",
     "proxmox-dns-api",
+    "proxmox-fixed-string",
     "proxmox-http",
     "proxmox-http-error",
     "proxmox-human-byte",
@@ -148,6 +149,7 @@ proxmox-async = { version = "0.5.0", path = "proxmox-async" }
 proxmox-base64 = {  version = "1.0.0", path = "proxmox-base64" }
 proxmox-compression = { version = "1.0.0", path = "proxmox-compression" }
 proxmox-daemon = { version = "1.0.0", path = "proxmox-daemon" }
+proxmox-fixed-string = { version = "0.1.0", path = "proxmox-fixed-string" }
 proxmox-http = { version = "1.0.3", path = "proxmox-http" }
 proxmox-http-error = { version = "1.0.0", path = "proxmox-http-error" }
 proxmox-human-byte = { version = "1.0.0", path = "proxmox-human-byte" }
diff --git a/proxmox-fixed-string/Cargo.toml b/proxmox-fixed-string/Cargo.toml
new file mode 100644
index 00000000..01273d40
--- /dev/null
+++ b/proxmox-fixed-string/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "proxmox-fixed-string"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+serde.workspace = true
+serde_plain.workspace = true
diff --git a/proxmox-fixed-string/debian/changelog b/proxmox-fixed-string/debian/changelog
new file mode 100644
index 00000000..ba1b56b5
--- /dev/null
+++ b/proxmox-fixed-string/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-fixed-string (0.1.0) trixie; urgency=medium
+
+  * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com>  Thu, 20 Nov 2025 13:09:14 +0100
diff --git a/proxmox-fixed-string/debian/control b/proxmox-fixed-string/debian/control
new file mode 100644
index 00000000..99a754b6
--- /dev/null
+++ b/proxmox-fixed-string/debian/control
@@ -0,0 +1,34 @@
+Source: rust-proxmox-fixed-string
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-plain-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-fixed-string
+
+Package: librust-proxmox-fixed-string-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-serde-1+default-dev,
+ librust-serde-plain-1+default-dev
+Provides:
+ librust-proxmox-fixed-string+default-dev (= ${binary:Version}),
+ librust-proxmox-fixed-string-0-dev (= ${binary:Version}),
+ librust-proxmox-fixed-string-0+default-dev (= ${binary:Version}),
+ librust-proxmox-fixed-string-0.1-dev (= ${binary:Version}),
+ librust-proxmox-fixed-string-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-fixed-string-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-fixed-string-0.1.0+default-dev (= ${binary:Version})
+Description: Rust crate "proxmox-fixed-string" - Rust source code
+ Source code for Debianized Rust crate "proxmox-fixed-string"
diff --git a/proxmox-fixed-string/debian/copyright b/proxmox-fixed-string/debian/copyright
new file mode 100644
index 00000000..1ea8a56b
--- /dev/null
+++ b/proxmox-fixed-string/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-fixed-string/debian/debcargo.toml b/proxmox-fixed-string/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-fixed-string/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-fixed-string/src/lib.rs b/proxmox-fixed-string/src/lib.rs
new file mode 100644
index 00000000..e70e1327
--- /dev/null
+++ b/proxmox-fixed-string/src/lib.rs
@@ -0,0 +1,274 @@
+use std::borrow::Borrow;
+use std::cmp::Ordering;
+use std::error::Error;
+use std::fmt;
+use std::ops::Deref;
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+
+/// Error type used by constructors of [`FixedString`]
+#[derive(Clone, Copy, Debug)]
+pub struct TooLongError;
+
+impl Error for TooLongError {}
+
+impl fmt::Display for TooLongError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        f.write_str("string is longer than 23 characters")
+    }
+}
+
+/// An immutable string type with a maximum size of 23 bytes.
+///
+/// After construction it is guaranteed that its contents are:
+/// * valid utf-8
+/// * not longer than 23 characters
+///
+/// FixedString is immutable, therefore it is sufficient to validate the invariants only at
+/// construction time to guarantee that they will always hold during the lifecycle of the
+/// struct.
+#[derive(Clone, Copy)]
+pub struct FixedString {
+    buf: [u8; 23],
+    len: u8,
+}
+
+impl FixedString {
+    /// Creates a new FixedString instance from a str reference.
+    ///
+    /// # Errors
+    /// This function will return an error if:
+    /// * The passed string is longer than 23 bytes
+    pub fn new(value: &str) -> Result<Self, TooLongError> {
+        if value.len() > 23 {
+            return Err(TooLongError);
+        }
+
+        let mut buf = [0; 23];
+        buf[..value.len()].copy_from_slice(value.as_bytes());
+
+        Ok(Self {
+            buf,
+            // SAFETY: self.len is at least 0 and at most 23, which fits into u8
+            len: value.len() as u8,
+        })
+    }
+
+    /// Returns a str reference to the stored data
+    #[inline]
+    pub fn as_str(&self) -> &str {
+        // SAFETY: self.buf must be a valid utf-8 string by construction
+        unsafe { str::from_utf8_unchecked(self.as_bytes()) }
+    }
+
+    /// Returns a reference to the set bytes in the stored buffer
+    #[inline]
+    pub fn as_bytes(&self) -> &[u8] {
+        // SAFETY: self.len >= 0 and self.len <= 23 by construction
+        unsafe { self.buf.get_unchecked(..self.len as usize) }
+    }
+}
+
+macro_rules! forward_impl_to_bytes {
+    ($($trait:ident {$fn:ident -> $out:ty })+) => {
+        $(
+            impl $trait for FixedString {
+                #[inline]
+                fn $fn(&self, other: &Self) -> $out {
+                    <[u8] as $trait>::$fn(self.as_bytes(), other.as_bytes())
+                }
+            }
+        )+
+    };
+}
+
+forward_impl_to_bytes! {
+    PartialEq { eq -> bool }
+    PartialOrd { partial_cmp -> Option<Ordering> }
+    Ord { cmp -> Ordering }
+}
+
+macro_rules! forward_impl_to_str_bidir {
+    ($($trait:ident {$fn:ident -> $out:ty })+) => {
+        $(
+            impl $trait<str> for FixedString {
+                #[inline]
+                fn $fn(&self, other: &str) -> $out {
+                    <str as $trait>::$fn(self.as_str(), other)
+                }
+            }
+
+            impl $trait<FixedString> for &str {
+                #[inline]
+                fn $fn(&self, other: &FixedString) -> $out {
+                    <str as $trait>::$fn(self, other.as_str())
+                }
+            }
+        )+
+    };
+}
+
+forward_impl_to_str_bidir! {
+    PartialEq { eq -> bool }
+    PartialOrd { partial_cmp -> Option<Ordering> }
+}
+
+impl Eq for FixedString {}
+
+impl fmt::Display for FixedString {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        fmt::Display::fmt(self.as_str(), f)
+    }
+}
+
+impl fmt::Debug for FixedString {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        fmt::Display::fmt(self.as_str(), f)
+    }
+}
+
+impl Deref for FixedString {
+    type Target = str;
+
+    fn deref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl AsRef<str> for FixedString {
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl AsRef<[u8]> for FixedString {
+    fn as_ref(&self) -> &[u8] {
+        self.as_bytes()
+    }
+}
+
+impl Borrow<str> for FixedString {
+    fn borrow(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl TryFrom<String> for FixedString {
+    type Error = TooLongError;
+
+    fn try_from(value: String) -> Result<Self, Self::Error> {
+        FixedString::new(value.as_str())
+    }
+}
+
+impl FromStr for FixedString {
+    type Err = TooLongError;
+
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        FixedString::new(value)
+    }
+}
+
+impl TryFrom<&str> for FixedString {
+    type Error = TooLongError;
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        FixedString::new(value)
+    }
+}
+
+impl Serialize for FixedString {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(self.as_str())
+    }
+}
+
+impl<'de> Deserialize<'de> for FixedString {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        struct FixedStringVisitor;
+
+        impl<'de> serde::de::Visitor<'de> for FixedStringVisitor {
+            type Value = FixedString;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
+                f.write_str("a string that is at most 23 bytes long")
+            }
+
+            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                v.try_into().map_err(E::custom)
+            }
+        }
+
+        deserializer.deserialize_str(FixedStringVisitor)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use serde_plain;
+
+    #[test]
+    fn test_construct() {
+        let fixed_string = FixedString::new("").expect("empty string is valid");
+        assert_eq!("", fixed_string);
+
+        let fixed_string = FixedString::new("a").expect("valid string");
+        assert_eq!("a", fixed_string);
+
+        let fixed_string = FixedString::new("🌏🌏🌏🌏🌏").expect("valid string");
+        assert_eq!("🌏🌏🌏🌏🌏", fixed_string);
+
+        let fixed_string =
+            FixedString::new("aaaaaaaaaaaaaaaaaaaaaaa").expect("23 characters are allowed");
+        assert_eq!("aaaaaaaaaaaaaaaaaaaaaaa", fixed_string);
+
+        FixedString::new("🌏🌏🌏🌏🌏🌏").expect_err("string too long");
+        FixedString::new("aaaaaaaaaaaaaaaaaaaaaaaa").expect_err("string too long");
+    }
+
+    #[test]
+    fn test_serialize_deserialize() {
+        let valid_string = "aaaaaaaaaaaaaaaaaaaaaaa";
+
+        let fixed_string: FixedString =
+            serde_plain::from_str("aaaaaaaaaaaaaaaaaaaaaaa").expect("deserialization works");
+        assert_eq!(valid_string, fixed_string);
+
+        let serialized_string =
+            serde_plain::to_string(&fixed_string).expect("can be serialized into a string");
+        assert_eq!(valid_string, serialized_string);
+
+        serde_plain::from_str::<FixedString>("aaaaaaaaaaaaaaaaaaaaaaaa")
+            .expect_err("cannot deserialize string that is too long");
+    }
+
+    #[test]
+    fn test_ord() {
+        let fixed_string = FixedString::new("abc").expect("valid string");
+
+        assert!(fixed_string == fixed_string);
+        assert!(fixed_string >= fixed_string);
+        assert!(fixed_string <= fixed_string);
+
+        assert!("ab" < fixed_string);
+        assert!("abc" == fixed_string);
+        assert!("abcd" > fixed_string);
+
+        let larger_fixed_string = FixedString::new("abcde").expect("valid string");
+
+        assert!(larger_fixed_string > fixed_string);
+        assert!(fixed_string < larger_fixed_string);
+    }
+}
-- 
2.47.3


_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel

  parent reply	other threads:[~2025-11-20 14:51 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-20 14:50 [pbs-devel] [RFC proxmox{, -datacenter-manager} 0/6] Add fallback variants to PBS API types Stefan Hanreich
2025-11-20 14:50 ` [pbs-devel] [PATCH proxmox 1/5] proxmox-upgrade-checks: fix meta package version check Stefan Hanreich
2025-11-20 15:04   ` Shannon Sterz
2025-11-20 15:08     ` Stefan Hanreich
2025-11-20 14:50 ` Stefan Hanreich [this message]
2025-11-20 14:50 ` [pbs-devel] [PATCH proxmox 3/5] proxmox-fixed-string: implement hash trait Stefan Hanreich
2025-11-20 14:50 ` [pbs-devel] [PATCH proxmox 4/5] pve-api-types: add proxmox-fixed-string Stefan Hanreich
2025-11-20 14:50 ` [pbs-devel] [PATCH proxmox 5/5] pbs-api-types: add fallback variants to enums in public API Stefan Hanreich
2025-11-20 14:50 ` [pbs-devel] [PATCH proxmox-datacenter-manager 1/1] tree-wide: add enum fallback variants for pbs api types Stefan Hanreich

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=20251120145031.550340-3-s.hanreich@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=pbs-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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal