Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- [cometbft-proto] Intoduce `CowStr` deserializer.
([\#111](https://github.com/cometbft/cometbft-rs/issues/111))
- [cometbft] Fix deserialization from `serde_json::Value`.
([\#111](https://github.com/cometbft/cometbft-rs/issues/111))
46 changes: 39 additions & 7 deletions cometbft/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use core::{
};

use bytes::Bytes;
use cometbft_proto::serializers::cow_str::CowStr;
use cometbft_proto::Protobuf;
use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
use subtle_encoding::{base64, Encoding, Hex};
Expand Down Expand Up @@ -166,12 +167,12 @@ impl FromStr for Hash {

impl<'de> Deserialize<'de> for Hash {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let hex = <&str>::deserialize(deserializer)?;
let hex = CowStr::deserialize(deserializer)?;

if hex.is_empty() {
Err(D::Error::custom("empty hash"))
} else {
Ok(Self::from_str(hex).map_err(|e| D::Error::custom(format!("{e}")))?)
Ok(Self::from_str(&hex).map_err(|e| D::Error::custom(format!("{e}")))?)
}
}
}
Expand Down Expand Up @@ -200,8 +201,8 @@ pub mod allow_empty {
where
D: Deserializer<'de>,
{
let hex = <&str>::deserialize(deserializer)?;
Hash::from_str(hex).map_err(serde::de::Error::custom)
let hex = CowStr::deserialize(deserializer)?;
Hash::from_str(&hex).map_err(serde::de::Error::custom)
}
}

Expand Down Expand Up @@ -301,15 +302,22 @@ mod tests {
use super::*;

#[derive(Debug, serde::Deserialize)]
struct Test {
struct AppHashTest {
#[serde(default)]
#[serde(with = "crate::serializers::apphash")]
pub app_hash: AppHash,
}

#[derive(Debug, serde::Deserialize)]
struct HashTest {
hash: Hash,
#[serde(with = "super::allow_empty")]
empty_hash: Hash,
}

#[test]
fn apphash_decode_base64() {
let test = serde_json::from_str::<Test>(
let test = serde_json::from_str::<AppHashTest>(
r#"{"app_hash":"MfX9f+bYoI8IioRb4YT/8/VhPvtNjgWFgTi4mmMSkBc="}"#,
)
.unwrap();
Expand All @@ -326,7 +334,7 @@ mod tests {

#[test]
fn apphash_decode_hex() {
let test = serde_json::from_str::<Test>(
let test = serde_json::from_str::<AppHashTest>(
r#"{"app_hash":"31F5FD7FE6D8A08F088A845BE184FFF3F5613EFB4D8E05858138B89A63129017"}"#,
)
.unwrap();
Expand All @@ -340,4 +348,28 @@ mod tests {
]
);
}

#[test]
fn hash_decode_hex() {
let s = r#"{
"hash": "9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08",
"empty_hash": ""
}"#;

let expected_hash = &[
0x9F, 0x86, 0xD0, 0x81, 0x88, 0x4C, 0x7D, 0x65, 0x9A, 0x2F, 0xEA, 0xA0, 0xC5, 0x5A,
0xD0, 0x15, 0xA3, 0xBF, 0x4F, 0x1B, 0x2B, 0x0B, 0x82, 0x2C, 0xD1, 0x5D, 0x6C, 0x15,
0xB0, 0xF0, 0x0A, 0x08,
];

let test = serde_json::from_str::<HashTest>(s).unwrap();
assert_eq!(test.hash.as_ref(), expected_hash);
assert_eq!(test.empty_hash, Hash::None);

// Test issue 1474
let json_value = serde_json::from_str::<serde_json::Value>(s).unwrap();
let test = serde_json::from_value::<HashTest>(json_value).unwrap();
assert_eq!(test.hash.as_ref(), expected_hash);
assert_eq!(test.empty_hash, Hash::None);
}
}
1 change: 1 addition & 0 deletions proto/src/serializers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@

pub mod allow_null;
pub mod bytes;
pub mod cow_str;
pub mod evidence;
pub mod from_str;
pub mod from_str_allow_null;
Expand Down
17 changes: 11 additions & 6 deletions proto/src/serializers/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ pub mod hexstring {
use subtle_encoding::hex;

use crate::prelude::*;
use crate::serializers::cow_str::CowStr;

/// Deserialize a hex-encoded string into `Vec<u8>`
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let string = Option::<String>::deserialize(deserializer)?.unwrap_or_default();
let string = Option::<CowStr<'_>>::deserialize(deserializer)?.unwrap_or_default();
hex::decode_upper(&string)
.or_else(|_| hex::decode(&string))
.map_err(serde::de::Error::custom)
Expand All @@ -36,14 +37,15 @@ pub mod base64string {
use subtle_encoding::base64;

use crate::prelude::*;
use crate::serializers::cow_str::CowStr;

/// Deserialize base64string into `Vec<u8>`
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
Vec<u8>: Into<T>,
{
let s = Option::<String>::deserialize(deserializer)?.unwrap_or_default();
let s = Option::<CowStr<'_>>::deserialize(deserializer)?.unwrap_or_default();
let v = base64::decode(s).map_err(serde::de::Error::custom)?;
Ok(v.into())
}
Expand All @@ -53,7 +55,7 @@ pub mod base64string {
where
D: Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?.unwrap_or_default();
let s = Option::<CowStr<'_>>::deserialize(deserializer)?.unwrap_or_default();
String::from_utf8(base64::decode(s).map_err(serde::de::Error::custom)?)
.map_err(serde::de::Error::custom)
}
Expand All @@ -76,13 +78,14 @@ pub mod vec_base64string {
use subtle_encoding::base64;

use crate::prelude::*;
use crate::serializers::cow_str::CowStr;

/// Deserialize array into `Vec<Vec<u8>>`
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
Option::<Vec<String>>::deserialize(deserializer)?
Option::<Vec<CowStr<'_>>>::deserialize(deserializer)?
.unwrap_or_default()
.into_iter()
.map(|s| base64::decode(s).map_err(serde::de::Error::custom))
Expand Down Expand Up @@ -111,13 +114,14 @@ pub mod option_base64string {
use subtle_encoding::base64;

use crate::prelude::*;
use crate::serializers::cow_str::CowStr;

/// Deserialize `Option<base64string>` into `Vec<u8>` or null
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?.unwrap_or_default();
let s = Option::<CowStr<'_>>::deserialize(deserializer)?.unwrap_or_default();
base64::decode(s).map_err(serde::de::Error::custom)
}

Expand All @@ -138,14 +142,15 @@ pub mod string {
use serde::{Deserialize, Deserializer, Serializer};

use crate::prelude::*;
use crate::serializers::cow_str::CowStr;

/// Deserialize string into `Vec<u8>`
#[allow(dead_code)]
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let string = Option::<String>::deserialize(deserializer)?.unwrap_or_default();
let string = Option::<CowStr<'_>>::deserialize(deserializer)?.unwrap_or_default();
Ok(string.as_bytes().to_vec())
}

Expand Down
174 changes: 174 additions & 0 deletions proto/src/serializers/cow_str.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//! Wrapper `Cow<'_, str>` for deserializing without allocation.
//!
//! This is a workaround for [serde's issue 1852](https://github.com/serde-rs/serde/issues/1852).

use alloc::borrow::{Cow, ToOwned};
use alloc::string::String;
use core::fmt::{self, Debug, Display, Formatter};
use core::ops::Deref;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};

/// Wrapper `Cow<'_, str>` for deserializing without allocation.
#[derive(Default)]
pub struct CowStr<'a>(Cow<'a, str>);

impl<'a> CowStr<'a> {
/// Convert into `Cow<'a, str>`.
pub fn into_inner(self) -> Cow<'a, str> {
self.0
}
}

impl<'de> Deserialize<'de> for CowStr<'de> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;

impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = CowStr<'de>;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a string")
}

fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(CowStr(Cow::Borrowed(value)))
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(CowStr(Cow::Owned(value.to_owned())))
}

fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(CowStr(Cow::Owned(value)))
}
}

deserializer.deserialize_str(Visitor)
}
}

impl Serialize for CowStr<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}

