diff --git a/Cargo.lock b/Cargo.lock index 339ec94..43ca230 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,8 +13,6 @@ dependencies = [ "amplify_num", "amplify_syn", "ascii", - "serde", - "stringly_conversions", "wasm-bindgen", ] @@ -47,7 +45,6 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afed304556696656d2d71495e1e5f2c4b524a3fb6eb0f2f3778ffc482a40b8a8" dependencies = [ - "serde", "wasm-bindgen", ] @@ -73,9 +70,6 @@ name = "ascii" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" -dependencies = [ - "serde", -] [[package]] name = "async-trait" @@ -419,12 +413,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -611,16 +599,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_str_helpers" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b744a7c94f2f3785496af33a0d93857dfc0c521e25c38e993e9c5bb45f09c841" -dependencies = [ - "serde", - "serde_derive", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -646,16 +624,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" -[[package]] -name = "stringly_conversions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff63080f492dd4d289ffcaed8d7ece38adfb423db910eb342c0e04d409536a7a" -dependencies = [ - "paste", - "serde_str_helpers", -] - [[package]] name = "syn" version = "1.0.109" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 5144a50..ceec4d6 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -58,7 +58,6 @@ float = [ ] serde = [ "serde_crate", - "amplify/serde" ] bitcoin = [ "dep:bitcoin", diff --git a/rust/src/bitcoin_types.rs b/rust/src/bitcoin_types.rs index a8c56f2..58df205 100644 --- a/rust/src/bitcoin_types.rs +++ b/rust/src/bitcoin_types.rs @@ -398,7 +398,10 @@ impl Sats { derive(Serialize, Deserialize), serde(crate = "serde_crate", transparent) )] -pub struct ScriptBytes(pub Confined, 0, U32>); +pub struct ScriptBytes( + #[cfg_attr(feature = "serde", serde(with = "crate::serde_helpers::confined"))] + pub Confined, 0, U32>, +); impl TryFrom> for ScriptBytes { type Error = amplify::confinement::Error; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index e5c3981..94e1d34 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -60,6 +60,8 @@ mod writer; mod util; mod primitives; mod embedded; +#[cfg(feature = "serde")] +pub mod serde_helpers; pub mod stl; #[cfg(test)] pub(crate) mod test; diff --git a/rust/src/serde_helpers.rs b/rust/src/serde_helpers.rs new file mode 100644 index 0000000..ed35742 --- /dev/null +++ b/rust/src/serde_helpers.rs @@ -0,0 +1,302 @@ +// Strict encoding library for deterministic binary serialization. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2026 RGB-Tools developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +//! Serde helpers for `amplify` types, to be used with `#[serde(with = "...")]`. +//! +//! `amplify` provides its own serde implementations for these types, but only under its `serde` +//! feature, which also pulls in the unmaintained `paste` crate through `stringly_conversions`. +//! These helpers produce the very same representation, so they can be used instead of enabling +//! `amplify/serde`. + +/// (De)serialization of [`amplify::confinement::Confined`] collections, represented as the +/// unconfined collection itself. +pub mod confined { + use amplify::confinement::{Collection, Confined}; + use serde_crate::de::Error; + use serde_crate::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize( + confined: &Confined, + serializer: S, + ) -> Result + where + C: Collection + Serialize, + S: Serializer, + { + confined.as_unconfined().serialize(serializer) + } + + pub fn deserialize<'de, C, D, const MIN: usize, const MAX: usize>( + deserializer: D, + ) -> Result, D::Error> + where + C: Collection + Deserialize<'de>, + D: Deserializer<'de>, + { + let collection = C::deserialize(deserializer)?; + Confined::try_from(collection).map_err(D::Error::custom) + } +} + +/// (De)serialization of [`amplify::Array`] of bytes, represented as a hex string for +/// human-readable formats and as a tuple of bytes otherwise. +pub mod byte_array { + use core::fmt; + + use amplify::hex::{FromHex, ToHex}; + use amplify::Array; + use serde_crate::de::{Error, SeqAccess, Visitor}; + use serde_crate::ser::SerializeTuple; + use serde_crate::{Deserialize, Deserializer, Serializer}; + + pub fn serialize( + array: &Array, + serializer: S, + ) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&array.to_hex()) + } else { + let mut ser = serializer.serialize_tuple(LEN)?; + for byte in array.as_slice() { + ser.serialize_element(byte)?; + } + ser.end() + } + } + + pub fn deserialize<'de, D, const LEN: usize, const REVERSE_STR: bool>( + deserializer: D, + ) -> Result, D::Error> + where D: Deserializer<'de> { + if deserializer.is_human_readable() { + let string = String::deserialize(deserializer)?; + Array::from_hex(&string).map_err(|_| D::Error::custom("wrong hex data")) + } else { + struct ArrayVisitor; + + impl<'de, const LEN: usize> Visitor<'de> for ArrayVisitor { + type Value = [u8; LEN]; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "an array of length {LEN}") + } + + fn visit_seq(self, mut seq: A) -> Result<[u8; LEN], A::Error> + where A: SeqAccess<'de> { + let mut arr = [0; LEN]; + for (i, el) in arr.iter_mut().enumerate() { + *el = seq + .next_element()? + .ok_or_else(|| A::Error::invalid_length(i, &self))?; + } + Ok(arr) + } + } + + deserializer + .deserialize_tuple(LEN, ArrayVisitor::) + .map(Array::::from) + } + } +} + +/// (De)serialization of `amplify` big integers, represented by their big-endian bytes: as a hex +/// string for human-readable formats and as a byte string otherwise. +pub mod big_int { + use core::fmt; + use core::marker::PhantomData; + + use amplify::hex::{FromHex, ToHex}; + use serde_crate::de::{Error, Unexpected, Visitor}; + use serde_crate::{Deserializer, Serializer}; + + /// Big integers convertible from/into big-endian bytes. + pub trait BigIntBytes: Copy + Sized { + /// Big-endian byte representation of the integer. + type Bytes: AsRef<[u8]>; + /// Length of [`Self::Bytes`]. + const LEN: usize; + + fn to_be_bytes(self) -> Self::Bytes; + fn from_be_slice(bytes: &[u8]) -> Option; + } + + macro_rules! impl_big_int_bytes { + ($ty:ty, $len:literal) => { + impl BigIntBytes for $ty { + type Bytes = [u8; $len]; + const LEN: usize = $len; + + fn to_be_bytes(self) -> Self::Bytes { <$ty>::to_be_bytes(self) } + fn from_be_slice(bytes: &[u8]) -> Option { <$ty>::from_be_slice(bytes).ok() } + } + }; + } + + impl_big_int_bytes!(amplify::num::u256, 32); + impl_big_int_bytes!(amplify::num::u512, 64); + impl_big_int_bytes!(amplify::num::u1024, 128); + impl_big_int_bytes!(amplify::num::i256, 32); + impl_big_int_bytes!(amplify::num::i512, 64); + impl_big_int_bytes!(amplify::num::i1024, 128); + + pub fn serialize(value: &T, serializer: S) -> Result + where + T: BigIntBytes, + S: Serializer, + { + let bytes = value.to_be_bytes(); + if serializer.is_human_readable() { + serializer.serialize_str(&bytes.as_ref().to_hex()) + } else { + serializer.serialize_bytes(bytes.as_ref()) + } + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: BigIntBytes, + D: Deserializer<'de>, + { + struct BigIntVisitor(PhantomData); + + impl<'de, T: BigIntBytes> Visitor<'de> for BigIntVisitor { + type Value = T; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} bytes or a hex string with {} characters", T::LEN, T::LEN * 2) + } + + fn visit_str(self, s: &str) -> Result + where E: Error { + let bytes = + Vec::from_hex(s).map_err(|_| E::invalid_value(Unexpected::Str(s), &self))?; + T::from_be_slice(&bytes).ok_or_else(|| E::invalid_length(bytes.len() * 2, &self)) + } + + fn visit_bytes(self, bytes: &[u8]) -> Result + where E: Error { + T::from_be_slice(bytes).ok_or_else(|| E::invalid_length(bytes.len(), &self)) + } + } + + if deserializer.is_human_readable() { + deserializer.deserialize_str(BigIntVisitor(PhantomData)) + } else { + deserializer.deserialize_bytes(BigIntVisitor(PhantomData)) + } + } +} + +/// (De)serialization of `amplify` small integers, represented by the primitive integer they wrap. +/// +/// NB: unlike `amplify`, which derives `serde(transparent)` and thus performs no range check, +/// deserialization here rejects values exceeding the maximum of the target type. +pub mod small_int { + use core::fmt::Display; + + use serde_crate::de::Error; + use serde_crate::{Deserialize, Deserializer, Serialize, Serializer}; + + /// Small integers wrapping a primitive integer. + pub trait SmallIntPrimitive: Copy + Sized { + /// The wrapped primitive integer. + type Inner: Copy + Display + Serialize + for<'de> Deserialize<'de>; + + fn to_inner(self) -> Self::Inner; + fn from_inner(inner: Self::Inner) -> Option; + } + + macro_rules! impl_small_int { + ($ty:ty, $inner:ty) => { + impl SmallIntPrimitive for $ty { + type Inner = $inner; + + fn to_inner(self) -> Self::Inner { self.into() } + fn from_inner(inner: Self::Inner) -> Option { Self::try_from(inner).ok() } + } + }; + } + + impl_small_int!(amplify::num::u1, u8); + impl_small_int!(amplify::num::u2, u8); + impl_small_int!(amplify::num::u3, u8); + impl_small_int!(amplify::num::u4, u8); + impl_small_int!(amplify::num::u5, u8); + impl_small_int!(amplify::num::u6, u8); + impl_small_int!(amplify::num::u7, u8); + impl_small_int!(amplify::num::u10, u16); + impl_small_int!(amplify::num::u12, u16); + impl_small_int!(amplify::num::u14, u16); + impl_small_int!(amplify::num::u20, u32); + impl_small_int!(amplify::num::u24, u32); + impl_small_int!(amplify::num::u40, u64); + impl_small_int!(amplify::num::u48, u64); + impl_small_int!(amplify::num::u56, u64); + + pub fn serialize(value: &T, serializer: S) -> Result + where + T: SmallIntPrimitive, + S: Serializer, + { + value.to_inner().serialize(serializer) + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: SmallIntPrimitive, + D: Deserializer<'de>, + { + let inner = T::Inner::deserialize(deserializer)?; + T::from_inner(inner) + .ok_or_else(|| D::Error::custom(format!("value `{inner}` is out of range"))) + } +} + +/// (De)serialization of [`amplify::confinement::Confined`] ASCII strings, represented as a plain +/// string. +/// +/// [`confined`] can't be used for them, since `AsciiString` implements serde traits only under the +/// `ascii/serde` feature, enabled by `amplify/serde`. +pub mod confined_ascii { + use amplify::ascii::AsciiString; + use amplify::confinement::Confined; + use serde_crate::de::{Error, Unexpected}; + use serde_crate::{Deserialize, Deserializer, Serializer}; + + pub fn serialize( + confined: &Confined, + serializer: S, + ) -> Result + where + S: Serializer, + { + serializer.serialize_str(confined.as_str()) + } + + pub fn deserialize<'de, D, const MIN: usize, const MAX: usize>( + deserializer: D, + ) -> Result, D::Error> + where D: Deserializer<'de> { + let s = String::deserialize(deserializer)?; + let ascii = AsciiString::from_ascii(s.as_str()) + .map_err(|_| D::Error::invalid_value(Unexpected::Str(&s), &"an ascii string"))?; + Confined::try_from(ascii).map_err(D::Error::custom) + } +} diff --git a/rust/src/stl.rs b/rust/src/stl.rs index 992e578..0e0c45e 100644 --- a/rust/src/stl.rs +++ b/rust/src/stl.rs @@ -72,7 +72,6 @@ pub trait RestrictedCharSet: } #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize), serde(crate = "serde_crate", transparent))] pub struct RString< C1: RestrictedCharSet, C: RestrictedCharSet = C1, @@ -221,18 +220,46 @@ impl Serialize + for RString + { + fn serialize(&self, serializer: S) -> Result + where S: Serializer { + serializer.serialize_str(self.s.as_str()) + } + } + impl<'de, C1: RestrictedCharSet, C: RestrictedCharSet, const MIN: usize, const MAX: usize> Deserialize<'de> for RString { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { - let ascii = AsciiString::deserialize(deserializer)?; - Self::try_from(ascii).map_err(D::Error::custom) + deserializer.deserialize_string(RStringVisitor(PhantomData)) + } + } + + struct RStringVisitor(PhantomData<(C1, C)>); + + impl<'de, C1: RestrictedCharSet, C: RestrictedCharSet, const MIN: usize, const MAX: usize> + Visitor<'de> for RStringVisitor + { + type Value = RString; + + fn expecting(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "an ascii string") + } + + fn visit_str(self, s: &str) -> Result { + let ascii = AsciiString::from_ascii(s) + .map_err(|_| E::invalid_value(Unexpected::Str(s), &self))?; + RString::try_from(ascii).map_err(E::custom) } } }