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
next prev 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox