diff --git a/lib/unionlabs/src/bytes.rs b/lib/unionlabs/src/bytes.rs new file mode 100644 index 0000000000..53d84bea71 --- /dev/null +++ b/lib/unionlabs/src/bytes.rs @@ -0,0 +1,256 @@ +use alloc::borrow::Cow; +use core::{cmp::Ordering, fmt, marker::PhantomData, ops::Deref, str::FromStr}; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::hash::hash_v2::{Encoding, HexPrefixed}; + +pub struct Bytes { + bytes: Cow<'static, [u8]>, + __marker: PhantomData E>, +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl Clone for Bytes { + fn clone(&self) -> Self { + Self::new(self.bytes.clone()) + } +} + +impl core::hash::Hash for Bytes { + fn hash(&self, state: &mut H) { + core::hash::Hash::hash(&**self, state); + } +} + +impl Bytes { + #[must_use = "constructing a Bytes has no effect"] + pub fn new(bytes: impl Into>) -> Self { + Self { + bytes: bytes.into(), + __marker: PhantomData, + } + } + + #[must_use = "constructing a Bytes has no effect"] + pub const fn new_static(bytes: &'static [u8]) -> Self { + Self { + bytes: Cow::Borrowed(bytes), + __marker: PhantomData, + } + } + + pub fn iter(&self) -> core::slice::Iter<'_, u8> { + <&Self as IntoIterator>::into_iter(self) + } + + #[must_use = "converting a hash to a hash with a different encoding has no effect"] + #[inline] + pub fn into_encoding(self) -> Bytes { + Bytes::new(self.bytes) + } + + #[must_use = "converting to a vec has no effect"] + pub fn into_vec(self) -> Vec { + self.bytes.into() + } +} + +impl Deref for Bytes { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.bytes + } +} + +impl fmt::Debug for Bytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("Bytes({self})")) + } +} + +impl fmt::Display for Bytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + E::fmt(self, f) + } +} + +impl PartialEq> for Bytes { + fn eq(&self, other: &Bytes) -> bool { + (**self).eq(&**other) + } +} + +impl Eq for Bytes {} + +impl PartialOrd> for Bytes { + fn partial_cmp(&self, other: &Bytes) -> Option { + (**self).partial_cmp(&**other) + } +} + +impl Ord for Bytes { + fn cmp(&self, other: &Self) -> Ordering { + (**self).cmp(&**other) + } +} + +impl Serialize for Bytes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + serializer.collect_str(self) + } else { + ::new(self).serialize(serializer) + } + } +} + +impl<'de, E: Encoding> Deserialize<'de> for Bytes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + String::deserialize(deserializer) + .and_then(|s| s.parse().map_err(::serde::de::Error::custom)) + } else { + <&serde_bytes::Bytes>::deserialize(deserializer).map(|b| Bytes::new(b.to_vec())) + } + } +} + +impl FromStr for Bytes { + type Err = E::Error; + + fn from_str(s: &str) -> Result { + E::decode(s).map(Self::new) + } +} + +impl Default for Bytes { + fn default() -> Self { + Self::new_static(&[]) + } +} + +impl<'a, E: Encoding> IntoIterator for &'a Bytes { + type Item = &'a u8; + type IntoIter = core::slice::Iter<'a, u8>; + + fn into_iter(self) -> core::slice::Iter<'a, u8> { + (**self).iter() + } +} + +impl IntoIterator for Bytes { + type Item = u8; + type IntoIter = alloc::vec::IntoIter; + + #[allow(clippy::unnecessary_to_owned)] + fn into_iter(self) -> Self::IntoIter { + self.bytes.to_vec().into_iter() + } +} + +impl From> for Bytes { + fn from(value: Vec) -> Self { + Self::new(value) + } +} + +impl From<&Vec> for Bytes { + fn from(value: &Vec) -> Self { + Self::new(value.to_owned()) + } +} + +impl From<&[u8]> for Bytes { + fn from(value: &[u8]) -> Self { + Self::new(value.to_owned()) + } +} + +impl From> for Vec { + fn from(value: Bytes) -> Self { + value.deref().into() + } +} + +// TODO: Feature gate rlp across the crate +// #[cfg(feature = "rlp")] +impl rlp::Decodable for Bytes { + fn decode(rlp: &rlp::Rlp) -> Result { + rlp.decoder() + .decode_value(|bytes| Ok(Self::new(bytes.to_owned()))) + } +} + +// TODO: Feature gate rlp across the crate +// #[cfg(feature = "rlp")] +impl rlp::Encodable for Bytes { + fn rlp_append(&self, s: &mut ::rlp::RlpStream) { + s.encoder().encode_value(self.as_ref()); + } +} + +#[cfg(feature = "ethabi")] +impl From for Bytes { + fn from(value: alloy::core::primitives::Bytes) -> Self { + value.0.to_vec().into() + } +} + +#[cfg(feature = "ethabi")] +impl From> for alloy::core::primitives::Bytes { + fn from(value: Bytes) -> Self { + value.deref().to_owned().into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hash::hash_v2::{Base64, HexUnprefixed}; + + const BASE64_STR: &str = "YWJjZA=="; + const HEX_PREFIXED_STR: &str = "0x61626364"; + const HEX_UNPREFIXED_STR: &str = "61626364"; + + const RAW_VALUE: &[u8; 4] = b"abcd"; + + #[test] + fn hex_prefixed() { + let decoded = >::from_str(HEX_PREFIXED_STR).unwrap(); + + assert_eq!(HEX_PREFIXED_STR, decoded.to_string()); + + assert_eq!(&*decoded, b"abcd"); + } + + #[test] + fn hex_unprefixed() { + let decoded = >::from_str(HEX_UNPREFIXED_STR).unwrap(); + + assert_eq!(HEX_UNPREFIXED_STR, decoded.to_string()); + + assert_eq!(&*decoded, b"abcd"); + } + + #[test] + fn base64() { + let decoded = >::from_str(BASE64_STR).unwrap(); + + assert_eq!(BASE64_STR, decoded.to_string()); + + assert_eq!(&*decoded, RAW_VALUE); + } +} diff --git a/lib/unionlabs/src/lib.rs b/lib/unionlabs/src/lib.rs index 14e2e64564..44044b142d 100644 --- a/lib/unionlabs/src/lib.rs +++ b/lib/unionlabs/src/lib.rs @@ -78,6 +78,7 @@ pub mod ics24; pub mod validated; +pub mod bytes; pub mod hash; pub mod encoding;