diff --git a/Cargo.lock b/Cargo.lock index 96c8aa4..95c6adb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -348,15 +348,6 @@ dependencies = [ "five8_core", ] -[[package]] -name = "five8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772" -dependencies = [ - "five8_core", -] - [[package]] name = "five8_const" version = "0.1.3" @@ -366,15 +357,6 @@ dependencies = [ "five8_core", ] -[[package]] -name = "five8_const" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5" -dependencies = [ - "five8_core", -] - [[package]] name = "five8_core" version = "0.1.1" @@ -1091,8 +1073,8 @@ dependencies = [ "bytemuck", "bytemuck_derive", "curve25519-dalek", - "five8 0.2.1", - "five8_const 0.1.3", + "five8", + "five8_const", "rand 0.8.5", "solana-atomic-u64", "solana-define-syscall 3.0.0", @@ -1110,9 +1092,8 @@ dependencies = [ "borsh", "bytemuck", "bytemuck_derive", - "five8 1.0.0", - "five8_const 1.0.0", - "solana-program-error", + "serde", + "serde_derive", "wincode", ] @@ -1222,7 +1203,7 @@ checksum = "8a063723b9e84c14d8c0d2cdf0268207dc7adecf546e31251f9e07c7b00b566c" dependencies = [ "bytemuck", "bytemuck_derive", - "five8 0.2.1", + "five8", "solana-atomic-u64", "solana-sanitize", ] @@ -1428,7 +1409,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19bb713a132fe904caa1f86c331d32846048ae517a3ebf52b068ed07a33070db" dependencies = [ - "five8 0.2.1", + "five8", "solana-sanitize", ] @@ -1658,7 +1639,6 @@ dependencies = [ name = "spl-pod" version = "0.7.2" dependencies = [ - "base64", "borsh", "bytemuck", "bytemuck_derive", diff --git a/pod/Cargo.toml b/pod/Cargo.toml index 66b76e8..f0928a7 100644 --- a/pod/Cargo.toml +++ b/pod/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" [features] bytemuck = ["dep:bytemuck", "dep:bytemuck_derive", "solana-address/bytemuck"] -serde = ["dep:serde", "dep:serde_derive", "solana-address/decode"] +serde = ["dep:serde", "dep:serde_derive", "solana-address/serde"] borsh = ["dep:borsh", "solana-address/borsh"] wincode = ["dep:wincode", "dep:wincode-derive"] @@ -26,10 +26,8 @@ wincode = { version = "0.4.4", default-features = false, optional = true } wincode-derive = { version = "0.4.2", optional = true } [dev-dependencies] -base64 = { version = "0.22.1" } serde_json = "1.0.145" -solana-address = { version = "2.2.0", features = ["decode"] } -spl-pod = { path = ".", features = ["bytemuck", "wincode"] } +spl-pod = { path = ".", features = ["bytemuck", "wincode", "borsh"] } test-case = "3.3.1" [lib] diff --git a/pod/src/lib.rs b/pod/src/lib.rs index 3f1be8f..2509506 100644 --- a/pod/src/lib.rs +++ b/pod/src/lib.rs @@ -8,7 +8,6 @@ extern crate alloc; #[cfg(feature = "bytemuck")] pub mod bytemuck; pub mod option; -pub mod optional_keys; pub mod primitives; // Export current sdk types for downstream users building with a different sdk diff --git a/pod/src/option.rs b/pod/src/option.rs index 43802ce..ad5004d 100644 --- a/pod/src/option.rs +++ b/pod/src/option.rs @@ -8,6 +8,13 @@ #[cfg(feature = "bytemuck")] use bytemuck::{Pod, Zeroable}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(feature = "borsh")] +use { + alloc::format, + borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, +}; use { solana_address::{Address, ADDRESS_BYTES}, solana_program_error::ProgramError, @@ -39,6 +46,10 @@ pub trait Nullable: PartialEq + Sized { /// This can be used when a specific value of `T` indicates that its /// value is `None`. #[repr(transparent)] +#[cfg_attr( + feature = "borsh", + derive(BorshDeserialize, BorshSerialize, BorshSchema) +)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct PodOption(T); @@ -118,6 +129,22 @@ impl From for PodOption { } } +impl From> for Option { + fn from(value: PodOption) -> Self { + value.get() + } +} + +impl From> for COption { + fn from(value: PodOption) -> Self { + if value.0.is_none() { + COption::None + } else { + COption::Some(value.0) + } + } +} + impl TryFrom> for PodOption { type Error = ProgramError; @@ -147,55 +174,57 @@ impl Nullable for Address { const NONE: Self = Address::new_from_array([0u8; ADDRESS_BYTES]); } -#[cfg(test)] -mod tests { - #[cfg(feature = "bytemuck")] - use crate::bytemuck::pod_slice_from_bytes; - use {super::*, alloc::vec::Vec}; - const ID: Address = Address::from_str_const("TestSysvar111111111111111111111111111111111"); - - #[cfg(feature = "bytemuck")] - #[test] - fn test_pod_option_address() { - let some_address = PodOption::from(ID); - assert_eq!(some_address.get(), Some(ID)); - - let none_address = PodOption::from(Address::default()); - assert_eq!(none_address.get(), None); - - let mut data = Vec::with_capacity(64); - data.extend_from_slice(ID.as_ref()); - data.extend_from_slice(&[0u8; 32]); +#[cfg(feature = "serde")] +impl Serialize for PodOption +where + T: Nullable + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.0.is_none() { + serializer.serialize_none() + } else { + serializer.serialize_some(&self.0) + } + } +} - let values = pod_slice_from_bytes::>(&data).unwrap(); - assert_eq!(values[0], PodOption::from(ID)); - assert_eq!(values[1], PodOption::from(Address::default())); +#[cfg(feature = "serde")] +impl<'de, T> Deserialize<'de> for PodOption +where + T: Nullable + Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let option = Option::::deserialize(deserializer)?; + match option { + Some(value) if value.is_none() => Err(serde::de::Error::custom( + "Invalid PodOption encoding: Some(value) cannot equal the none marker.", + )), + Some(value) => Ok(PodOption(value)), + None => Ok(PodOption(T::NONE)), + } + } +} - let option_pubkey = Some(ID); - let pod_option_pubkey: PodOption
= option_pubkey.try_into().unwrap(); - assert_eq!(pod_option_pubkey, PodOption::from(ID)); - assert_eq!( - pod_option_pubkey, - PodOption::try_from(option_pubkey).unwrap() - ); +#[cfg(test)] +mod tests { + use super::*; - let coption_pubkey = COption::Some(ID); - let pod_option_pubkey: PodOption
= coption_pubkey.try_into().unwrap(); - assert_eq!(pod_option_pubkey, PodOption::from(ID)); - assert_eq!( - pod_option_pubkey, - PodOption::try_from(coption_pubkey).unwrap() - ); - } + const ID: Address = Address::new_from_array([8; ADDRESS_BYTES]); #[test] fn test_try_from_option() { - let some_pubkey = Some(ID); - assert_eq!(PodOption::try_from(some_pubkey).unwrap(), PodOption(ID)); + let some_address = Some(ID); + assert_eq!(PodOption::try_from(some_address).unwrap(), PodOption(ID)); - let none_pubkey = None; + let none_address = None; assert_eq!( - PodOption::try_from(none_pubkey).unwrap(), + PodOption::try_from(none_address).unwrap(), PodOption::from(Address::NONE) ); @@ -204,6 +233,24 @@ mod tests { assert_eq!(err, ProgramError::InvalidArgument); } + #[test] + fn test_try_from_coption_reject_some_zero_address() { + let invalid_option = COption::Some(Address::NONE); + let err = PodOption::try_from(invalid_option).unwrap_err(); + assert_eq!(err, ProgramError::InvalidArgument); + } + + #[test] + fn test_from_pod_option() { + let some = PodOption::from(ID); + let none = PodOption::from(Address::NONE); + + assert_eq!(Option::
::from(some), Some(ID)); + assert_eq!(Option::
::from(none), None); + assert_eq!(COption::
::from(some), COption::Some(ID)); + assert_eq!(COption::
::from(none), COption::None); + } + #[test] fn test_default() { let def = PodOption::
::default(); @@ -219,6 +266,20 @@ mod tests { assert_eq!(none_address.copied(), None); } + #[test] + fn test_as_mut() { + let mut some = PodOption::from(Address::new_from_array([3; ADDRESS_BYTES])); + assert!(some.as_mut().is_some()); + *some.as_mut().unwrap() = Address::new_from_array([4; ADDRESS_BYTES]); + assert_eq!( + some.get(), + Some(Address::new_from_array([4; ADDRESS_BYTES])) + ); + + let mut none = PodOption::from(Address::NONE); + assert!(none.as_mut().is_none()); + } + #[derive(Clone, Debug, PartialEq)] struct TestNonCopyNullable([u8; 4]); @@ -226,6 +287,10 @@ mod tests { const NONE: Self = Self([0u8; 4]); } + impl Nullable for u64 { + const NONE: Self = 0; + } + #[test] fn test_cloned_with_non_copy_nullable() { let some = PodOption::from(TestNonCopyNullable([1, 2, 3, 4])); @@ -234,4 +299,150 @@ mod tests { let none = PodOption::from(TestNonCopyNullable::NONE); assert_eq!(none.cloned(), None); } + + #[cfg(feature = "borsh")] + mod borsh_tests { + use {super::*, alloc::vec}; + + #[test] + fn test_borsh_roundtrip_and_encoding() { + let some = PodOption::from(Address::new_from_array([1; ADDRESS_BYTES])); + let none = PodOption::from(Address::NONE); + + let some_bytes = borsh::to_vec(&some).unwrap(); + let none_bytes = borsh::to_vec(&none).unwrap(); + + assert_eq!(some_bytes, vec![1; ADDRESS_BYTES]); + assert_eq!(none_bytes, vec![0; ADDRESS_BYTES]); + assert_eq!( + borsh::from_slice::>(&some_bytes).unwrap(), + some + ); + assert_eq!( + borsh::from_slice::>(&none_bytes).unwrap(), + none + ); + assert!(borsh::from_slice::>(&[]).is_err()); + } + } + + #[cfg(feature = "serde")] + mod serde_tests { + use {super::*, alloc::string::ToString}; + + #[test] + fn test_serde_some() { + let some = PodOption::from(Address::new_from_array([1; ADDRESS_BYTES])); + let serialized = serde_json::to_string(&some).unwrap(); + assert_eq!( + &serialized, + "[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]" + ); + let deserialized = serde_json::from_str::>(&serialized).unwrap(); + assert_eq!(some, deserialized); + } + + #[test] + fn test_serde_none() { + let none = PodOption::from(Address::new_from_array([0; ADDRESS_BYTES])); + let serialized = serde_json::to_string(&none).unwrap(); + assert_eq!(&serialized, "null"); + let deserialized = serde_json::from_str::>(&serialized).unwrap(); + assert_eq!(none, deserialized); + } + + #[test] + fn test_serde_reject_zero_address_bytes() { + let zero_bytes = "[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]"; + assert!(serde_json::from_str::>(zero_bytes).is_err()); + } + + #[test] + fn test_serde_reject_invalid_address_string() { + assert!(serde_json::from_str::>("\"not_an_address\"").is_err()); + } + + #[test] + fn test_serde_u64_some() { + let some = PodOption::from(7u64); + let serialized = serde_json::to_string(&some).unwrap(); + assert_eq!(serialized, "7"); + let deserialized = serde_json::from_str::>(&serialized).unwrap(); + assert_eq!(deserialized, some); + } + + #[test] + fn test_serde_u64_none() { + let deserialized = serde_json::from_str::>("null").unwrap(); + assert_eq!(deserialized, PodOption::from(0)); + } + + #[test] + fn test_serde_u64_none_marker_error_message() { + let err = serde_json::from_str::>("0").unwrap_err(); + let message = err.to_string(); + assert!(message.contains("PodOption encoding")); + assert!(message.contains("none marker")); + } + + #[test] + fn test_serde_u64_reject_invalid_input() { + assert!(serde_json::from_str::>("\"abc\"").is_err()); + assert!(serde_json::from_str::>("{}").is_err()); + } + } + + #[cfg(feature = "bytemuck")] + mod bytemuck_tests { + use { + super::*, + crate::bytemuck::{pod_from_bytes, pod_slice_from_bytes}, + alloc::vec::Vec, + }; + + #[test] + fn test_pod_option_address() { + let some_address = PodOption::from(ID); + assert_eq!(some_address.get(), Some(ID)); + + let none_address = PodOption::from(Address::default()); + assert_eq!(none_address.get(), None); + + let mut data = Vec::with_capacity(64); + data.extend_from_slice(ID.as_ref()); + data.extend_from_slice(&[0u8; 32]); + + let values = pod_slice_from_bytes::>(&data).unwrap(); + assert_eq!(values[0], PodOption::from(ID)); + assert_eq!(values[1], PodOption::from(Address::default())); + } + + #[test] + fn test_pod_from_bytes() { + assert_eq!( + Option::
::from( + *pod_from_bytes::>(&[1; ADDRESS_BYTES]).unwrap() + ), + Some(Address::new_from_array([1; ADDRESS_BYTES])), + ); + assert_eq!( + Option::
::from( + *pod_from_bytes::>(&[0; ADDRESS_BYTES]).unwrap() + ), + None, + ); + assert_eq!( + pod_from_bytes::>(&[]).unwrap_err(), + ProgramError::InvalidArgument + ); + assert_eq!( + pod_from_bytes::>(&[0; 1]).unwrap_err(), + ProgramError::InvalidArgument + ); + assert_eq!( + pod_from_bytes::>(&[1; 1]).unwrap_err(), + ProgramError::InvalidArgument + ); + } + } } diff --git a/pod/src/optional_keys.rs b/pod/src/optional_keys.rs deleted file mode 100644 index ed5c569..0000000 --- a/pod/src/optional_keys.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Optional addresses that can be used a `Pod`s -#[cfg(any(feature = "borsh", feature = "serde"))] -use alloc::string::ToString; -#[cfg(feature = "borsh")] -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -#[cfg(feature = "bytemuck")] -use bytemuck_derive::{Pod, Zeroable}; -#[cfg(feature = "serde")] -use { - core::{convert::TryFrom, fmt, str::FromStr}, - serde::de::{Error, Unexpected, Visitor}, - serde::{Deserialize, Deserializer, Serialize, Serializer}, -}; -use {solana_address::Address, solana_program_error::ProgramError, solana_program_option::COption}; - -/// A Pubkey that encodes `None` as all `0`, meant to be usable as a `Pod` type, -/// similar to all `NonZero*` number types from the `bytemuck` library. -#[cfg_attr( - feature = "borsh", - derive(BorshDeserialize, BorshSerialize, BorshSchema) -)] -#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[repr(transparent)] -pub struct OptionalNonZeroPubkey(pub Address); -impl TryFrom> for OptionalNonZeroPubkey { - type Error = ProgramError; - fn try_from(p: Option
) -> Result { - match p { - None => Ok(Self(Address::default())), - Some(pubkey) => { - if pubkey == Address::default() { - Err(ProgramError::InvalidArgument) - } else { - Ok(Self(pubkey)) - } - } - } - } -} -impl TryFrom> for OptionalNonZeroPubkey { - type Error = ProgramError; - fn try_from(p: COption
) -> Result { - match p { - COption::None => Ok(Self(Address::default())), - COption::Some(pubkey) => { - if pubkey == Address::default() { - Err(ProgramError::InvalidArgument) - } else { - Ok(Self(pubkey)) - } - } - } - } -} -impl From for Option
{ - fn from(p: OptionalNonZeroPubkey) -> Self { - if p.0 == Address::default() { - None - } else { - Some(p.0) - } - } -} -impl From for COption
{ - fn from(p: OptionalNonZeroPubkey) -> Self { - if p.0 == Address::default() { - COption::None - } else { - COption::Some(p.0) - } - } -} - -#[cfg(feature = "serde")] -impl Serialize for OptionalNonZeroPubkey { - fn serialize(&self, s: S) -> Result - where - S: Serializer, - { - if self.0 == Address::default() { - s.serialize_none() - } else { - s.serialize_some(&self.0.to_string()) - } - } -} - -#[cfg(feature = "serde")] -/// Visitor for deserializing `OptionalNonZeroPubkey` -struct OptionalNonZeroPubkeyVisitor; - -#[cfg(feature = "serde")] -impl Visitor<'_> for OptionalNonZeroPubkeyVisitor { - type Value = OptionalNonZeroPubkey; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a Pubkey in base58 or `null`") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - let pkey = Address::from_str(v) - .map_err(|_| Error::invalid_value(Unexpected::Str(v), &"value string"))?; - - OptionalNonZeroPubkey::try_from(Some(pkey)) - .map_err(|_| Error::custom("Failed to convert from pubkey")) - } - - fn visit_unit(self) -> Result - where - E: Error, - { - OptionalNonZeroPubkey::try_from(None).map_err(|e| Error::custom(e.to_string())) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for OptionalNonZeroPubkey { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(OptionalNonZeroPubkeyVisitor) - } -} - -#[cfg(test)] -mod tests { - #[cfg(feature = "bytemuck")] - use crate::bytemuck::pod_from_bytes; - use {super::*, solana_address::ADDRESS_BYTES}; - - #[cfg(feature = "bytemuck")] - #[test] - fn test_pod_non_zero_option() { - assert_eq!( - Some(Address::new_from_array([1; ADDRESS_BYTES])), - Option::
::from( - *pod_from_bytes::(&[1; ADDRESS_BYTES]).unwrap() - ) - ); - assert_eq!( - None, - Option::
::from( - *pod_from_bytes::(&[0; ADDRESS_BYTES]).unwrap() - ) - ); - assert_eq!( - pod_from_bytes::(&[]).unwrap_err(), - ProgramError::InvalidArgument - ); - assert_eq!( - pod_from_bytes::(&[0; 1]).unwrap_err(), - ProgramError::InvalidArgument - ); - assert_eq!( - pod_from_bytes::(&[1; 1]).unwrap_err(), - ProgramError::InvalidArgument - ); - } - - #[cfg(feature = "serde")] - #[test] - fn test_pod_non_zero_option_serde_some() { - let optional_non_zero_pubkey_some = - OptionalNonZeroPubkey(Address::new_from_array([1; ADDRESS_BYTES])); - let serialized_some = serde_json::to_string(&optional_non_zero_pubkey_some).unwrap(); - assert_eq!( - &serialized_some, - "\"4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi\"" - ); - - let deserialized_some = - serde_json::from_str::(&serialized_some).unwrap(); - assert_eq!(optional_non_zero_pubkey_some, deserialized_some); - } - - #[cfg(feature = "serde")] - #[test] - fn test_pod_non_zero_option_serde_none() { - let optional_non_zero_pubkey_none = - OptionalNonZeroPubkey(Address::new_from_array([0; ADDRESS_BYTES])); - let serialized_none = serde_json::to_string(&optional_non_zero_pubkey_none).unwrap(); - assert_eq!(&serialized_none, "null"); - - let deserialized_none = - serde_json::from_str::(&serialized_none).unwrap(); - assert_eq!(optional_non_zero_pubkey_none, deserialized_none); - } -}