public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Nicolas Frey <n.frey@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH pbs-api-types] crypto: improve deserialization for Fingerprint with custom visitor
Date: Fri, 26 Sep 2025 13:08:42 +0200	[thread overview]
Message-ID: <20250926110842.216011-1-n.frey@proxmox.com> (raw)

Improve Fingerprint deserialization by introducing a custom Visitor,
which:
- eliminates unsafe code by zero-initializing buffers
- removes unnecessary allocations by using a fixed-size buffer
- improves efficiency by filtering and decoding directly from input

Also added tests to establish baseline behavior and verify
correctness.

Signed-off-by: Nicolas Frey <n.frey@proxmox.com>
---
 pbs-api-types/src/crypto.rs | 102 ++++++++++++++++++++++++++++++++----
 1 file changed, 92 insertions(+), 10 deletions(-)

diff --git a/pbs-api-types/src/crypto.rs b/pbs-api-types/src/crypto.rs
index cdc1ba64..a9528022 100644
--- a/pbs-api-types/src/crypto.rs
+++ b/pbs-api-types/src/crypto.rs
@@ -67,9 +67,9 @@ fn as_fingerprint(bytes: &[u8]) -> String {
 }
 
 pub mod bytes_as_fingerprint {
-    use std::mem::MaybeUninit;
+    use std::fmt;
 
-    use serde::{Deserialize, Deserializer, Serializer};
+    use serde::{de, Deserializer, Serializer};
 
     pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
     where
@@ -83,13 +83,95 @@ pub mod bytes_as_fingerprint {
     where
         D: Deserializer<'de>,
     {
-        // TODO: more efficiently implement with a Visitor implementing visit_str using split() and
-        // hex::decode by-byte
-        let mut s = String::deserialize(deserializer)?;
-        s.retain(|c| c != ':');
-        let mut out = MaybeUninit::<[u8; 32]>::uninit();
-        hex::decode_to_slice(s.as_bytes(), unsafe { &mut (*out.as_mut_ptr())[..] })
-            .map_err(serde::de::Error::custom)?;
-        Ok(unsafe { out.assume_init() })
+        struct FingerprintVisitor;
+
+        impl<'de> de::Visitor<'de> for FingerprintVisitor {
+            type Value = [u8; 32];
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                f.write_str("a 32-byte fingerprint hex string with colons")
+            }
+
+            fn visit_str<E>(self, val: &str) -> Result<Self::Value, E>
+            where
+                E: de::Error,
+            {
+                let mut filtered = [0u8; 64];
+                let mut idx = 0;
+
+                for &b in val.as_bytes().iter().filter(|&&b| b != b':') {
+                    if idx == 64 {
+                        return Err(E::custom("fingerprint too long"));
+                    }
+                    filtered[idx] = b;
+                    idx += 1;
+                }
+
+                if idx != 64 {
+                    return Err(E::custom("fingerprint too short"));
+                }
+
+                let mut out = [0u8; 32];
+                hex::decode_to_slice(&filtered, &mut out).map_err(serde::de::Error::custom)?;
+                Ok(out)
+            }
+        }
+
+        deserializer.deserialize_str(FingerprintVisitor)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::Fingerprint;
+
+    static SAMPLE_BYTES: [u8; 32] = [
+        0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
+        0xff, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0,
+        0xf0, 0x01,
+    ];
+
+    #[test]
+    fn serialize_valid() {
+        let s = Fingerprint::new(SAMPLE_BYTES);
+        let encoded = serde_plain::to_string(&s).unwrap();
+        assert!(encoded.contains("00:11:22:33:44:55:66:77"));
+        assert!(encoded.contains("f0:01"));
+    }
+
+    #[test]
+    fn deserialize_valid() {
+        let s = "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:10:20:30:40:50:60:70:80:90:a0:b0:c0:d0:e0:f0:01";
+        let parsed = serde_plain::from_str::<Fingerprint>(s).unwrap();
+        assert_eq!(parsed.bytes(), &SAMPLE_BYTES);
+    }
+
+    #[test]
+    fn roundtrip() {
+        let original = Fingerprint::new(SAMPLE_BYTES);
+        let encoded = serde_plain::to_string(&original).unwrap();
+        let decoded: Fingerprint = serde_plain::from_str(&encoded).unwrap();
+        assert_eq!(decoded, original);
+    }
+
+    #[test]
+    fn deserialize_invalid_char() {
+        let s = "zz:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:10:20:30:40:50:60:70:80:90:a0:b0:c0:d0:e0:f0:01";
+        let parsed = serde_plain::from_str::<Fingerprint>(s);
+        assert!(parsed.is_err());
+    }
+
+    #[test]
+    fn deserialize_too_short() {
+        let s = "00:11:22:33";
+        let parsed = serde_plain::from_str::<Fingerprint>(s);
+        assert!(parsed.is_err());
+    }
+
+    #[test]
+    fn deserialize_too_long() {
+        let s = &"00:".repeat(33);
+        let parsed = serde_plain::from_str::<Fingerprint>(&s);
+        assert!(parsed.is_err());
     }
 }
-- 
2.47.3


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


                 reply	other threads:[~2025-09-26 11:08 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

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=20250926110842.216011-1-n.frey@proxmox.com \
    --to=n.frey@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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal