diff --git a/source/postcard-dyn-ng/Cargo.toml b/source/postcard-dyn-ng/Cargo.toml index 85c5df41..db2560d6 100644 --- a/source/postcard-dyn-ng/Cargo.toml +++ b/source/postcard-dyn-ng/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "postcard-dyn-ng" -version = "0.3.0" -authors = ["James Munns "] +version = "0.4.0" +authors = ["James Munns "] edition = "2021" repository = "https://github.com/jamesmunns/postcard" description = "Dynamic ser/de for postcard" @@ -27,6 +27,6 @@ features = ["use-std"] path = "../postcard" [dependencies.postcard-schema-ng] -version = "0.3" +version = "0.4" features = ["use-std", "derive"] path = "../postcard-schema-ng" diff --git a/source/postcard-dyn-ng/src/de.rs b/source/postcard-dyn-ng/src/de.rs index cfd7be8c..01502e14 100644 --- a/source/postcard-dyn-ng/src/de.rs +++ b/source/postcard-dyn-ng/src/de.rs @@ -1,4 +1,4 @@ -use std::str::from_utf8; +use std::{ops::Deref, str::from_utf8}; use postcard_schema_ng::schema::owned::{OwnedData, OwnedDataModelType}; use serde_json::{Map, Number, Value}; @@ -139,14 +139,14 @@ fn deserialize<'a>(ty: &OwnedDataModelType, data: &'a [u8]) -> Result<(Value, &' Ok((val, rest)) } OwnedDataModelType::Char => todo!(), - OwnedDataModelType::String => { + OwnedDataModelType::String { .. } => { let (val, rest) = try_take_varint_usize(data)?; let (bytes, rest) = rest.take_n(val)?; let s = from_utf8(bytes).map_err(|_| Error::SchemaMismatch)?; let val = Value::String(s.to_string()); Ok((val, rest)) } - OwnedDataModelType::ByteArray => { + OwnedDataModelType::ByteArray { .. } => { let (val, rest) = try_take_varint_usize(data)?; let (bytes, rest) = rest.take_n(val)?; let vvec = bytes @@ -178,7 +178,7 @@ fn deserialize<'a>(ty: &OwnedDataModelType, data: &'a [u8]) -> Result<(Value, &' name: _, data: OwnedData::Newtype(ty), } => deserialize(ty, data), - OwnedDataModelType::Seq(ty) => { + OwnedDataModelType::Seq { element: ty, .. } => { let (val, mut rest) = try_take_varint_usize(data)?; let mut vec = vec![]; for _ in 0..val { @@ -214,12 +214,12 @@ fn deserialize<'a>(ty: &OwnedDataModelType, data: &'a [u8]) -> Result<(Value, &' } } } - OwnedDataModelType::Map { key, val } => { + OwnedDataModelType::Map { key, val, .. } => { // TODO: impling blind because we can't test this, oops // // TODO: There's also a mismatch here because serde_json::Value requires // keys to be strings, when postcard doesn't. - if **key != OwnedDataModelType::String { + if !matches!(key.deref(), &OwnedDataModelType::String { max_len: _ }) { return Err(Error::ShouldSupportButDont); } diff --git a/source/postcard-dyn-ng/src/ser.rs b/source/postcard-dyn-ng/src/ser.rs index ab99c975..7961a201 100644 --- a/source/postcard-dyn-ng/src/ser.rs +++ b/source/postcard-dyn-ng/src/ser.rs @@ -1,4 +1,4 @@ -use std::num::TryFromIntError; +use std::{num::TryFromIntError, ops::Deref}; use postcard_schema_ng::schema::owned::{OwnedData, OwnedDataModelType}; use serde_json::Value; @@ -162,7 +162,7 @@ fn ser_named_type(ty: &OwnedDataModelType, value: &Value, out: &mut Vec) -> let val = val.to_le_bytes(); out.extend_from_slice(&val); } - OwnedDataModelType::String | OwnedDataModelType::Char => { + OwnedDataModelType::String { .. } | OwnedDataModelType::Char => { let val = value.as_str().right()?; // First add len @@ -174,7 +174,7 @@ fn ser_named_type(ty: &OwnedDataModelType, value: &Value, out: &mut Vec) -> // Then add payload out.extend_from_slice(val.as_bytes()); } - OwnedDataModelType::ByteArray => { + OwnedDataModelType::ByteArray { .. } => { let val = value.as_array().right()?; // First add len @@ -209,7 +209,7 @@ fn ser_named_type(ty: &OwnedDataModelType, value: &Value, out: &mut Vec) -> } => { ser_named_type(ty, value, out)?; } - OwnedDataModelType::Seq(ty) => { + OwnedDataModelType::Seq { element: ty, .. } => { let val = value.as_array().right()?; // First add len @@ -243,12 +243,12 @@ fn ser_named_type(ty: &OwnedDataModelType, value: &Value, out: &mut Vec) -> ser_named_type(ty, val, out)?; } } - OwnedDataModelType::Map { key, val } => { + OwnedDataModelType::Map { key, val, .. } => { // TODO: impling blind because we can't test this, oops // // TODO: There's also a mismatch here because serde_json::Value requires // keys to be strings, when postcard doesn't. - if **key != OwnedDataModelType::String { + if !matches!(key.deref(), OwnedDataModelType::String { max_len: _ }) { return Err(Error::ShouldSupportButDont); } diff --git a/source/postcard-schema-ng/Cargo.toml b/source/postcard-schema-ng/Cargo.toml index ee857de1..c9127268 100644 --- a/source/postcard-schema-ng/Cargo.toml +++ b/source/postcard-schema-ng/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "postcard-schema-ng" -version = "0.3.2" -authors = ["James Munns "] +version = "0.4.0" +authors = ["James Munns "] edition = "2021" readme = "README.md" repository = "https://github.com/jamesmunns/postcard" @@ -14,7 +14,7 @@ categories = [ keywords = [ "serde", ] -documentation = "https://docs.rs/postcard-schema/" +documentation = "https://docs.rs/postcard-schema-ng/" [package.metadata.docs.rs] all-features = true @@ -22,6 +22,10 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] +[dependencies.postcard-core] +version = "0.1.0" +path = "../postcard-core" + [dependencies.serde] version = "1.0" default-features = false @@ -102,6 +106,18 @@ version = "0.33.0" default-features = false features = ["serde-serialize-no-std"] +[dev-dependencies.uuid_v1_0] +package = "uuid" +version = "1.0" +default-features = false +features = ["serde"] + +[dev-dependencies.chrono_v0_4] +package = "chrono" +version = "0.4" +default-features = false +features = ["serde", "clock"] + [features] default = [] use-std = ["serde/std"] diff --git a/source/postcard-schema-ng/src/impls/builtins_alloc.rs b/source/postcard-schema-ng/src/impls/builtins_alloc.rs index 64146554..3d79c7a4 100644 --- a/source/postcard-schema-ng/src/impls/builtins_alloc.rs +++ b/source/postcard-schema-ng/src/impls/builtins_alloc.rs @@ -6,12 +6,15 @@ extern crate alloc; #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] impl Schema for alloc::vec::Vec { - const SCHEMA: &'static DataModelType = &DataModelType::Seq(T::SCHEMA); + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: T::SCHEMA, + max_len: None, + }; } #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] impl Schema for alloc::string::String { - const SCHEMA: &'static DataModelType = &DataModelType::String; + const SCHEMA: &'static DataModelType = &DataModelType::String { max_len: None }; } #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] @@ -19,12 +22,16 @@ impl Schema for alloc::collections::BTreeMap { const SCHEMA: &'static DataModelType = &DataModelType::Map { key: K::SCHEMA, val: V::SCHEMA, + max_len: None, }; } #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] impl Schema for alloc::collections::BTreeSet { - const SCHEMA: &'static DataModelType = &DataModelType::Seq(K::SCHEMA); + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: K::SCHEMA, + max_len: None, + }; } #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] diff --git a/source/postcard-schema-ng/src/impls/builtins_maxlen.rs b/source/postcard-schema-ng/src/impls/builtins_maxlen.rs new file mode 100644 index 00000000..f515d24a --- /dev/null +++ b/source/postcard-schema-ng/src/impls/builtins_maxlen.rs @@ -0,0 +1,603 @@ +//! MaxLen collections +//! +//! These items have schemas that include bounded information. + +use crate::{schema::DataModelType, Schema}; +use core::ops::Deref; +use serde::{de::Visitor, Deserialize, Serialize}; + +/// Provided item exceeded the max len of this type +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct TooLong; + +// +// STR +// + +/// MaxLen version of `&str` +#[derive(PartialEq, Clone, Copy)] +pub struct MaxLenStr<'a, const N: usize> { + inner: &'a str, +} + +impl<'a, const N: usize> core::fmt::Debug for MaxLenStr<'a, N> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.inner.fmt(f) + } +} + +impl<'a, const N: usize> core::fmt::Display for MaxLenStr<'a, N> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.inner.fmt(f) + } +} + +impl<'a, const N: usize> From> for &'a str { + fn from(value: MaxLenStr<'a, N>) -> Self { + value.inner + } +} + +impl<'a, const N: usize> Deref for MaxLenStr<'a, N> { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + self.inner + } +} + +impl<'a, const N: usize> AsRef for MaxLenStr<'a, N> { + #[inline] + fn as_ref(&self) -> &str { + self.inner + } +} + +impl<'a, const N: usize> TryFrom<&'a str> for MaxLenStr<'a, N> { + type Error = TooLong; + + #[inline] + fn try_from(value: &'a str) -> Result { + if value.len() > N { + Err(TooLong) + } else { + Ok(Self { inner: value }) + } + } +} + +impl<'a, const N: usize> Serialize for MaxLenStr<'a, N> { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.inner) + } +} + +impl<'de, const N: usize> Deserialize<'de> for MaxLenStr<'de, N> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct StrVisitor; + impl<'de, const N: usize> Visitor<'de> for StrVisitor { + type Value = MaxLenStr<'de, N>; + + #[inline] + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(formatter, "a string with fewer than {N} chars") + } + + #[inline] + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + if let Ok(b) = MaxLenStr::<'de, N>::try_from(v) { + Ok(b) + } else { + Err(E::invalid_length(v.len(), &self)) + } + } + } + + deserializer.deserialize_str(StrVisitor::) + } +} + +impl<'a, const N: usize> Schema for MaxLenStr<'a, N> { + const SCHEMA: &'static DataModelType = &DataModelType::String { max_len: Some(N) }; +} + +// +// BYTES +// +/// MaxLen version of `&[u8]` +#[derive(PartialEq, Clone, Copy)] +pub struct MaxLenByteSlice<'a, const N: usize> { + inner: &'a [u8], +} + +impl<'a, const N: usize> core::fmt::Debug for MaxLenByteSlice<'a, N> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.inner.fmt(f) + } +} + +impl<'a, const N: usize> From> for &'a [u8] { + fn from(value: MaxLenByteSlice<'a, N>) -> Self { + value.inner + } +} + +impl<'a, const N: usize> Deref for MaxLenByteSlice<'a, N> { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.inner + } +} + +impl<'a, const N: usize> AsRef<[u8]> for MaxLenByteSlice<'a, N> { + #[inline] + fn as_ref(&self) -> &[u8] { + self.inner + } +} + +impl<'a, const N: usize> TryFrom<&'a [u8]> for MaxLenByteSlice<'a, N> { + type Error = TooLong; + + #[inline] + fn try_from(value: &'a [u8]) -> Result { + if value.len() > N { + Err(TooLong) + } else { + Ok(Self { inner: value }) + } + } +} + +impl<'a, const N: usize, const M: usize> TryFrom<&'a [u8; M]> for MaxLenByteSlice<'a, N> { + type Error = TooLong; + + #[inline] + fn try_from(value: &'a [u8; M]) -> Result { + if M > N { + Err(TooLong) + } else { + Ok(Self { + inner: value.as_slice(), + }) + } + } +} + +impl<'a, const N: usize> Serialize for MaxLenByteSlice<'a, N> { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(self.inner) + } +} + +impl<'de, const N: usize> Deserialize<'de> for MaxLenByteSlice<'de, N> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct BytesVisitor; + impl<'de, const N: usize> Visitor<'de> for BytesVisitor { + type Value = MaxLenByteSlice<'de, N>; + + #[inline] + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(formatter, "bytes with fewer than {N} items") + } + + #[inline] + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result + where + E: serde::de::Error, + { + if let Ok(b) = MaxLenByteSlice::<'de, N>::try_from(v) { + Ok(b) + } else { + Err(E::invalid_length(v.len(), &self)) + } + } + } + + deserializer.deserialize_bytes(BytesVisitor::) + } +} + +impl<'a, const N: usize> Schema for MaxLenByteSlice<'a, N> { + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: &DataModelType::U8, + max_len: Some(N), + }; +} + +#[cfg(feature = "use-std")] +pub(crate) mod std { + // + // STRING + // + + use core::ops::Deref; + + use serde::{de::Visitor, Deserialize, Serialize}; + + use crate::{max_len::TooLong, schema::DataModelType, Schema}; + + /// MaxLen version of `String` + #[derive(PartialEq, Clone)] + pub struct MaxLenString { + inner: String, + } + + impl core::fmt::Debug for MaxLenString { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.inner.fmt(f) + } + } + + impl core::fmt::Display for MaxLenString { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.inner.fmt(f) + } + } + + impl From> for String { + fn from(value: MaxLenString) -> Self { + value.inner + } + } + + impl Deref for MaxLenString { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } + } + // We don't implement DerefMut because we don't want people extending + // the string. + + impl AsRef for MaxLenString { + #[inline] + fn as_ref(&self) -> &str { + &self.inner + } + } + + impl TryFrom<&str> for MaxLenString { + type Error = TooLong; + + #[inline] + fn try_from(value: &str) -> Result { + if value.len() > N { + Err(TooLong) + } else { + Ok(Self { + inner: value.to_string(), + }) + } + } + } + + impl TryFrom for MaxLenString { + type Error = TooLong; + + #[inline] + fn try_from(value: String) -> Result { + if value.len() > N { + Err(TooLong) + } else { + Ok(Self { inner: value }) + } + } + } + + impl Serialize for MaxLenString { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.inner) + } + } + + impl<'de, const N: usize> Deserialize<'de> for MaxLenString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct StrVisitor; + impl<'de, const N: usize> Visitor<'de> for StrVisitor { + type Value = MaxLenString; + + #[inline] + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(formatter, "a string with fewer than {N} chars") + } + + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + let len = v.len(); + if let Ok(b) = MaxLenString::::try_from(v) { + Ok(b) + } else { + Err(E::invalid_length(len, &self)) + } + } + + #[inline] + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if let Ok(b) = MaxLenString::::try_from(v) { + Ok(b) + } else { + Err(E::invalid_length(v.len(), &self)) + } + } + } + + deserializer.deserialize_str(StrVisitor::) + } + } + + impl Schema for MaxLenString { + const SCHEMA: &'static DataModelType = &DataModelType::String { max_len: Some(N) }; + } + + // + // BYTES + // + /// MaxLen version of `Box<[u8]>` + #[derive(PartialEq, Clone)] + pub struct MaxLenBytes { + inner: Box<[u8]>, + } + + impl core::fmt::Debug for MaxLenBytes { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.inner.fmt(f) + } + } + + impl Deref for MaxLenBytes { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + impl AsRef<[u8]> for MaxLenBytes { + #[inline] + fn as_ref(&self) -> &[u8] { + &self.inner + } + } + + impl TryFrom<&[u8]> for MaxLenBytes { + type Error = TooLong; + + #[inline] + fn try_from(value: &[u8]) -> Result { + if value.len() > N { + Err(TooLong) + } else { + Ok(Self { + inner: value.into(), + }) + } + } + } + + impl TryFrom<&[u8; M]> for MaxLenBytes { + type Error = TooLong; + + #[inline] + fn try_from(value: &[u8; M]) -> Result { + if M > N { + Err(TooLong) + } else { + Ok(Self { + inner: value.as_slice().into(), + }) + } + } + } + + impl Serialize for MaxLenBytes { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(&self.inner) + } + } + + impl<'de, const N: usize> Deserialize<'de> for MaxLenBytes { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct BytesVisitor; + impl<'de, const N: usize> Visitor<'de> for BytesVisitor { + type Value = MaxLenBytes; + + #[inline] + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(formatter, "bytes with fewer than {N} items") + } + + #[inline] + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + if let Ok(b) = MaxLenBytes::::try_from(v) { + Ok(b) + } else { + Err(E::invalid_length(v.len(), &self)) + } + } + } + + deserializer.deserialize_bytes(BytesVisitor::) + } + } + + impl Schema for MaxLenBytes { + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: &DataModelType::U8, + max_len: Some(N), + }; + } + + #[cfg(test)] + mod test { + use postcard::Error::SerdeDeCustom; + + use crate::max_len::{MaxLenByteSlice, MaxLenStr}; + + #[test] + fn bounded_str() { + use super::*; + assert_eq!(MaxLenString::<8>::SCHEMA.max_size(), Some(9)); + assert_eq!(MaxLenString::<127>::SCHEMA.max_size(), Some(128)); + assert_eq!(MaxLenString::<128>::SCHEMA.max_size(), Some(130)); + + let b: MaxLenString<5> = "hello".try_into().unwrap(); + let x = postcard::to_stdvec(&b).unwrap(); + let base = postcard::to_stdvec("hello").unwrap(); + assert_eq!(x, base); + assert!(MaxLenString::<4>::try_from("hello").is_err()); + + let c = postcard::from_bytes::>(&x).unwrap(); + assert_eq!(b, c); + let c2 = postcard::from_bytes::>(&x).unwrap(); + assert_eq!(b.deref(), c2.deref()); + + let d = postcard::from_bytes::>(&x); + assert_eq!(d, Err(SerdeDeCustom)); + + assert_eq!( + >::SCHEMA, + &DataModelType::String { max_len: Some(5) } + ); + assert_eq!(>::SCHEMA, >::SCHEMA); + } + + #[test] + fn bounded_bytes() { + use super::*; + assert_eq!(MaxLenBytes::<8>::SCHEMA.max_size(), Some(9)); + assert_eq!(MaxLenBytes::<127>::SCHEMA.max_size(), Some(128)); + assert_eq!(MaxLenBytes::<128>::SCHEMA.max_size(), Some(130)); + + let _b: MaxLenBytes<6> = b"hello".try_into().unwrap(); + let b: MaxLenBytes<5> = b"hello".try_into().unwrap(); + assert_eq!(b.get(..3), Some(b"hel".as_slice())); + let x = postcard::to_stdvec(&b).unwrap(); + let base = postcard::to_stdvec(b"hello".as_slice()).unwrap(); + assert_eq!(x, base); + assert!(MaxLenBytes::<4>::try_from(b"hello").is_err()); + + let c = postcard::from_bytes::>(&x).unwrap(); + assert_eq!(b, c); + let c2 = postcard::from_bytes::>(&x).unwrap(); + assert_eq!(b.deref(), c2.deref()); + + let d = postcard::from_bytes::>(&x); + assert_eq!(d, Err(SerdeDeCustom)); + + assert_eq!( + >::SCHEMA, + &DataModelType::Seq { + element: &DataModelType::U8, + max_len: Some(5), + } + ); + assert_eq!(>::SCHEMA, >::SCHEMA); + } + } +} + +#[cfg(test)] +mod test { + use postcard::Error::SerdeDeCustom; + + #[test] + fn bounded_str() { + use super::*; + assert_eq!(MaxLenStr::<8>::SCHEMA.max_size(), Some(9)); + assert_eq!(MaxLenStr::<127>::SCHEMA.max_size(), Some(128)); + assert_eq!(MaxLenStr::<128>::SCHEMA.max_size(), Some(130)); + + let b: MaxLenStr<'_, 5> = "hello".try_into().unwrap(); + let x = postcard::to_stdvec(&b).unwrap(); + let base = postcard::to_stdvec("hello").unwrap(); + assert_eq!(x, base); + assert!(MaxLenStr::<4>::try_from("hello").is_err()); + + let c = postcard::from_bytes::>(&x).unwrap(); + assert_eq!(b, c); + + let d = postcard::from_bytes::>(&x); + assert_eq!(d, Err(SerdeDeCustom)); + + assert_eq!( + >::SCHEMA, + &DataModelType::String { max_len: Some(5) } + ); + } + + #[test] + fn bounded_bytes() { + use super::*; + assert_eq!(MaxLenByteSlice::<8>::SCHEMA.max_size(), Some(9)); + assert_eq!(MaxLenByteSlice::<127>::SCHEMA.max_size(), Some(128)); + assert_eq!(MaxLenByteSlice::<128>::SCHEMA.max_size(), Some(130)); + + let _b: MaxLenByteSlice<'_, 6> = b"hello".try_into().unwrap(); + let b: MaxLenByteSlice<'_, 5> = b"hello".try_into().unwrap(); + assert_eq!(b.get(..3), Some(b"hel".as_slice())); + let x = postcard::to_stdvec(&b).unwrap(); + let base = postcard::to_stdvec(b"hello".as_slice()).unwrap(); + assert_eq!(x, base); + assert!(MaxLenByteSlice::<4>::try_from(b"hello").is_err()); + + let c = postcard::from_bytes::>(&x).unwrap(); + assert_eq!(b, c); + + let d = postcard::from_bytes::>(&x); + assert_eq!(d, Err(SerdeDeCustom)); + + assert_eq!( + >::SCHEMA, + &DataModelType::Seq { + element: &DataModelType::U8, + max_len: Some(5), + } + ); + } +} diff --git a/source/postcard-schema-ng/src/impls/builtins_nostd.rs b/source/postcard-schema-ng/src/impls/builtins_nostd.rs index 5180f034..d5761bee 100644 --- a/source/postcard-schema-ng/src/impls/builtins_nostd.rs +++ b/source/postcard-schema-ng/src/impls/builtins_nostd.rs @@ -43,7 +43,7 @@ impl_schema![ f32: DataModelType::F32, f64: DataModelType::F64, char: DataModelType::Char, - str: DataModelType::String, + str: DataModelType::String { max_len: None }, (): DataModelType::Unit, i16: DataModelType::I16, i32: DataModelType::I32, i64: DataModelType::I64, i128: DataModelType::I128, u16: DataModelType::U16, u32: DataModelType::U32, u64: DataModelType::U64, u128: DataModelType::U128, @@ -87,7 +87,10 @@ impl Schema for &'_ T { } impl Schema for [T] { - const SCHEMA: &'static DataModelType = &DataModelType::Seq(T::SCHEMA); + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: T::SCHEMA, + max_len: None, + }; } impl Schema for [T; N] { @@ -278,5 +281,5 @@ impl Schema for PhantomData { } impl Schema for Arguments<'_> { - const SCHEMA: &'static crate::schema::DataModelType = &DataModelType::String; + const SCHEMA: &'static crate::schema::DataModelType = &DataModelType::String { max_len: None }; } diff --git a/source/postcard-schema-ng/src/impls/builtins_std.rs b/source/postcard-schema-ng/src/impls/builtins_std.rs index 9bb85c66..d9ab5b89 100644 --- a/source/postcard-schema-ng/src/impls/builtins_std.rs +++ b/source/postcard-schema-ng/src/impls/builtins_std.rs @@ -4,17 +4,20 @@ use crate::{schema::DataModelType, Schema}; #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] impl Schema for std::vec::Vec { - const SCHEMA: &'static DataModelType = &DataModelType::Seq(T::SCHEMA); + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: T::SCHEMA, + max_len: None, + }; } #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] impl Schema for std::string::String { - const SCHEMA: &'static DataModelType = &DataModelType::String; + const SCHEMA: &'static DataModelType = &DataModelType::String { max_len: None }; } #[cfg_attr(docsrs, doc(cfg(feature = "use-std")))] impl Schema for std::path::PathBuf { - const SCHEMA: &'static DataModelType = &DataModelType::String; + const SCHEMA: &'static DataModelType = &DataModelType::String { max_len: None }; } #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] @@ -22,6 +25,7 @@ impl Schema for std::collections::HashMap { const SCHEMA: &'static DataModelType = &DataModelType::Map { key: K::SCHEMA, val: V::SCHEMA, + max_len: None, }; } @@ -30,17 +34,24 @@ impl Schema for std::collections::BTreeMap { const SCHEMA: &'static DataModelType = &DataModelType::Map { key: K::SCHEMA, val: V::SCHEMA, + max_len: None, }; } #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] impl Schema for std::collections::HashSet { - const SCHEMA: &'static DataModelType = &DataModelType::Seq(K::SCHEMA); + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: K::SCHEMA, + max_len: None, + }; } #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] impl Schema for std::collections::BTreeSet { - const SCHEMA: &'static DataModelType = &DataModelType::Seq(K::SCHEMA); + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: K::SCHEMA, + max_len: None, + }; } #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "use-std"))))] diff --git a/source/postcard-schema-ng/src/impls/chrono_v0_4.rs b/source/postcard-schema-ng/src/impls/chrono_v0_4.rs index 1e206a7f..ee8e8a76 100644 --- a/source/postcard-schema-ng/src/impls/chrono_v0_4.rs +++ b/source/postcard-schema-ng/src/impls/chrono_v0_4.rs @@ -4,5 +4,56 @@ use crate::{schema::DataModelType, Schema}; #[cfg_attr(docsrs, doc(cfg(feature = "chrono-v0_4")))] impl Schema for chrono_v0_4::DateTime { - const SCHEMA: &'static DataModelType = &DataModelType::String; + // Chrono serializes as an rfc3339 string, which is typically about 27 bytes: + // + // "2026-09-06T13:24:04.659820Z" + // 000000000011111111112222222 + // 012345678901234567890123456 + // + // But you can also make it spit out somewhat silly output with custom precision + // and time zones, see `longest_rfc3339` below. If you can make chrono output a + // longer serialized output, please open an issue. + // + // "+262142-12-31T23:59:59.999999999+23:59" + // 00000000001111111111222222222233333333 + // 01234567890123456789012345678901234567 + const SCHEMA: &'static DataModelType = &DataModelType::String { max_len: Some(38) }; +} + +#[cfg(test)] +mod test { + use crate::Schema; + use chrono_v0_4::{DateTime, FixedOffset, NaiveDate, TimeZone, Utc}; + + fn longest_rfc3339() -> DateTime { + let offset = FixedOffset::east_opt(23 * 3600 + 59 * 60).unwrap(); + let local = NaiveDate::MAX + .and_hms_nano_opt(23, 59, 59, 999_999_999) + .unwrap(); + offset.from_local_datetime(&local).single().unwrap() + } + + #[test] + fn chrono_longest_check() { + let dt = longest_rfc3339(); + let mut buf = [0u8; 64]; + let used = postcard::to_slice(&dt, &mut buf).unwrap(); + assert_eq!( + used.len(), + DateTime::::SCHEMA.max_size().unwrap() + ); + assert_eq!( + postcard::from_bytes::>(used).unwrap(), + dt + ); + } + + #[test] + fn chrono_basic_check() { + let dt = Utc::now(); + let mut buf = [0u8; 64]; + let used = postcard::to_slice(&dt, &mut buf).unwrap(); + assert!(used.len() <= DateTime::::SCHEMA.max_size().unwrap()); + assert_eq!(postcard::from_bytes::>(used).unwrap(), dt); + } } diff --git a/source/postcard-schema-ng/src/impls/heapless_v0_7.rs b/source/postcard-schema-ng/src/impls/heapless_v0_7.rs index d08a4913..8b190e14 100644 --- a/source/postcard-schema-ng/src/impls/heapless_v0_7.rs +++ b/source/postcard-schema-ng/src/impls/heapless_v0_7.rs @@ -4,10 +4,13 @@ use crate::{schema::DataModelType, Schema}; #[cfg_attr(docsrs, doc(cfg(feature = "heapless-v0_7")))] impl Schema for heapless_v0_7::Vec { - const SCHEMA: &'static DataModelType = &DataModelType::Seq(T::SCHEMA); + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: T::SCHEMA, + max_len: Some(N), + }; } #[cfg_attr(docsrs, doc(cfg(feature = "heapless-v0_7")))] impl Schema for heapless_v0_7::String { - const SCHEMA: &'static DataModelType = &DataModelType::String; + const SCHEMA: &'static DataModelType = &DataModelType::String { max_len: Some(N) }; } diff --git a/source/postcard-schema-ng/src/impls/heapless_v0_8.rs b/source/postcard-schema-ng/src/impls/heapless_v0_8.rs index 85ed3d43..3c5ceaa9 100644 --- a/source/postcard-schema-ng/src/impls/heapless_v0_8.rs +++ b/source/postcard-schema-ng/src/impls/heapless_v0_8.rs @@ -4,10 +4,13 @@ use crate::{schema::DataModelType, Schema}; #[cfg_attr(docsrs, doc(cfg(feature = "heapless-v0_8")))] impl Schema for heapless_v0_8::Vec { - const SCHEMA: &'static DataModelType = &DataModelType::Seq(T::SCHEMA); + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: T::SCHEMA, + max_len: Some(N), + }; } #[cfg_attr(docsrs, doc(cfg(feature = "heapless-v0_8")))] impl Schema for heapless_v0_8::String { - const SCHEMA: &'static DataModelType = &DataModelType::String; + const SCHEMA: &'static DataModelType = &DataModelType::String { max_len: Some(N) }; } diff --git a/source/postcard-schema-ng/src/impls/heapless_v0_9.rs b/source/postcard-schema-ng/src/impls/heapless_v0_9.rs index 68790cb5..10212edd 100644 --- a/source/postcard-schema-ng/src/impls/heapless_v0_9.rs +++ b/source/postcard-schema-ng/src/impls/heapless_v0_9.rs @@ -4,10 +4,13 @@ use crate::{schema::DataModelType, Schema}; #[cfg_attr(docsrs, doc(cfg(feature = "heapless-v0_9")))] impl Schema for heapless_v0_9::Vec { - const SCHEMA: &'static DataModelType = &DataModelType::Seq(T::SCHEMA); + const SCHEMA: &'static DataModelType = &DataModelType::Seq { + element: T::SCHEMA, + max_len: Some(N), + }; } #[cfg_attr(docsrs, doc(cfg(feature = "heapless-v0_9")))] impl Schema for heapless_v0_9::String { - const SCHEMA: &'static DataModelType = &DataModelType::String; + const SCHEMA: &'static DataModelType = &DataModelType::String { max_len: Some(N) }; } diff --git a/source/postcard-schema-ng/src/impls/mod.rs b/source/postcard-schema-ng/src/impls/mod.rs index 49471f14..b8d12bc5 100644 --- a/source/postcard-schema-ng/src/impls/mod.rs +++ b/source/postcard-schema-ng/src/impls/mod.rs @@ -4,6 +4,7 @@ use crate::{schema::DataModelType, Schema}; +pub(crate) mod builtins_maxlen; pub mod builtins_nostd; #[cfg(all(not(feature = "use-std"), feature = "alloc"))] diff --git a/source/postcard-schema-ng/src/impls/uuid_v1_0.rs b/source/postcard-schema-ng/src/impls/uuid_v1_0.rs index f8d3baa0..39449690 100644 --- a/source/postcard-schema-ng/src/impls/uuid_v1_0.rs +++ b/source/postcard-schema-ng/src/impls/uuid_v1_0.rs @@ -3,5 +3,17 @@ use crate::{schema::DataModelType, Schema}; impl Schema for uuid_v1_0::Uuid { - const SCHEMA: &'static DataModelType = &DataModelType::ByteArray; + const SCHEMA: &'static DataModelType = &DataModelType::ByteArray { max_len: Some(16) }; +} + +#[cfg(test)] +mod test { + use crate::Schema; + #[test] + fn basic() { + let uuid = uuid_v1_0::uuid!("21E764AB-6B44-42D3-BE73-682E5E85C196"); + let mut buf = [0u8; 17]; + let res = postcard::to_slice(&uuid, &mut buf).unwrap(); + assert_eq!(res.len(), uuid_v1_0::Uuid::SCHEMA.max_size().unwrap()); + } } diff --git a/source/postcard-schema-ng/src/key/hash.rs b/source/postcard-schema-ng/src/key/hash.rs index dec2a53f..aa87735f 100644 --- a/source/postcard-schema-ng/src/key/hash.rs +++ b/source/postcard-schema-ng/src/key/hash.rs @@ -15,6 +15,40 @@ pub struct Fnv1a64Hasher { state: u64, } +/// Options for const hashing +/// +/// All options start as disabled. The default value can be obtained either +/// using `HashOptions::DEFAULT` (in const context), or `HashOptions::default()` +/// (in non-const context) +#[non_exhaustive] +#[derive(Debug, Copy, Clone)] +pub struct HashOptions { + /// Include the type's max serialized size as part of the hash + pub hash_max_size: bool, +} + +impl HashOptions { + /// The default (all options off) value for use in const context + pub const DEFAULT: Self = HashOptions { + hash_max_size: false, + }; +} + +impl Default for HashOptions { + fn default() -> Self { + Self::DEFAULT + } +} + +/// A hashable item, used for hashing a series of items together at once +#[non_exhaustive] +pub enum HashItem<'a> { + /// A type's schema + Schema(&'a DataModelType), + /// A string + String(&'a str), +} + impl Fnv1a64Hasher { // source: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function const BASIS: u64 = 0xcbf2_9ce4_8422_2325; @@ -58,11 +92,41 @@ pub mod fnv1a64 { use super::*; + /// Hash an arbitrary sequence of items in the order provided + pub const fn hasher(opt: &HashOptions, items: &[HashItem<'_>]) -> [u8; 8] { + let mut state = Fnv1a64Hasher::BASIS; + let mut idx = 0; + while idx < items.len() { + state = match items[idx] { + HashItem::Schema(sdmty) => hash_sdm_type(opt, state, sdmty), + HashItem::String(s) => hash_update_str(state, s), + }; + idx += 1; + } + state.to_le_bytes() + } + /// Calculate the Key hash for the given path and type T pub const fn hash_ty_path(path: &str) -> [u8; 8] { - let schema = T::SCHEMA; - let state = hash_update_str(Fnv1a64Hasher::BASIS, path); - hash_sdm_type(state, schema).to_le_bytes() + let items = [HashItem::String(path), HashItem::Schema(T::SCHEMA)]; + hasher(&HashOptions::DEFAULT, &items) + } + + pub(crate) const fn hash_max_len(state: u64, max_len: Option) -> u64 { + let mut state = hash_update(state, &[0x2B]); + let Some(bound) = max_len else { + return state; + }; + let mut value = bound; + loop { + let byte = value.to_le_bytes()[0]; + if value < 128 { + return hash_update(state, &[byte]); + } + + state = hash_update(state, &[byte | 0x80]); + value >>= 7; + } } pub(crate) const fn hash_update(mut state: u64, bytes: &[u8]) -> u64 { @@ -80,7 +144,7 @@ pub mod fnv1a64 { hash_update(state, s.as_bytes()) } - const fn hash_sdm_type(state: u64, sdmty: &'static DataModelType) -> u64 { + const fn hash_sdm_type(opt: &HashOptions, state: u64, sdmty: &DataModelType) -> u64 { // The actual values we use here don't matter that much (as far as I know), // as long as the values for each variant are unique. I am unsure of the // implications of doing a TON of single byte calls to `update`, it may be @@ -110,6 +174,8 @@ pub mod fnv1a64 { // 0x1F, 0xA3, 0x35, 0x43, 0x89, 0x49, 0xE3, 0x07, // 0x53, 0xF1, 0x17, 0x2F, 0x29, 0x59, // ]; + // + // 0x2B reserved for max len match sdmty { DataModelType::Bool => hash_update(state, &[0x11]), DataModelType::I8 => hash_update(state, &[0xC5]), @@ -127,37 +193,63 @@ pub mod fnv1a64 { DataModelType::F32 => hash_update(state, &[0xEF]), DataModelType::F64 => hash_update(state, &[0x71]), DataModelType::Char => hash_update(state, &[0xC1]), - DataModelType::String => hash_update(state, &[0x25]), - DataModelType::ByteArray => hash_update(state, &[0x65]), + DataModelType::String { max_len } => { + let mut state = hash_update(state, &[0x25]); + if opt.hash_max_size { + state = hash_max_len(state, *max_len); + } + state + } + DataModelType::ByteArray { max_len } => { + // This is an identical hash to `Seq(u8)` for compatibility reasons, + // in postcard these types are equivalent. + let mut state = hash_update(state, &[0x03]); + state = hash_sdm_type(opt, state, u8::SCHEMA); + if opt.hash_max_size { + state = hash_max_len(state, *max_len); + } + state + } DataModelType::Option(t) => { let state = hash_update(state, &[0x6D]); - hash_sdm_type(state, t) + hash_sdm_type(opt, state, t) } DataModelType::Unit => hash_update(state, &[0x47]), - DataModelType::Seq(t) => { - let state = hash_update(state, &[0x03]); - hash_sdm_type(state, t) + DataModelType::Seq { + element: t, + max_len, + } => { + let mut state = hash_update(state, &[0x03]); + state = hash_sdm_type(opt, state, t); + if opt.hash_max_size { + state = hash_max_len(state, *max_len); + } + state } DataModelType::Tuple(ts) => { let mut state = hash_update(state, &[0xA7]); let mut idx = 0; while idx < ts.len() { - state = hash_sdm_type(state, ts[idx]); + state = hash_sdm_type(opt, state, ts[idx]); idx += 1; } state } - DataModelType::Map { key, val } => { - let state = hash_update(state, &[0x4F]); - let state = hash_sdm_type(state, key); - hash_sdm_type(state, val) + DataModelType::Map { key, val, max_len } => { + let mut state = hash_update(state, &[0x4F]); + state = hash_sdm_type(opt, state, key); + state = hash_sdm_type(opt, state, val); + if opt.hash_max_size { + state = hash_max_len(state, *max_len); + } + state } - DataModelType::Struct { name, data } => hash_struct(state, name, data), + DataModelType::Struct { name: _, data } => hash_struct(opt, state, data), DataModelType::Enum { name: _, variants } => { let mut state = hash_update(state, &[0xE9]); let mut idx = 0; while idx < variants.len() { - state = hash_variant(state, variants[idx]); + state = hash_variant(opt, state, variants[idx]); idx += 1; } state @@ -166,24 +258,22 @@ pub mod fnv1a64 { } } - const fn hash_struct(state: u64, _name: &str, data: &Data) -> u64 { + const fn hash_struct(opt: &HashOptions, state: u64, data: &Data) -> u64 { // NOTE: We do *not* hash the name of the type in hashv2. This // is to allow "safe" type punning, e.g. treating `Vec` and // `&[u8]` as compatible types, when talking between std and no-std // targets - // - // let state = hash_update(state, name.as_bytes()); match data { Data::Unit => hash_update(state, &[0xBF]), Data::Newtype(dmt) => { let state = hash_update(state, &[0x9D]); - hash_sdm_type(state, dmt) + hash_sdm_type(opt, state, dmt) } Data::Tuple(dmts) => { let mut state = hash_update(state, &[0x05]); let mut idx = 0; while idx < dmts.len() { - state = hash_sdm_type(state, dmts[idx]); + state = hash_sdm_type(opt, state, dmts[idx]); idx += 1; } state @@ -192,7 +282,7 @@ pub mod fnv1a64 { let mut state = hash_update(state, &[0x7F]); let mut idx = 0; while idx < nfs.len() { - state = hash_named_field(state, nfs[idx]); + state = hash_named_field(opt, state, nfs[idx]); idx += 1; } state @@ -200,19 +290,19 @@ pub mod fnv1a64 { } } - const fn hash_variant(state: u64, nt: &Variant) -> u64 { + const fn hash_variant(opt: &HashOptions, state: u64, nt: &Variant) -> u64 { let state = hash_update(state, nt.name.as_bytes()); match nt.data { Data::Unit => hash_update(state, &[0xB5]), Data::Newtype(t) => { let state = hash_update(state, &[0xDF]); - hash_sdm_type(state, t) + hash_sdm_type(opt, state, t) } Data::Tuple(ts) => { let mut state = hash_update(state, &[0xC7]); let mut idx = 0; while idx < ts.len() { - state = hash_sdm_type(state, ts[idx]); + state = hash_sdm_type(opt, state, ts[idx]); idx += 1; } state @@ -221,7 +311,7 @@ pub mod fnv1a64 { let mut state = hash_update(state, &[0x67]); let mut idx = 0; while idx < fields.len() { - state = hash_named_field(state, fields[idx]); + state = hash_named_field(opt, state, fields[idx]); idx += 1; } state @@ -229,9 +319,9 @@ pub mod fnv1a64 { } } - const fn hash_named_field(state: u64, nt: &NamedField) -> u64 { + const fn hash_named_field(opt: &HashOptions, state: u64, nt: &NamedField) -> u64 { let state = hash_update(state, nt.name.as_bytes()); - hash_sdm_type(state, nt.ty) + hash_sdm_type(opt, state, nt.ty) } } @@ -244,13 +334,42 @@ pub mod fnv1a64_owned { use super::fnv1a64::*; use super::*; + /// A hashable item, used for hashing a series of items together at once + #[non_exhaustive] + pub enum OwnedHashItem<'a> { + /// A type's schema + Schema(&'a OwnedDataModelType), + /// A string + String(&'a str), + } + + /// Hash an arbitrary sequence of items in the order provided + pub fn hasher(opt: &HashOptions, items: &[OwnedHashItem<'_>]) -> [u8; 8] { + let mut state = Fnv1a64Hasher::BASIS; + let mut idx = 0; + while idx < items.len() { + state = match items[idx] { + OwnedHashItem::Schema(sdmty) => hash_sdm_type_owned(opt, state, sdmty), + OwnedHashItem::String(s) => hash_update_str(state, s), + }; + idx += 1; + } + state.to_le_bytes() + } + + /// Calculate the Key hash for the given path and type T + pub fn hash_ty_path(path: &str, ty: &OwnedDataModelType) -> [u8; 8] { + let items = [OwnedHashItem::String(path), OwnedHashItem::Schema(ty)]; + hasher(&HashOptions::DEFAULT, &items) + } + /// Calculate the Key hash for the given path and [`OwnedDataModelType`] pub fn hash_ty_path_owned(path: &str, ty: &OwnedDataModelType) -> [u8; 8] { let state = hash_update_str(Fnv1a64Hasher::BASIS, path); - hash_sdm_type_owned(state, ty).to_le_bytes() + hash_sdm_type_owned(&HashOptions::default(), state, ty).to_le_bytes() } - const fn hash_sdm_type_owned(state: u64, sdmty: &OwnedDataModelType) -> u64 { + fn hash_sdm_type_owned(opt: &HashOptions, state: u64, sdmty: &OwnedDataModelType) -> u64 { // The actual values we use here don't matter that much (as far as I know), // as long as the values for each variant are unique. I am unsure of the // implications of doing a TON of single byte calls to `update`, it may be @@ -280,6 +399,8 @@ pub mod fnv1a64_owned { // 0x1F, 0xA3, 0x35, 0x43, 0x89, 0x49, 0xE3, 0x07, // 0x53, 0xF1, 0x17, 0x2F, 0x29, 0x59, // ]; + // + // 0x2B reserved for max_len match sdmty { OwnedDataModelType::Bool => hash_update(state, &[0x11]), OwnedDataModelType::I8 => hash_update(state, &[0xC5]), @@ -297,37 +418,64 @@ pub mod fnv1a64_owned { OwnedDataModelType::F32 => hash_update(state, &[0xEF]), OwnedDataModelType::F64 => hash_update(state, &[0x71]), OwnedDataModelType::Char => hash_update(state, &[0xC1]), - OwnedDataModelType::String => hash_update(state, &[0x25]), - OwnedDataModelType::ByteArray => hash_update(state, &[0x65]), + OwnedDataModelType::String { max_len } => { + let mut state = hash_update(state, &[0x25]); + if opt.hash_max_size { + state = hash_max_len(state, *max_len); + } + state + } + OwnedDataModelType::ByteArray { max_len } => { + // This is an identical hash to `Seq(u8)` for compatibility reasons, + // in postcard these types are equivalent. + let mut state = hash_update(state, &[0x03]); + let schema = u8::SCHEMA.into(); + state = hash_sdm_type_owned(opt, state, &schema); + if opt.hash_max_size { + state = hash_max_len(state, *max_len); + } + state + } OwnedDataModelType::Option(t) => { let state = hash_update(state, &[0x6D]); - hash_sdm_type_owned(state, t) + hash_sdm_type_owned(opt, state, t) } OwnedDataModelType::Unit => hash_update(state, &[0x47]), - OwnedDataModelType::Seq(t) => { - let state = hash_update(state, &[0x03]); - hash_sdm_type_owned(state, t) + OwnedDataModelType::Seq { + element: t, + max_len, + } => { + let mut state = hash_update(state, &[0x03]); + state = hash_sdm_type_owned(opt, state, t); + if opt.hash_max_size { + state = hash_max_len(state, *max_len); + } + state } OwnedDataModelType::Tuple(ts) => { let mut state = hash_update(state, &[0xA7]); let mut idx = 0; while idx < ts.len() { - state = hash_sdm_type_owned(state, &ts[idx]); + state = hash_sdm_type_owned(opt, state, &ts[idx]); idx += 1; } state } - OwnedDataModelType::Map { key, val } => { - let state = hash_update(state, &[0x4F]); - let state = hash_sdm_type_owned(state, key); - hash_sdm_type_owned(state, val) + OwnedDataModelType::Map { key, val, max_len } => { + let mut state = hash_update(state, &[0x4F]); + state = hash_sdm_type_owned(opt, state, key); + state = hash_sdm_type_owned(opt, state, val); + if opt.hash_max_size { + state = hash_max_len(state, *max_len); + } + state } - OwnedDataModelType::Struct { name, data } => hash_struct(state, name, data), + OwnedDataModelType::Struct { name: _, data } => hash_struct(opt, state, data), OwnedDataModelType::Enum { name: _, variants } => { let mut state = hash_update(state, &[0xE9]); let mut idx = 0; while idx < variants.len() { - state = hash_variant(state, &variants[idx]); + state = hash_variant(opt, state, &variants[idx]); idx += 1; } state @@ -336,24 +484,22 @@ pub mod fnv1a64_owned { } } - const fn hash_struct(state: u64, _name: &str, data: &OwnedData) -> u64 { + fn hash_struct(opt: &HashOptions, state: u64, data: &OwnedData) -> u64 { // NOTE: We do *not* hash the name of the type in hashv2. This // is to allow "safe" type punning, e.g. treating `Vec` and // `&[u8]` as compatible types, when talking between std and no-std // targets - // - // let state = hash_update(state, name.as_bytes()); match data { OwnedData::Unit => hash_update(state, &[0xBF]), OwnedData::Newtype(dmt) => { let state = hash_update(state, &[0x9D]); - hash_sdm_type_owned(state, dmt) + hash_sdm_type_owned(opt, state, dmt) } OwnedData::Tuple(dmts) => { let mut state = hash_update(state, &[0x05]); let mut idx = 0; while idx < dmts.len() { - state = hash_sdm_type_owned(state, &dmts[idx]); + state = hash_sdm_type_owned(opt, state, &dmts[idx]); idx += 1; } state @@ -362,7 +508,7 @@ pub mod fnv1a64_owned { let mut state = hash_update(state, &[0x7F]); let mut idx = 0; while idx < nfs.len() { - state = hash_named_field(state, &nfs[idx]); + state = hash_named_field(opt, state, &nfs[idx]); idx += 1; } state @@ -370,19 +516,19 @@ pub mod fnv1a64_owned { } } - const fn hash_variant(state: u64, nt: &OwnedVariant) -> u64 { + fn hash_variant(opt: &HashOptions, state: u64, nt: &OwnedVariant) -> u64 { let state = hash_update(state, nt.name.as_bytes()); match &nt.data { OwnedData::Unit => hash_update(state, &[0xB5]), OwnedData::Newtype(t) => { let state = hash_update(state, &[0xDF]); - hash_sdm_type_owned(state, t) + hash_sdm_type_owned(opt, state, t) } OwnedData::Tuple(ts) => { let mut state = hash_update(state, &[0xC7]); let mut idx = 0; while idx < ts.len() { - state = hash_sdm_type_owned(state, &ts[idx]); + state = hash_sdm_type_owned(opt, state, &ts[idx]); idx += 1; } state @@ -391,7 +537,7 @@ pub mod fnv1a64_owned { let mut state = hash_update(state, &[0x67]); let mut idx = 0; while idx < fields.len() { - state = hash_named_field(state, &fields[idx]); + state = hash_named_field(opt, state, &fields[idx]); idx += 1; } state @@ -399,15 +545,19 @@ pub mod fnv1a64_owned { } } - const fn hash_named_field(state: u64, nt: &OwnedNamedField) -> u64 { + fn hash_named_field(opt: &HashOptions, state: u64, nt: &OwnedNamedField) -> u64 { let state = hash_update(state, nt.name.as_bytes()); - hash_sdm_type_owned(state, &nt.ty) + hash_sdm_type_owned(opt, state, &nt.ty) } } #[cfg(test)] mod test { - use postcard_derive_ng::Schema; + use crate::{ + key::hash::{fnv1a64::hasher, HashItem, HashOptions}, + max_len::MaxLenString, + Schema, + }; use super::fnv1a64::hash_ty_path; @@ -429,10 +579,94 @@ mod test { B(Foo), } + #[derive(Schema)] + #[postcard(crate = crate)] + struct Foo2 { + a: u32, + b: MaxLenString, + } + + #[derive(Schema)] + #[postcard(crate = crate)] + enum Bar2 { + A, + B(Foo2), + } + + let default_opts = HashOptions::default(); + let max_opts = HashOptions { + hash_max_size: true, + ..HashOptions::default() + }; + assert_eq!( hash_ty_path::("test_path"), [139, 128, 52, 27, 107, 8, 218, 98] ); + assert_eq!( + hasher( + &default_opts, + &[HashItem::String("test_path"), HashItem::Schema(Bar::SCHEMA)] + ), + [139, 128, 52, 27, 107, 8, 218, 98] + ); + assert_eq!( + hasher( + &max_opts, + &[HashItem::String("test_path"), HashItem::Schema(Bar::SCHEMA)] + ), + [224, 143, 54, 58, 255, 237, 252, 44] + ); + + assert_eq!( + hash_ty_path::>("test_path"), + [139, 128, 52, 27, 107, 8, 218, 98] + ); + assert_eq!( + hasher( + &default_opts, + &[ + HashItem::String("test_path"), + HashItem::Schema(Bar2::<32>::SCHEMA) + ] + ), + [139, 128, 52, 27, 107, 8, 218, 98] + ); + assert_eq!( + hasher( + &max_opts, + &[ + HashItem::String("test_path"), + HashItem::Schema(Bar2::<32>::SCHEMA) + ] + ), + [64, 67, 182, 234, 175, 40, 88, 168] + ); + + assert_eq!( + hash_ty_path::>("test_path"), + [139, 128, 52, 27, 107, 8, 218, 98] + ); + assert_eq!( + hasher( + &default_opts, + &[ + HashItem::String("test_path"), + HashItem::Schema(Bar2::<64>::SCHEMA) + ] + ), + [139, 128, 52, 27, 107, 8, 218, 98] + ); + assert_eq!( + hasher( + &max_opts, + &[ + HashItem::String("test_path"), + HashItem::Schema(Bar2::<64>::SCHEMA) + ] + ), + [224, 12, 182, 234, 175, 8, 88, 168] + ); } #[test] diff --git a/source/postcard-schema-ng/src/lib.rs b/source/postcard-schema-ng/src/lib.rs index 576aefb2..ce2d817f 100644 --- a/source/postcard-schema-ng/src/lib.rs +++ b/source/postcard-schema-ng/src/lib.rs @@ -7,6 +7,13 @@ pub mod impls; pub mod key; pub mod schema; +/// Types with a fixed upper bound +pub mod max_len { + #[cfg(feature = "use-std")] + pub use crate::impls::builtins_maxlen::std::{MaxLenBytes, MaxLenString}; + pub use crate::impls::builtins_maxlen::{MaxLenByteSlice, MaxLenStr, TooLong}; +} + /// Derive [`Schema`] for a struct or enum /// /// # Examples diff --git a/source/postcard-schema-ng/src/schema/fmt.rs b/source/postcard-schema-ng/src/schema/fmt.rs index c247046d..536b3bc7 100644 --- a/source/postcard-schema-ng/src/schema/fmt.rs +++ b/source/postcard-schema-ng/src/schema/fmt.rs @@ -14,38 +14,6 @@ use std::{string::String, vec::Vec}; #[cfg(all(not(feature = "use-std"), feature = "alloc"))] use alloc::{format, string::String, vec::Vec}; -/// Is this [`OwnedDataModelType`] a primitive? -pub fn is_prim(osdmty: &OwnedDataModelType) -> bool { - match osdmty { - OwnedDataModelType::Bool => true, - OwnedDataModelType::I8 => true, - OwnedDataModelType::U8 => true, - OwnedDataModelType::I16 => true, - OwnedDataModelType::I32 => true, - OwnedDataModelType::I64 => true, - OwnedDataModelType::I128 => true, - OwnedDataModelType::U16 => true, - OwnedDataModelType::U32 => true, - OwnedDataModelType::U64 => true, - OwnedDataModelType::U128 => true, - OwnedDataModelType::Usize => true, - OwnedDataModelType::Isize => true, - OwnedDataModelType::F32 => true, - OwnedDataModelType::F64 => true, - OwnedDataModelType::Char => true, - OwnedDataModelType::String => true, - OwnedDataModelType::ByteArray => true, - OwnedDataModelType::Option(ty) => is_prim(ty), - OwnedDataModelType::Unit => true, - OwnedDataModelType::Seq(_) => false, - OwnedDataModelType::Tuple(_) => false, - OwnedDataModelType::Map { key, val } => is_prim(key) && is_prim(val), - OwnedDataModelType::Struct { .. } => false, - OwnedDataModelType::Enum { .. } => false, - OwnedDataModelType::Schema => true, - } -} - /// Format an [`OwnedDataModelType`] to the given string. /// /// Use `top_level = true` when this is a standalone type, and `top_level = false` @@ -105,18 +73,34 @@ pub fn fmt_owned_dmt_to_buf(dmt: &OwnedDataModelType, buf: &mut String, top_leve OwnedDataModelType::F32 => *buf += "f32", OwnedDataModelType::F64 => *buf += "f64", OwnedDataModelType::Char => *buf += "char", - OwnedDataModelType::String => *buf += "String", - OwnedDataModelType::ByteArray => *buf += "[u8]", + OwnedDataModelType::String { max_len } => { + *buf += "str"; + if let Some(max_len) = max_len { + *buf += &format!("[..{max_len}]"); + } + } + OwnedDataModelType::ByteArray { max_len } => { + *buf += "[u8]"; + if let Some(max_len) = max_len { + *buf += &format!("[..{max_len}]"); + } + } OwnedDataModelType::Option(ty) => { *buf += "Option<"; fmt_owned_dmt_to_buf(ty, buf, false); *buf += ">"; } OwnedDataModelType::Unit => *buf += "()", - OwnedDataModelType::Seq(ty) => { + OwnedDataModelType::Seq { + element: ty, + max_len, + } => { *buf += "["; fmt_owned_dmt_to_buf(ty, buf, false); *buf += "]"; + if let Some(max_len) = max_len { + *buf += &format!("[..{max_len}]"); + } } OwnedDataModelType::Tuple(vec) => { if !vec.is_empty() { @@ -146,12 +130,15 @@ pub fn fmt_owned_dmt_to_buf(dmt: &OwnedDataModelType, buf: &mut String, top_leve *buf += "()"; } } - OwnedDataModelType::Map { key, val } => { + OwnedDataModelType::Map { key, val, max_len } => { *buf += "Map<"; fmt_owned_dmt_to_buf(key, buf, false); *buf += ", "; fmt_owned_dmt_to_buf(val, buf, false); *buf += ">"; + if let Some(max_len) = max_len { + *buf += &format!("[..{max_len}]"); + } } OwnedDataModelType::Struct { name, data } => { if top_level { @@ -230,13 +217,16 @@ pub fn discover_tys( OwnedDataModelType::F32 => {} OwnedDataModelType::F64 => {} OwnedDataModelType::Char => {} - OwnedDataModelType::String => {} - OwnedDataModelType::ByteArray => {} + OwnedDataModelType::String { max_len: _ } => {} + OwnedDataModelType::ByteArray { max_len: _ } => {} OwnedDataModelType::Option(inner) => { discover_tys(inner, set); } OwnedDataModelType::Unit => {} - OwnedDataModelType::Seq(elements) => { + OwnedDataModelType::Seq { + element: elements, + max_len: _, + } => { discover_tys(elements, set); } OwnedDataModelType::Tuple(vec) => { @@ -244,7 +234,11 @@ pub fn discover_tys( discover_tys(v, set); } } - OwnedDataModelType::Map { key, val } => { + OwnedDataModelType::Map { + key, + val, + max_len: _, + } => { discover_tys(key, set); discover_tys(val, set); } diff --git a/source/postcard-schema-ng/src/schema/mod.rs b/source/postcard-schema-ng/src/schema/mod.rs index 15d983df..4a769bd1 100644 --- a/source/postcard-schema-ng/src/schema/mod.rs +++ b/source/postcard-schema-ng/src/schema/mod.rs @@ -20,9 +20,6 @@ //! versions of the Data Model types. These owned types implement `From` for their borrowed //! counterpoint, so if you need to deserialize something, you probably want the Owned variant! -#[cfg(any(feature = "use-std", feature = "alloc"))] -pub mod owned; - #[cfg(any(feature = "use-std", feature = "alloc"))] pub mod fmt; @@ -84,10 +81,16 @@ pub enum DataModelType { Char, /// The `String` Serde Data Model Type - String, + String { + /// Maximum length (in bytes) of the string + max_len: Option, + }, /// The `&[u8]` Serde Data Model Type - ByteArray, + ByteArray { + /// Maximum length of byte array + max_len: Option, + }, /// The `Option` Serde Data Model Type Option(&'static Self), @@ -96,7 +99,12 @@ pub enum DataModelType { Unit, /// The "Sequence" Serde Data Model Type - Seq(&'static Self), + Seq { + /// Element type of the sequence + element: &'static Self, + /// Maximum length of the sequence + max_len: Option, + }, /// The "Tuple" Serde Data Model Type Tuple(&'static [&'static Self]), @@ -107,6 +115,8 @@ pub enum DataModelType { key: &'static Self, /// The map "Value" type val: &'static Self, + /// Maximum length of K:V pairs + max_len: Option, }, /// One of the struct Serde Data Model types @@ -129,6 +139,126 @@ pub enum DataModelType { Schema, } +const fn arr_max_size(data_model_types: &[&DataModelType]) -> Option { + let mut idx = 0; + let mut sum = 0; + while idx < data_model_types.len() { + let Some(size) = data_model_types[idx].max_size() else { + return None; + }; + sum += size; + idx += 1; + } + Some(sum) +} + +pub(crate) const fn size_as_varint_usize(n: usize) -> usize { + if n == 0 { + return 1; + } + let ttl_bits = usize::BITS as usize; + let ldg_bits = n.leading_zeros() as usize; + let used_bits = ttl_bits - ldg_bits; + used_bits.div_ceil(7) +} + +macro_rules! max_size_dmt { + ($kind:ty) => { + impl $kind { + /// Max serialized size + pub const fn max_size(&self) -> Option { + use $kind::*; + match self { + Bool => Some(1), + I8 => Some(1), + U8 => Some(1), + I16 => Some(postcard_core::varint::varint_max::()), + I32 => Some(postcard_core::varint::varint_max::()), + I64 => Some(postcard_core::varint::varint_max::()), + I128 => Some(postcard_core::varint::varint_max::()), + U16 => Some(postcard_core::varint::varint_max::()), + U32 => Some(postcard_core::varint::varint_max::()), + U64 => Some(postcard_core::varint::varint_max::()), + U128 => Some(postcard_core::varint::varint_max::()), + Usize => Some(postcard_core::varint::varint_max::()), + Isize => Some(postcard_core::varint::varint_max::()), + F32 => Some(4), + F64 => Some(8), + Char => Some(5), + String { max_len } | ByteArray { max_len } => { + if let Some(len) = max_len { + Some(size_as_varint_usize(*len) + *len) + } else { + None + } + } + Option(data_model_type) => { + if let Some(len) = data_model_type.max_size() { + Some(1 + len) + } else { + None + } + } + Unit => Some(0), + Seq { element, max_len } => { + let Some(len) = max_len else { + return None; + }; + let Some(size) = element.max_size() else { + return None; + }; + let elements = (*len) * size; + Some(size_as_varint_usize(*len) + elements) + } + Tuple(data_model_types) => arr_max_size(data_model_types), + Map { key, val, max_len } => { + let Some(len) = max_len else { + return None; + }; + let Some(ksize) = key.max_size() else { + return None; + }; + let Some(vsize) = val.max_size() else { + return None; + }; + let pair = ksize + vsize; + let pairs = (*len) * pair; + Some(size_as_varint_usize(*len) + pairs) + } + Struct { name: _, data } => data.max_size(), + Enum { name: _, variants } => { + let mut idx = 0; + + // So, if an enum has zero variants, we can't actually + // really serialize or deserialize it, because it is + // uninhabited! I guess logically the max len is then + // also zero? + let disc = if variants.is_empty() { + 0 + } else { + size_as_varint_usize(variants.len() - 1) + }; + let mut max = 0; + while idx < variants.len() { + let Some(size) = variants[idx].data.max_size() else { + return None; + }; + if size > max { + max = size; + } + idx += 1; + } + Some(disc + max) + } + // Schemas are unbounded + Schema => None, + } + } + } + }; +} +max_size_dmt!(DataModelType); + /// The contents of a struct or enum variant. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] pub enum Data { @@ -145,6 +275,29 @@ pub enum Data { Struct(&'static [&'static NamedField]), } +impl Data { + /// Max serialized size + pub const fn max_size(&self) -> Option { + match self { + Data::Unit => Some(0), + Data::Newtype(data_model_type) => data_model_type.max_size(), + Data::Tuple(data_model_types) => arr_max_size(data_model_types), + Data::Struct(named_fields) => { + let mut idx = 0; + let mut sum = 0; + while idx < named_fields.len() { + let Some(size) = named_fields[idx].ty.max_size() else { + return None; + }; + sum += size; + idx += 1; + } + Some(sum) + } + } + } +} + /// This represents a named struct field. /// /// For example, in `struct Ex { a: u32 }` the field `a` would be reflected as @@ -165,3 +318,6 @@ pub struct Variant { /// The data contained in this variant pub data: Data, } + +#[cfg(any(feature = "use-std", feature = "alloc"))] +pub mod owned; diff --git a/source/postcard-schema-ng/src/schema/owned.rs b/source/postcard-schema-ng/src/schema/owned.rs index 978ff94c..e33d740e 100644 --- a/source/postcard-schema-ng/src/schema/owned.rs +++ b/source/postcard-schema-ng/src/schema/owned.rs @@ -1,5 +1,7 @@ //! Owned Schema version +use crate::schema::size_as_varint_usize; + use super::{Data, DataModelType, NamedField, Variant}; use serde::{Deserialize, Serialize}; @@ -96,10 +98,16 @@ pub enum OwnedDataModelType { Char, /// The `String` Serde Data Model Type - String, + String { + /// Maximum length (in bytes) of the string + max_len: Option, + }, /// The `&[u8]` Serde Data Model Type - ByteArray, + ByteArray { + /// Maximum length of byte array + max_len: Option, + }, /// The `Option` Serde Data Model Type Option(Box), @@ -108,7 +116,12 @@ pub enum OwnedDataModelType { Unit, /// The "Sequence" Serde Data Model Type - Seq(Box), + Seq { + /// Items in the sequence + element: Box, + /// Maximum length of the sequence + max_len: Option, + }, /// The "Tuple" Serde Data Model Type Tuple(Box<[Self]>), @@ -119,6 +132,8 @@ pub enum OwnedDataModelType { key: Box, /// The map "Value" type val: Box, + /// Maximum length of K:V pairs + max_len: Option, }, /// One of the struct Serde Data Model types @@ -141,6 +156,21 @@ pub enum OwnedDataModelType { Schema, } +const fn arr_max_size(data_model_types: &[OwnedDataModelType]) -> Option { + let mut idx = 0; + let mut sum = 0; + while idx < data_model_types.len() { + let Some(size) = data_model_types[idx].max_size() else { + return None; + }; + sum += size; + idx += 1; + } + Some(sum) +} + +max_size_dmt!(OwnedDataModelType); + impl From<&DataModelType> for OwnedDataModelType { fn from(other: &DataModelType) -> Self { match other { @@ -160,15 +190,22 @@ impl From<&DataModelType> for OwnedDataModelType { DataModelType::F32 => Self::F32, DataModelType::F64 => Self::F64, DataModelType::Char => Self::Char, - DataModelType::String => Self::String, - DataModelType::ByteArray => Self::ByteArray, + DataModelType::String { max_len } => Self::String { max_len: *max_len }, + DataModelType::ByteArray { max_len } => Self::ByteArray { max_len: *max_len }, DataModelType::Option(o) => Self::Option(Box::new((*o).into())), DataModelType::Unit => Self::Unit, - DataModelType::Seq(s) => Self::Seq(Box::new((*s).into())), + DataModelType::Seq { + element: s, + max_len, + } => Self::Seq { + element: Box::new((*s).into()), + max_len: *max_len, + }, DataModelType::Tuple(t) => Self::Tuple(t.iter().map(|i| (*i).into()).collect()), - DataModelType::Map { key, val } => Self::Map { + DataModelType::Map { key, val, max_len } => Self::Map { key: Box::new((*key).into()), val: Box::new((*val).into()), + max_len: *max_len, }, DataModelType::Struct { name, data } => Self::Struct { name: (*name).into(), @@ -201,6 +238,29 @@ pub enum OwnedData { Struct(Box<[OwnedNamedField]>), } +impl OwnedData { + /// Max serialized size + pub const fn max_size(&self) -> Option { + match self { + OwnedData::Unit => Some(0), + OwnedData::Newtype(data_model_type) => data_model_type.max_size(), + OwnedData::Tuple(data_model_types) => arr_max_size(data_model_types), + OwnedData::Struct(named_fields) => { + let mut idx = 0; + let mut sum = 0; + while idx < named_fields.len() { + let Some(size) = named_fields[idx].ty.max_size() else { + return None; + }; + sum += size; + idx += 1; + } + Some(sum) + } + } + } +} + impl From<&Data> for OwnedData { fn from(data: &Data) -> Self { match data { diff --git a/source/postcard-schema-ng/tests/schema.rs b/source/postcard-schema-ng/tests/schema.rs index 31ae9534..d2db0297 100644 --- a/source/postcard-schema-ng/tests/schema.rs +++ b/source/postcard-schema-ng/tests/schema.rs @@ -128,7 +128,10 @@ fn test_struct_serialize() { }, &NamedField { name: "f", - ty: &DataModelType::Seq(u8::SCHEMA), + ty: &DataModelType::Seq { + element: u8::SCHEMA, + max_len: None + }, }, ]), } @@ -142,7 +145,10 @@ fn test_slice_serialize() { name: "Slice", data: Data::Struct(&[&NamedField { name: "x", - ty: &DataModelType::Seq(u8::SCHEMA), + ty: &DataModelType::Seq { + element: u8::SCHEMA, + max_len: None + }, }]), }, Slice::SCHEMA @@ -202,7 +208,7 @@ fn owned_punning() { // TODO: This is wildly repetitive, and likely could benefit from interning of // repeated types, strings, etc. - assert_eq!(ser_borrowed_schema.len(), 187); + assert_eq!(ser_borrowed_schema.len(), 189); // Check that we round-trip correctly let deser_borrowed_schema = @@ -323,11 +329,11 @@ fn smoke() { (dewit::<()>, "()"), (dewit::, "char"), (dewit::, "bool"), - (dewit::, "String"), + (dewit::, "str"), (dewit::>, "Option"), (dewit::, "struct UnitStruct"), (dewit::>, "Option"), - (dewit::, "struct NewTypeStruct(String)"), + (dewit::, "struct NewTypeStruct(str)"), (dewit::>, "Option"), ( dewit::, @@ -338,11 +344,11 @@ fn smoke() { (dewit::>, "[u16]"), (dewit::<[u8; 16]>, "[u8; 16]"), (dewit::<(u8, u16, u32)>, "(u8, u16, u32)"), - (dewit::, "struct TupStruct(u64, String)"), + (dewit::, "struct TupStruct(u64, str)"), (dewit::>, "Option"), ( dewit::>, - "Map", + "Map", ), (dewit::>, "[u32]"), ( @@ -355,9 +361,56 @@ fn smoke() { ), (dewit::>, "Option"), (dewit::>>, "Option"), - (dewit::, "String"), + (dewit::, "str"), ]; for (f, s) in tests { assert_eq!(f().as_str(), *s); } } + +#[test] +fn size_smoke() { + #[allow(clippy::type_complexity)] + let tests: &[(Option, Option)] = &[ + (u8::SCHEMA.max_size(), Some(1)), + (u16::SCHEMA.max_size(), Some(3)), + (u32::SCHEMA.max_size(), Some(5)), + (u64::SCHEMA.max_size(), Some(10)), + (u128::SCHEMA.max_size(), Some(19)), + (i8::SCHEMA.max_size(), Some(1)), + (i16::SCHEMA.max_size(), Some(3)), + (i32::SCHEMA.max_size(), Some(5)), + (i64::SCHEMA.max_size(), Some(10)), + (i128::SCHEMA.max_size(), Some(19)), + (<()>::SCHEMA.max_size(), Some(0)), + (char::SCHEMA.max_size(), Some(5)), + (bool::SCHEMA.max_size(), Some(1)), + (String::SCHEMA.max_size(), None), + (>::SCHEMA.max_size(), Some(4)), + (UnitStruct::SCHEMA.max_size(), Some(0)), + (>::SCHEMA.max_size(), Some(1)), + (NewTypeStruct::SCHEMA.max_size(), None), + (>::SCHEMA.max_size(), None), + (Enums::SCHEMA.max_size(), Some(11)), + (>::SCHEMA.max_size(), Some(12)), + (<&[u8]>::SCHEMA.max_size(), None), + (>::SCHEMA.max_size(), None), + (<[u8; 16]>::SCHEMA.max_size(), Some(16)), + (<(u8, u16, u32)>::SCHEMA.max_size(), Some(9)), + (TupStruct::SCHEMA.max_size(), None), + (>::SCHEMA.max_size(), None), + ( + >::SCHEMA.max_size(), + None, + ), + (>::SCHEMA.max_size(), None), + (::SCHEMA.max_size(), Some(9)), + (>::SCHEMA.max_size(), Some(10)), + (>::SCHEMA.max_size(), Some(10)), + (>>::SCHEMA.max_size(), Some(11)), + (PathBuf::SCHEMA.max_size(), None), + ]; + for (calc, bound) in tests { + assert_eq!(calc, bound); + } +}