From 8c67640b425078ad6600380a3ca59a0a822f5aea Mon Sep 17 00:00:00 2001 From: Konrad Kohbrok Date: Fri, 27 Mar 2026 17:23:18 +0100 Subject: [PATCH] add transparent VLBytes version --- tls_codec/src/lib.rs | 2 +- tls_codec/src/quic_vec.rs | 140 ++++++++++++++++++++++++++++++++- tls_codec/tests/serde_impls.rs | 37 ++++++++- 3 files changed, 174 insertions(+), 5 deletions(-) diff --git a/tls_codec/src/lib.rs b/tls_codec/src/lib.rs index 3ee3a889e..ee9c31c1d 100644 --- a/tls_codec/src/lib.rs +++ b/tls_codec/src/lib.rs @@ -50,7 +50,7 @@ pub use tls_vec::{ #[cfg(feature = "std")] pub use quic_vec::{SecretVLBytes, rw as vlen}; -pub use quic_vec::{VLByteSlice, VLBytes}; +pub use quic_vec::{Bytes, VLByteSlice, VLBytes}; #[cfg(feature = "derive")] pub use tls_codec_derive::{ diff --git a/tls_codec/src/quic_vec.rs b/tls_codec/src/quic_vec.rs index 84b2bc43d..631a6f610 100644 --- a/tls_codec/src/quic_vec.rs +++ b/tls_codec/src/quic_vec.rs @@ -272,6 +272,65 @@ impl From for Vec { } } +/// Variable-length encoded byte vectors with transparent serde serialization. +/// +/// This is equivalent to [`VLBytes`] for TLS codec (de)serialization, but uses +/// `#[serde(transparent)]` so that formats like CBOR serialize the bytes +/// directly instead of wrapping them in a map with a field name. +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[derive(Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct Bytes( + #[cfg_attr(feature = "serde", serde(serialize_with = "serde_bytes::serialize"))] + #[cfg_attr( + feature = "serde", + serde(deserialize_with = "serde_impl::de_vec_bytes_compat") + )] + Vec, +); + +impl Bytes { + /// Generate a new variable-length byte vector. + pub fn new(vec: Vec) -> Self { + Self(vec) + } + + fn vec(&self) -> &[u8] { + &self.0 + } + + fn vec_mut(&mut self) -> &mut Vec { + &mut self.0 + } +} + +impl_vl_bytes_generic!(Bytes); + +#[cfg(feature = "std")] +impl Zeroize for Bytes { + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +impl From for Vec { + fn from(b: Bytes) -> Self { + b.0 + } +} + +impl From for Bytes { + fn from(b: VLBytes) -> Self { + Self(b.vec) + } +} + +impl From for VLBytes { + fn from(b: Bytes) -> Self { + Self { vec: b.0 } + } +} + #[inline(always)] fn tls_serialize_bytes_len(bytes: &[u8]) -> usize { let content_length = bytes.len(); @@ -327,9 +386,31 @@ impl Size for &VLBytes { } } +impl Size for Bytes { + #[inline(always)] + fn tls_serialized_len(&self) -> usize { + tls_serialize_bytes_len(self.as_slice()) + } +} + +impl DeserializeBytes for Bytes { + #[inline(always)] + fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error> { + let (vl, remainder) = VLBytes::tls_deserialize_bytes(bytes)?; + Ok((Self::from(vl), remainder)) + } +} + +impl Size for &Bytes { + #[inline(always)] + fn tls_serialized_len(&self) -> usize { + (*self).tls_serialized_len() + } +} + #[cfg(feature = "serde")] mod serde_impl { - use std::{fmt, vec::Vec}; + use std::{fmt, string::String, vec::Vec}; use serde::{Deserializer, de}; @@ -343,7 +424,7 @@ mod serde_impl { type Value = Vec; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("either a byte blob or a sequence of u8") + f.write_str("a byte blob, a sequence of u8, or a map with a \"vec\" key") } // New format (native bytes; e.g., CBOR/Bincode/Msgpack) @@ -372,6 +453,25 @@ mod serde_impl { } Ok(out) } + + // Legacy VLBytes format (map with "vec" key) + fn visit_map(self, mut map: A) -> Result + where + A: de::MapAccess<'de>, + { + let mut value: Option> = None; + while let Some(key) = map.next_key::()? { + if key == "vec" { + if value.is_some() { + return Err(de::Error::duplicate_field("vec")); + } + value = Some(map.next_value()?); + } else { + return Err(de::Error::unknown_field(&key, &["vec"])); + } + } + value.ok_or_else(|| de::Error::missing_field("vec")) + } } deserializer.deserialize_any(BytesOrSeq) @@ -573,6 +673,26 @@ mod rw_bytes { tls_serialize_bytes(writer, self.0) } } + + impl Serialize for Bytes { + #[inline(always)] + fn tls_serialize(&self, writer: &mut W) -> Result { + tls_serialize_bytes(writer, self.as_slice()) + } + } + + impl Serialize for &Bytes { + #[inline(always)] + fn tls_serialize(&self, writer: &mut W) -> Result { + (*self).tls_serialize(writer) + } + } + + impl Deserialize for Bytes { + fn tls_deserialize(bytes: &mut R) -> Result { + VLBytes::tls_deserialize(bytes).map(Self::from) + } + } } #[cfg(feature = "std")] @@ -668,10 +788,19 @@ impl<'a> Arbitrary<'a> for VLBytes { } } +#[cfg(feature = "arbitrary")] +impl<'a> Arbitrary<'a> for Bytes { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let mut vec = Vec::arbitrary(u)?; + vec.truncate(ContentLength::MAX as usize); + Ok(Self(vec)) + } +} + #[cfg(feature = "std")] #[cfg(test)] mod test { - use crate::{SecretVLBytes, VLByteSlice, VLBytes}; + use crate::{Bytes, SecretVLBytes, VLByteSlice, VLBytes}; use std::println; #[test] @@ -700,6 +829,11 @@ mod test { println!("{got}"); assert_eq!(expected_vl_bytes, got); + let expected_bytes = format!("Bytes {{ {expected} }}"); + let got = format!("{:?}", Bytes::new(test.clone())); + println!("{got}"); + assert_eq!(expected_bytes, got); + let expected_secret_vl_bytes = format!("SecretVLBytes {{ {expected} }}"); let got = format!("{:?}", SecretVLBytes::new(test.clone())); println!("{got}"); diff --git a/tls_codec/tests/serde_impls.rs b/tls_codec/tests/serde_impls.rs index c9292af2e..2b9567bcf 100644 --- a/tls_codec/tests/serde_impls.rs +++ b/tls_codec/tests/serde_impls.rs @@ -1,6 +1,6 @@ #![cfg(feature = "serde")] -use tls_codec::VLBytes; +use tls_codec::{Bytes, VLBytes}; // Old VLBytes without serde bytes serialization #[derive(serde::Serialize, serde::Deserialize)] @@ -33,3 +33,38 @@ fn serde_impls() { assert_eq!(deserialized, old_deserialized); } + +#[test] +fn bytes_is_transparent() { + let data = vec![32; 128]; + let bytes_value = Bytes::new(data.clone()); + let vlbytes_value = VLBytes::new(data); + + let mut bytes_serialized = Vec::new(); + ciborium::into_writer(&bytes_value, &mut bytes_serialized).unwrap(); + let mut vlbytes_serialized = Vec::new(); + ciborium::into_writer(&vlbytes_value, &mut vlbytes_serialized).unwrap(); + + // Bytes (transparent) should produce smaller output than VLBytes (has field name) + assert!(bytes_serialized.len() < vlbytes_serialized.len()); + + // Bytes should roundtrip + let deserialized: Bytes = ciborium::from_reader(bytes_serialized.as_slice()).unwrap(); + assert_eq!(deserialized, bytes_value); +} + +#[test] +fn bytes_vlbytes_cross_deserialization() { + let data = vec![42; 64]; + let bytes_value = Bytes::new(data.clone()); + let vlbytes_value = VLBytes::new(data); + + let mut bytes_serialized = Vec::new(); + ciborium::into_writer(&bytes_value, &mut bytes_serialized).unwrap(); + let mut vlbytes_serialized = Vec::new(); + ciborium::into_writer(&vlbytes_value, &mut vlbytes_serialized).unwrap(); + + // Bytes can deserialize VLBytes-serialized data + let from_vlbytes: Bytes = ciborium::from_reader(vlbytes_serialized.as_slice()).unwrap(); + assert_eq!(from_vlbytes, bytes_value); +}