Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ciborium and Cleanups #2

Merged
merged 9 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
23 changes: 17 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<u8>) 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",
] }
Expand All @@ -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 }
Expand Down
83 changes: 83 additions & 0 deletions src/json_u64.rs
Original file line number Diff line number Diff line change
@@ -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<JsonU64> for u64 {
fn from(src: JsonU64) -> u64 {
src.0
}
}

impl AsRef<u64> 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<JsonU64>,
block: JsonU64,
}

#[test]
fn serialize_jsonu64_struct() {
let the_struct = TestStruct {
nums: [0, 1, 2, u64::MAX]
.iter()
.map(Into::<JsonU64>::into)
.collect(),
block: JsonU64(u64::MAX - 1),
};
let serialized = crate::serialize(&the_struct).unwrap();
let deserialized =
crate::deserialize::<TestStruct>(&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")
);
}
}
177 changes: 67 additions & 110 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,127 +1,113 @@
// 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};
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<serde_cbor::Error> for Error {
fn from(src: serde_cbor::Error) -> Self {
Self(src)
impl<T: Debug> From<CiboriumError<T>> for Error {
fn from(src: CiboriumError<T>) -> 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<serde_cbor::Error> 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<T: ?Sized>(value: &T) -> Result<Vec<u8>, encode::Error>
where
T: Serialize + Sized,
{
Ok(serde_cbor::to_vec(value)?)
let mut writer = Vec::new();
jcape marked this conversation as resolved.
Show resolved Hide resolved
// 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 invallible");
jcape marked this conversation as resolved.
Show resolved Hide resolved

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<T, decode::Error>
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<T: Message>(value: &T) -> Vec<u8> {
value.encode_to_vec()
}

/// Decode the protobuf-encoded bytes into a data structure using prost::Message.
pub fn decode<T: Message + Default>(buf: &[u8]) -> Result<T, DecodeError> {
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<JsonU64> for u64 {
fn from(src: JsonU64) -> u64 {
src.0
}
}

impl AsRef<u64> 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
Expand All @@ -147,15 +133,16 @@ pub fn round_trip_message<SRC: Message + Eq + Default, DEST: protobuf::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);
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::<String>(&serialized).unwrap();
assert_eq!(&deserialized, THE_STRING);
}

#[derive(PartialEq, Serialize, Deserialize, Debug)]
Expand All @@ -166,44 +153,14 @@ mod test {
}

#[test]
fn test_serialize_struct() {
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::<TestStruct>(&serialized).unwrap();
assert_eq!(deserialized, the_struct);
}
}

#[cfg(all(test, feature = "serde_with"))]
mod json_u64_tests {
use super::*;
use serde::{Deserialize, Serialize};

#[derive(PartialEq, Serialize, Deserialize, Debug)]
struct TestStruct {
nums: Vec<JsonU64>,
block: JsonU64,
}

#[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),
};
let serialized = serialize(&the_struct).unwrap();
let deserialized: TestStruct = deserialize(&serialized).unwrap();
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).unwrap());
assert_eq!(the_struct, serde_json::from_str(expected_json).unwrap());
}
}