diff --git a/piecrust-uplink/CHANGELOG.md b/piecrust-uplink/CHANGELOG.md index 6f631da3..1949d008 100644 --- a/piecrust-uplink/CHANGELOG.md +++ b/piecrust-uplink/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add serde `Serialize` and `Deserialize` implementations for `ContractId` and `Event` [#414] +- Add `serde`, `hex`, `base64` and `serde_json` optional dependencies [#414] +- Add `serde` feature [#414] + ## [0.17.1] - 2024-09-24 ### Changed @@ -216,6 +222,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - First `piecrust-uplink` release +[#414]: https://github.com/dusk-network/piecrust/issues/414 [#375]: https://github.com/dusk-network/piecrust/issues/375 [#365]: https://github.com/dusk-network/piecrust/issues/365 [#357]: https://github.com/dusk-network/piecrust/issues/357 diff --git a/piecrust-uplink/Cargo.toml b/piecrust-uplink/Cargo.toml index fc4b7a68..af21bb86 100644 --- a/piecrust-uplink/Cargo.toml +++ b/piecrust-uplink/Cargo.toml @@ -16,10 +16,18 @@ license = "MPL-2.0" rkyv = { version = "0.7", default-features = false, features = ["size_32", "alloc", "validation"] } bytecheck = { version = "0.6", default-features = false } dlmalloc = { version = "0.2", optional = true, features = ["global"] } +serde = { version = "1.0", optional = true } +hex = { version = "0.4" , optional = true } +base64 = { version = "0.22", optional = true } +serde_json = { version = "1.0", optional = true } + +[dev-dependencies] +rand = "0.8" [features] abi = [] debug = [] +serde = ["dep:serde", "serde_json", "hex", "base64"] [package.metadata.docs.rs] all-features = true diff --git a/piecrust-uplink/src/lib.rs b/piecrust-uplink/src/lib.rs index 2745ca97..7633d20a 100644 --- a/piecrust-uplink/src/lib.rs +++ b/piecrust-uplink/src/lib.rs @@ -78,6 +78,9 @@ pub use types::*; mod error; pub use error::*; +#[cfg(feature = "serde")] +mod serde_support; + /// How many bytes to use for scratch space when serializing pub const SCRATCH_BUF_BYTES: usize = 1024; diff --git a/piecrust-uplink/src/serde_support.rs b/piecrust-uplink/src/serde_support.rs new file mode 100644 index 00000000..7f4b20d1 --- /dev/null +++ b/piecrust-uplink/src/serde_support.rs @@ -0,0 +1,140 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use alloc::format; +use alloc::string::String; + +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use base64::Engine; +use serde::de::{Error as SerdeError, MapAccess, Visitor}; +use serde::ser::SerializeStruct; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{ContractId, Event, CONTRACT_ID_BYTES}; + +impl Serialize for ContractId { + fn serialize( + &self, + serializer: S, + ) -> Result { + let s = hex::encode(self.to_bytes()); + serializer.serialize_str(&s) + } +} + +impl<'de> Deserialize<'de> for ContractId { + fn deserialize>( + deserializer: D, + ) -> Result { + let s = String::deserialize(deserializer)?; + let decoded = hex::decode(&s).map_err(SerdeError::custom)?; + let decoded_len = decoded.len(); + let byte_length_str = format!("{CONTRACT_ID_BYTES}"); + let bytes: [u8; CONTRACT_ID_BYTES] = + decoded.try_into().map_err(|_| { + SerdeError::invalid_length( + decoded_len, + &byte_length_str.as_str(), + ) + })?; + Ok(bytes.into()) + } +} + +impl Serialize for Event { + fn serialize( + &self, + serializer: S, + ) -> Result { + let mut struct_ser = serializer.serialize_struct("Event", 3)?; + struct_ser.serialize_field("source", &self.source)?; + struct_ser.serialize_field("topic", &self.topic)?; + struct_ser + .serialize_field("data", &BASE64_STANDARD.encode(&self.data))?; + struct_ser.end() + } +} + +impl<'de> Deserialize<'de> for Event { + fn deserialize>( + deserializer: D, + ) -> Result { + struct StructVisitor; + + impl<'de> Visitor<'de> for StructVisitor { + type Value = Event; + + fn expecting( + &self, + formatter: &mut alloc::fmt::Formatter, + ) -> alloc::fmt::Result { + formatter + .write_str("a struct with fields: source, topic, and data") + } + + fn visit_map>( + self, + mut map: A, + ) -> Result { + let (mut source, mut topic, mut data) = (None, None, None); + while let Some(key) = map.next_key()? { + match key { + "source" => { + if source.is_some() { + return Err(SerdeError::duplicate_field( + "source", + )); + } + source = Some(map.next_value()?); + } + "topic" => { + if topic.is_some() { + return Err(SerdeError::duplicate_field( + "topic", + )); + } + topic = Some(map.next_value()?); + } + "data" => { + if data.is_some() { + return Err(SerdeError::duplicate_field( + "data", + )); + } + data = Some(map.next_value()?); + } + field => { + return Err(SerdeError::unknown_field( + field, + &["source", "topic", "data"], + )) + } + }; + } + let data: String = + data.ok_or_else(|| SerdeError::missing_field("data"))?; + let data = BASE64_STANDARD.decode(data).map_err(|e| { + SerdeError::custom(format!( + "failed to base64 decode Event data: {e}" + )) + })?; + Ok(Event { + source: source + .ok_or_else(|| SerdeError::missing_field("source"))?, + topic: topic + .ok_or_else(|| SerdeError::missing_field("topic"))?, + data, + }) + } + } + + deserializer.deserialize_struct( + "Event", + &["source", "topic", "data"], + StructVisitor, + ) + } +} diff --git a/piecrust-uplink/tests/serde.rs b/piecrust-uplink/tests/serde.rs new file mode 100644 index 00000000..62c2a02d --- /dev/null +++ b/piecrust-uplink/tests/serde.rs @@ -0,0 +1,85 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +#![cfg(feature = "serde")] + +use piecrust_uplink::{ContractId, Event, CONTRACT_ID_BYTES}; +use rand::rngs::StdRng; +use rand::{RngCore, SeedableRng}; + +fn rand_contract_id(rng: &mut StdRng) -> ContractId { + let mut bytes = [0; CONTRACT_ID_BYTES]; + rng.fill_bytes(&mut bytes); + bytes.into() +} + +fn rand_event(rng: &mut StdRng) -> Event { + let mut data = [0; 50]; + rng.fill_bytes(&mut data); + Event { + source: rand_contract_id(rng), + topic: "a-contract-topic".into(), + data: data.into(), + } +} + +#[test] +fn serde_contract_id() { + let mut rng = StdRng::seed_from_u64(0xdead); + let id: ContractId = rand_contract_id(&mut rng); + let ser = serde_json::to_string(&id).unwrap(); + let deser: ContractId = serde_json::from_str(&ser).unwrap(); + assert_eq!(id, deser); +} + +#[test] +fn serde_event() { + let mut rng = StdRng::seed_from_u64(0xbeef); + let event = rand_event(&mut rng); + let ser = serde_json::to_string(&event).unwrap(); + let deser: Event = serde_json::from_str(&ser).unwrap(); + assert_eq!(event, deser); +} + +#[test] +fn serde_wrong_encoded() { + let wrong_encoded = "wrong-encoded"; + + let contract_id: Result = + serde_json::from_str(&wrong_encoded); + assert!(contract_id.is_err()); +} + +#[test] +fn serde_too_long_encoded() { + let length_33_enc = "\"e4ab9de40283a85d6ea0cd0120500697d8b01c71b7b4b520292252d20937000631\""; + + let contract_id: Result = + serde_json::from_str(&length_33_enc); + assert!(contract_id.is_err()); +} + +#[test] +fn serde_too_short_encoded() { + let length_31_enc = + "\"1751c37a1dca7aa4c048fcc6177194243edc3637bae042e167e4285945e046\""; + + let contract_id: Result = + serde_json::from_str(&length_31_enc); + assert!(contract_id.is_err()); +} + +#[test] +fn serde_event_fields() { + let serde_json_string = "{\"source\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"topic\":\"\",\"data\":\"\"}"; + let event = Event { + source: ContractId::from_bytes([0; CONTRACT_ID_BYTES]), + topic: String::new(), + data: Vec::new(), + }; + let ser = serde_json::to_string(&event).unwrap(); + assert_eq!(serde_json_string, ser); +}