From cb6d60e3b5f36681e7b7564e2da858d3e0aaeca4 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Thu, 26 Feb 2026 10:53:27 +0100 Subject: [PATCH 1/3] remove optional_keys --- pod/src/lib.rs | 1 - pod/src/option.rs | 218 ++++++++++++++++++++++++++++++++++++--- pod/src/optional_keys.rs | 193 ---------------------------------- 3 files changed, 203 insertions(+), 209 deletions(-) delete mode 100644 pod/src/optional_keys.rs 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..0bac014 100644 --- a/pod/src/option.rs +++ b/pod/src/option.rs @@ -6,6 +6,8 @@ //! [`Option`](https://doc.rust-lang.org/std/num/type.NonZeroU64.html) //! and provide the same memory layout optimization. +#[cfg(feature = "borsh")] +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; #[cfg(feature = "bytemuck")] use bytemuck::{Pod, Zeroable}; use { @@ -13,6 +15,12 @@ use { solana_program_error::ProgramError, solana_program_option::COption, }; +#[cfg(feature = "serde")] +use { + core::{fmt, str::FromStr}, + serde::de::{Error, Unexpected, Visitor}, + serde::{Deserialize, Deserializer, Serialize, Serializer}, +}; /// Trait for types that can be `None`. /// @@ -39,6 +47,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 +130,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,11 +175,67 @@ impl Nullable for Address { const NONE: Self = Address::new_from_array([0u8; ADDRESS_BYTES]); } +#[cfg(feature = "serde")] +impl Serialize for PodOption
{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.0.is_none() { + serializer.serialize_none() + } else { + serializer.serialize_some(&self.0.to_string()) + } + } +} + +#[cfg(feature = "serde")] +struct PodOptionAddressVisitor; + +#[cfg(feature = "serde")] +impl Visitor<'_> for PodOptionAddressVisitor { + type Value = PodOption
; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an Address 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"))?; + PodOption::try_from(Some(pkey)).map_err(|_| Error::custom("Failed to convert from address")) + } + + fn visit_unit(self) -> Result + where + E: Error, + { + Ok(PodOption::default()) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for PodOption
{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(PodOptionAddressVisitor) + } +} + #[cfg(test)] mod tests { + use { + super::*, + alloc::vec::Vec, + crate::bytemuck::{pod_from_bytes, pod_slice_from_bytes}, + }; #[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")] @@ -171,31 +255,31 @@ mod tests { assert_eq!(values[0], PodOption::from(ID)); assert_eq!(values[1], PodOption::from(Address::default())); - let option_pubkey = Some(ID); - let pod_option_pubkey: PodOption
= option_pubkey.try_into().unwrap(); - assert_eq!(pod_option_pubkey, PodOption::from(ID)); + let option_address = Some(ID); + let pod_option_address: PodOption
= option_address.try_into().unwrap(); + assert_eq!(pod_option_address, PodOption::from(ID)); assert_eq!( - pod_option_pubkey, - PodOption::try_from(option_pubkey).unwrap() + pod_option_address, + PodOption::try_from(option_address).unwrap() ); - let coption_pubkey = COption::Some(ID); - let pod_option_pubkey: PodOption
= coption_pubkey.try_into().unwrap(); - assert_eq!(pod_option_pubkey, PodOption::from(ID)); + let coption_address = COption::Some(ID); + let pod_option_address: PodOption
= coption_address.try_into().unwrap(); + assert_eq!(pod_option_address, PodOption::from(ID)); assert_eq!( - pod_option_pubkey, - PodOption::try_from(coption_pubkey).unwrap() + pod_option_address, + PodOption::try_from(coption_address).unwrap() ); } #[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 +288,52 @@ 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_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 + ); + } + #[test] fn test_default() { let def = PodOption::
::default(); @@ -234,4 +364,62 @@ mod tests { let none = PodOption::from(TestNonCopyNullable::NONE); assert_eq!(none.cloned(), None); } + + #[cfg(feature = "borsh")] + #[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")] + #[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, + "\"4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi\"" + ); + let deserialized = serde_json::from_str::>(&serialized).unwrap(); + assert_eq!(some, deserialized); + } + + #[cfg(feature = "serde")] + #[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); + } + + #[cfg(feature = "serde")] + #[test] + fn test_serde_reject_zero_address_string() { + let zero_str = format!("\"{}\"", Address::NONE); + assert!(serde_json::from_str::>(&zero_str).is_err()); + } + + #[cfg(feature = "serde")] + #[test] + fn test_serde_reject_invalid_address_string() { + assert!(serde_json::from_str::>("\"not_an_address\"").is_err()); + } } 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); - } -} From 83f78e3f0b03164ea70e6953869d796e4b2b8a7e Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Fri, 27 Feb 2026 18:47:59 +0100 Subject: [PATCH 2/3] Review updates --- Cargo.lock | 31 +---- pod/Cargo.toml | 6 +- pod/src/option.rs | 337 +++++++++++++++++++++++++--------------------- 3 files changed, 189 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96c8aa4..9f8d697 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", ] diff --git a/pod/Cargo.toml b/pod/Cargo.toml index 66b76e8..d005a31 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"] @@ -28,8 +28,8 @@ 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"] } +solana-address = { version = "2.2.0", features = ["serde"] } +spl-pod = { path = ".", features = ["bytemuck", "wincode", "borsh"] } test-case = "3.3.1" [lib] diff --git a/pod/src/option.rs b/pod/src/option.rs index 0bac014..ad5004d 100644 --- a/pod/src/option.rs +++ b/pod/src/option.rs @@ -6,21 +6,20 @@ //! [`Option`](https://doc.rust-lang.org/std/num/type.NonZeroU64.html) //! and provide the same memory layout optimization. -#[cfg(feature = "borsh")] -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; #[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, solana_program_option::COption, }; -#[cfg(feature = "serde")] -use { - core::{fmt, str::FromStr}, - serde::de::{Error, Unexpected, Visitor}, - serde::{Deserialize, Deserializer, Serialize, Serializer}, -}; /// Trait for types that can be `None`. /// @@ -176,7 +175,10 @@ impl Nullable for Address { } #[cfg(feature = "serde")] -impl Serialize for PodOption
{ +impl Serialize for PodOption +where + T: Nullable + Serialize, +{ fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -184,93 +186,36 @@ impl Serialize for PodOption
{ if self.0.is_none() { serializer.serialize_none() } else { - serializer.serialize_some(&self.0.to_string()) + serializer.serialize_some(&self.0) } } } #[cfg(feature = "serde")] -struct PodOptionAddressVisitor; - -#[cfg(feature = "serde")] -impl Visitor<'_> for PodOptionAddressVisitor { - type Value = PodOption
; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an Address 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"))?; - PodOption::try_from(Some(pkey)).map_err(|_| Error::custom("Failed to convert from address")) - } - - fn visit_unit(self) -> Result - where - E: Error, - { - Ok(PodOption::default()) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for PodOption
{ +impl<'de, T> Deserialize<'de> for PodOption +where + T: Nullable + Deserialize<'de>, +{ fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - deserializer.deserialize_any(PodOptionAddressVisitor) + 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)), + } } } #[cfg(test)] mod tests { - use { - super::*, - alloc::vec::Vec, - crate::bytemuck::{pod_from_bytes, pod_slice_from_bytes}, - }; - #[cfg(feature = "bytemuck")] - use crate::bytemuck::pod_slice_from_bytes; - 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]); - - let values = pod_slice_from_bytes::>(&data).unwrap(); - assert_eq!(values[0], PodOption::from(ID)); - assert_eq!(values[1], PodOption::from(Address::default())); - - let option_address = Some(ID); - let pod_option_address: PodOption
= option_address.try_into().unwrap(); - assert_eq!(pod_option_address, PodOption::from(ID)); - assert_eq!( - pod_option_address, - PodOption::try_from(option_address).unwrap() - ); + use super::*; - let coption_address = COption::Some(ID); - let pod_option_address: PodOption
= coption_address.try_into().unwrap(); - assert_eq!(pod_option_address, PodOption::from(ID)); - assert_eq!( - pod_option_address, - PodOption::try_from(coption_address).unwrap() - ); - } + const ID: Address = Address::new_from_array([8; ADDRESS_BYTES]); #[test] fn test_try_from_option() { @@ -306,34 +251,6 @@ mod tests { assert_eq!(COption::
::from(none), COption::None); } - #[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 - ); - } - #[test] fn test_default() { let def = PodOption::
::default(); @@ -349,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]); @@ -356,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])); @@ -366,60 +301,148 @@ mod tests { } #[cfg(feature = "borsh")] - #[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()); + 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")] - #[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, - "\"4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi\"" - ); - let deserialized = serde_json::from_str::>(&serialized).unwrap(); - assert_eq!(some, deserialized); - } + 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); + } - #[cfg(feature = "serde")] - #[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_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); + } - #[cfg(feature = "serde")] - #[test] - fn test_serde_reject_zero_address_string() { - let zero_str = format!("\"{}\"", Address::NONE); - assert!(serde_json::from_str::>(&zero_str).is_err()); + #[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 = "serde")] - #[test] - fn test_serde_reject_invalid_address_string() { - assert!(serde_json::from_str::>("\"not_an_address\"").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 + ); + } } } From 8328549f820fffa57c2fb06354408ef290442149 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Fri, 27 Feb 2026 22:43:29 +0100 Subject: [PATCH 3/3] Remove unused dev deps --- Cargo.lock | 1 - pod/Cargo.toml | 2 -- 2 files changed, 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f8d697..95c6adb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1639,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 d005a31..f0928a7 100644 --- a/pod/Cargo.toml +++ b/pod/Cargo.toml @@ -26,9 +26,7 @@ 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 = ["serde"] } spl-pod = { path = ".", features = ["bytemuck", "wincode", "borsh"] } test-case = "3.3.1"