diff --git a/.github/settings.yml b/.github/settings.yml index 06a5c3f..75a1d3d 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -22,16 +22,20 @@ repository: labels: - name: github_actions color: '#000000' - description: Changes to github actions or dependencies + description: Pull requests that update github actions or dependencies - name: rust color: '#f74c00' - description: Changes to rust code or dependencies + description: Pull requests that update rust code or dependencies - name: python color: '#4584b6' - description: Changes to python code or dependencies + description: Pull requests that update python code or dependencies - name: go color: '#29beb0' - description: Changes to golang code or dependencies + description: Pull requests that update golang code or dependencies + + - name: dependencies + color: '#0366d6' + description: Pull requests that update a dependency file - name: size/XS color: '#00ed01' diff --git a/Cargo.toml b/Cargo.toml index 378fbfa..b631ad9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ rust-version = "1.67.1" [profile.release] lto = "thin" -[metadata.release] +[package.metadata.release] shared-version = true dev-version-ext = "beta.0" consolidate-commits = true @@ -32,10 +32,24 @@ pre-release-replacements = [ ] [features] -std = ["serde/std", "serde_cbor/std", "serde_with"] -test_utils = ["protobuf"] +std = ["serde/std", "ciborium/std"] +test_utils = ["dep:protobuf"] +jsonu64 = ["std", "dep:serde_with"] [dependencies] +# These dependencies locked because the interaction between cibiorum and ciborium-io is garbage. +# +# cibiorum::ser::into_writer(&Vec) is infallible, but only because of how cibiorum-io is +# implemented, in version 0.2.0, when the alloc feature (which is not exposed by ciborium) is +# enabled. +# +# If you are considering using serde for any reason, consider the choices you have made in your +# life, and then use protobufs (via prost) instead. +ciborium = { version = "=0.2.0", default-features = false } +ciborium-io = { version = "=0.2.0", default-features = false, features = [ + "alloc", +] } + prost = { version = "0.11", default-features = false, features = [ "prost-derive", ] } @@ -44,9 +58,6 @@ serde = { version = "1.0", default-features = false, features = [ "alloc", "derive", ] } -serde_cbor = { version = "0.11", default-features = false, features = [ - "alloc", -] } serde_with = { version = "2.0", default-features = false, features = [ "macros", ], optional = true } diff --git a/src/json_u64.rs b/src/json_u64.rs new file mode 100644 index 0000000..280bd15 --- /dev/null +++ b/src/json_u64.rs @@ -0,0 +1,83 @@ +// Copyright (c) 2018-2022 The MobileCoin Foundation + +//! Serialization and deserialization for a U64 in JSON + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; + +/// Represents u64 using string, when serializing to Json +/// Javascript integers are not 64 bit, and so it is not really proper json. +/// Using string avoids issues with some json parsers not handling large +/// numbers well. +/// +/// This does not rely on the serde-json arbitrary precision feature, which +/// (we fear) might break other things (e.g. https://github.com/serde-rs/json/issues/505) +#[serde_as] +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Hash, Serialize)] +#[serde(transparent)] +pub struct JsonU64(#[serde_as(as = "DisplayFromStr")] pub u64); + +impl From<&u64> for JsonU64 { + fn from(src: &u64) -> Self { + Self(*src) + } +} + +impl From<&JsonU64> for u64 { + fn from(src: &JsonU64) -> u64 { + src.0 + } +} + +impl From for u64 { + fn from(src: JsonU64) -> u64 { + src.0 + } +} + +impl AsRef for JsonU64 { + fn as_ref(&self) -> &u64 { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec::Vec; + use serde::{Deserialize, Serialize}; + + #[derive(PartialEq, Serialize, Deserialize, Debug)] + struct TestStruct { + nums: Vec, + block: JsonU64, + } + + #[test] + fn serialize_jsonu64_struct() { + let the_struct = TestStruct { + nums: [0, 1, 2, u64::MAX] + .iter() + .map(Into::::into) + .collect(), + block: JsonU64(u64::MAX - 1), + }; + let serialized = crate::serialize(&the_struct).unwrap(); + let deserialized = + crate::deserialize::(&serialized).expect("Could not deserialize struct"); + assert_eq!(deserialized, the_struct); + + // Sanity that serde_as works as expected: it should accept and hand us back + // strings. + let expected_json = + r#"{"nums":["0","1","2","18446744073709551615"],"block":"18446744073709551614"}"#; + assert_eq!( + expected_json, + serde_json::to_string(&the_struct).expect("Could not convert struct to json string") + ); + assert_eq!( + the_struct, + serde_json::from_str(expected_json).expect("Could not convert json string to struct") + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1054a3a..e86c772 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,127 +1,116 @@ // Copyright (c) 2018-2022 The MobileCoin Foundation +//! Serialization and deserialization utilities for MobileCoin. + #![no_std] +#![doc = include_str!("../README.md")] +#![deny(missing_docs, missing_debug_implementations, unsafe_code)] extern crate alloc; -use alloc::vec::Vec; - -pub extern crate prost; -pub use prost::{DecodeError, EncodeError, Message}; +use alloc::vec::Vec; +use core::{ + fmt::{Display, Formatter, Result as FmtResult}, + mem, +}; use serde::{Deserialize, Serialize}; -// We put a new-type around serde_cbor::Error in `mod decode` and `mod encode`, -// because this keeps us compatible with how rmp-serde was exporting its errors, -// and avoids unnecessary code changes. +pub use prost::{self, DecodeError, EncodeError, Message}; + +/// Decoding-specific types, here for backwards compatibility. pub mod decode { + use super::*; + use alloc::{ + format, + string::{String, ToString}, + }; + use ciborium::de::Error as CiboriumError; + use core::fmt::Debug; + + /// An error structure for CBOR decoding #[derive(Debug)] - pub struct Error(serde_cbor::Error); + pub struct Error(String); - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "Cbor Decode Error: {}", self.0) + impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "CBOR Decode Error: {:#?}", self.0) } } - impl From for Error { - fn from(src: serde_cbor::Error) -> Self { - Self(src) + impl From> for Error { + fn from(src: CiboriumError) -> Self { + let err_msg = match src { + CiboriumError::Io(underlying) => format!("IO Error: {underlying:#?}"), + CiboriumError::Syntax(offset) => format!("Syntax Error at byte {offset:#?}"), + CiboriumError::Semantic(offset, inner_msg) => { + format!("Semantic Error at byte {offset:#?}: {inner_msg:#?}") + } + CiboriumError::RecursionLimitExceeded => "Recursion limit exceeded".to_string(), + }; + + Self(err_msg) } } } +/// Encoding-specific types, here for backwards compatibility. pub mod encode { - #[derive(Debug)] - pub struct Error(serde_cbor::Error); + use super::*; - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "Cbor Encode Error: {}", self.0) - } - } + /// CBOR encoding errors (this shouldn't actually be seen in practice) + #[derive(Debug)] + pub struct Error; - impl From for Error { - fn from(src: serde_cbor::Error) -> Self { - Self(src) + impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "CBOR Encode Error") } } } /// Serialize the given data structure. /// -/// Forward mc_util_serial::serialize to bincode::serialize(..., Infinite) -/// Serialization can fail if `T`'s implementation of `Serialize` decides to -/// fail. +/// This method produces CBOR-encoded bytes. pub fn serialize(value: &T) -> Result, encode::Error> where T: Serialize + Sized, { - Ok(serde_cbor::to_vec(value)?) + let mut writer = Vec::with_capacity(2 * mem::size_of::()); + // NOTE: this depends on the precise behavior of ciborium-io 0.2: + // https://docs.rs/ciborium-io/0.2.0/src/ciborium_io/lib.rs.html#151 + ciborium::ser::into_writer(value, &mut writer) + .expect("into_writer with a Vec should be infallible"); + + Ok(writer) } /// Deserialize the given bytes to a data structure. /// -/// Forward mc_util_serial::deserialize to serde_cbor::from_slice +/// This method expects CBOR-encoded bytes. pub fn deserialize<'a, T>(bytes: &'a [u8]) -> Result where T: Deserialize<'a>, { - Ok(serde_cbor::from_slice(bytes)?) + Ok(ciborium::de::from_reader(bytes)?) } +/// Encode the give data structure to protobuf using prost::Message. pub fn encode(value: &T) -> Vec { value.encode_to_vec() } +/// Decode the protobuf-encoded bytes into a data structure using prost::Message. pub fn decode(buf: &[u8]) -> Result { T::decode(buf) } -#[cfg(feature = "serde_with")] -mod json_u64 { - use super::*; - use serde_with::{serde_as, DisplayFromStr}; - - /// Represents u64 using string, when serializing to Json - /// Javascript integers are not 64 bit, and so it is not really proper json. - /// Using string avoids issues with some json parsers not handling large - /// numbers well. - /// - /// This does not rely on the serde-json arbitrary precision feature, which - /// (we fear) might break other things (e.g. https://github.com/serde-rs/json/issues/505) - #[serde_as] - #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Hash, Serialize)] - #[serde(transparent)] - pub struct JsonU64(#[serde_as(as = "DisplayFromStr")] pub u64); - - impl From<&u64> for JsonU64 { - fn from(src: &u64) -> Self { - Self(*src) - } - } - - impl From<&JsonU64> for u64 { - fn from(src: &JsonU64) -> u64 { - src.0 - } - } - - impl From for u64 { - fn from(src: JsonU64) -> u64 { - src.0 - } - } - - impl AsRef for JsonU64 { - fn as_ref(&self) -> &u64 { - &self.0 - } - } -} +#[cfg(feature = "jsonu64")] +/// This module contains a JsonU64 type which is used to represent u64 values safely in JSON. +mod json_u64; /// JsonU64 is exported if it is available -- the serde_with crate which it /// depends on relies on std, so it must be optional. -#[cfg(feature = "serde_with")] +#[cfg(feature = "jsonu64")] pub use json_u64::JsonU64; /// Take a prost type and try to roundtrip it through a protobuf type @@ -147,17 +136,10 @@ pub fn round_trip_message( #[cfg(test)] mod test { use super::*; - use alloc::vec; + use alloc::{string::String, vec}; use serde::{Deserialize, Serialize}; - #[test] - fn test_serialize_string() { - let the_string = "There goes the baker with his tray, like always"; - let serialized = serialize(&the_string).unwrap(); - let deserialized: &str = deserialize(&serialized).unwrap(); - assert_eq!(deserialized, the_string); - } - + // A structure to test serialization and deserialization #[derive(PartialEq, Serialize, Deserialize, Debug)] struct TestStruct { vec: Vec, @@ -166,44 +148,124 @@ mod test { } #[test] - fn test_serialize_struct() { + fn serialize_string() { + const THE_STRING: &str = "There goes the baker with his tray, like always"; + + let serialized = serialize(&THE_STRING).unwrap(); + let deserialized = deserialize::(&serialized).unwrap(); + assert_eq!(&deserialized, THE_STRING); + } + + #[test] + fn serialize_struct() { let the_struct = TestStruct { vec: vec![233, 123, 0, 12], integer: 4_242_424_242, float: 1.2345, }; let serialized = serialize(&the_struct).unwrap(); - let deserialized: TestStruct = deserialize(&serialized).unwrap(); + let deserialized = deserialize::(&serialized).unwrap(); assert_eq!(deserialized, the_struct); } -} -#[cfg(all(test, feature = "serde_with"))] -mod json_u64_tests { - use super::*; - use serde::{Deserialize, Serialize}; + #[test] + fn serialize_array() { + let bytes = [0x55u8; 32]; - #[derive(PartialEq, Serialize, Deserialize, Debug)] - struct TestStruct { - nums: Vec, - block: JsonU64, + let serialized = serialize(&bytes).expect("Could not serialize byte array."); + let deserialized = + deserialize::<[u8; 32]>(&serialized).expect("Could not deserialize byte array."); + + assert_eq!(deserialized, bytes); + } + + /// Single, named byte array in a struct + #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] + struct ByteStruct { + bytes: [u8; 32], } #[test] - fn test_serialize_jsonu64_struct() { - let the_struct = TestStruct { - nums: [0, 1, 2, u64::MAX].iter().map(Into::into).collect(), - block: JsonU64(u64::MAX - 1), + fn struct_with_array() { + let expected = ByteStruct::default(); + let serialized = serialize(&expected).expect("Could not serialize byte struct."); + let actual = + deserialize::(&serialized).expect("Could not deserialize byte struct."); + + assert_eq!(actual, expected); + + let expected = ByteStruct { + bytes: [0x55u8; 32], }; - let serialized = serialize(&the_struct).unwrap(); - let deserialized: TestStruct = deserialize(&serialized).unwrap(); - assert_eq!(deserialized, the_struct); + let serialized = serialize(&expected).expect("Could not serialize byte struct."); + let actual = + deserialize::(&serialized).expect("Could not deserialize byte struct."); + + assert_eq!(actual, expected); + } + + /// Unnamed byte array in structs + #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] + struct NewtypeBytes([u8; 32]); + + #[test] + fn newtype_bytes() { + let expected = NewtypeBytes([0x55u8; 32]); + let serialized = serialize(&expected).expect("Could not serialize transparent struct."); + let actual = + deserialize::(&serialized).expect("Could not deserialize byte struct."); + assert_eq!(expected, actual); + } + + #[test] + fn newtype_bytes_vs_just_bytes() { + let expected = NewtypeBytes([0x55u8; 32]); + let serialized = serialize(&expected).expect("Could not serialize transparent struct."); + + let bytes = [0x55u8; 32]; + let bytes_serialized = serialize(&bytes).expect("Could not serialize byte array."); + assert_eq!(serialized, bytes_serialized); + + let actual = deserialize::(&bytes_serialized) + .expect("Could not deserialize byte array to struct."); + + assert_eq!(expected, actual); + } + + /// Tuple struct with unnamed byte arrays + #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] + struct TupleStruct([u8; 32], [u8; 32]); + + #[test] + fn tuplestruct() { + let expected = TupleStruct([0x55u8; 32], [0xffu8; 32]); + let serialized = serialize(&expected).expect("Could not serialize transparent struct."); + let actual = + deserialize::(&serialized).expect("Could not deserialize byte struct."); + assert_eq!(expected, actual); + } + + #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] + struct InnerStruct { + bytes: [u8; 32], + } + + #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] + struct OuterStruct { + inner: InnerStruct, + } + + #[test] + fn nested_struct() { + let expected = OuterStruct { + inner: InnerStruct { + bytes: [0x55u8; 32], + }, + }; + let serialized = serialize(&expected).expect("Could not serialize transparent struct."); + let actual = + deserialize::(&serialized).expect("Could not deserialize byte struct."); - // Sanity that serde_as works as expected: it should accept and hand us back - // strings. - let expected_json = - r#"{"nums":["0","1","2","18446744073709551615"],"block":"18446744073709551614"}"#; - assert_eq!(expected_json, serde_json::to_string(&the_struct).unwrap()); - assert_eq!(the_struct, serde_json::from_str(expected_json).unwrap()); + assert_eq!(expected, actual); } }