impl Debug for CowStr<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
<&str as Debug>::fmt(&&*self.0, f)
}
}

impl Display for CowStr<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
<&str as Display>::fmt(&&*self.0, f)
}
}

impl Deref for CowStr<'_> {
type Target = str;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl AsRef<str> for CowStr<'_> {
fn as_ref(&self) -> &str {
&self.0
}
}

impl AsRef<[u8]> for CowStr<'_> {
fn as_ref(&self) -> &[u8] {
self.0.as_bytes()
}
}

impl<'a> From<CowStr<'a>> for Cow<'a, str> {
fn from(value: CowStr<'a>) -> Self {
value.0
}
}

impl<'a> From<Cow<'a, str>> for CowStr<'a> {
fn from(value: Cow<'a, str>) -> Self {
CowStr(value)
}
}

/// Serialize `Cow<'_, str>`.
#[allow(clippy::ptr_arg)]
pub fn serialize<S>(value: &Cow<'_, str>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(value)
}

/// Deserialize `Cow<'_, str>`.
pub fn deserialize<'de, D>(deserializer: D) -> Result<Cow<'de, str>, D::Error>
where
D: Deserializer<'de>,
{
CowStr::deserialize(deserializer).map(|value| value.into_inner())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn borrowed() {
struct Test(u32);

impl<'de> Deserialize<'de> for Test {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = CowStr::deserialize(deserializer)?;
assert!(matches!(s.0, Cow::Borrowed(_)));
Ok(Test(s.parse().unwrap()))
}
}

let v = serde_json::from_str::<Test>("\"2\"").unwrap();
assert_eq!(v.0, 2);
}

#[test]
fn owned() {
struct Test(u32);

impl<'de> Deserialize<'de> for Test {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = CowStr::deserialize(deserializer)?;
assert!(matches!(s.0, Cow::Owned(_)));
Ok(Test(s.parse().unwrap()))
}
}

let json_value = serde_json::from_str::<serde_json::Value>("\"2\"").unwrap();
let v = serde_json::from_value::<Test>(json_value).unwrap();
assert_eq!(v.0, 2);
}
}
4 changes: 2 additions & 2 deletions proto/src/serializers/from_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
//! and [`Display`] to convert from or into string. Note this can be used for
//! all primitive data types.

use alloc::borrow::Cow;
use core::fmt::Display;
use core::str::FromStr;

use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};

use crate::prelude::*;
use crate::serializers::cow_str::CowStr;

/// Deserialize string into T
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
Expand All @@ -17,7 +17,7 @@ where
T: FromStr,
<T as FromStr>::Err: Display,
{
<Cow<'_, str>>::deserialize(deserializer)?
CowStr::deserialize(deserializer)?
.parse::<T>()
.map_err(D::Error::custom)
}
Expand Down
Loading
Loading