diff --git a/Cargo.lock b/Cargo.lock index 8867f399..ae4b336e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,9 +690,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -700,9 +700,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1646,8 +1646,12 @@ version = "0.1.0" dependencies = [ "bytemuck", "bytemuck_derive", + "num-derive", + "num-traits", + "num_enum", "solana-program-error", "spl-pod 0.7.2", + "thiserror 2.0.18", ] [[package]] @@ -1658,9 +1662,6 @@ dependencies = [ "borsh", "bytemuck", "bytemuck_derive", - "num-derive", - "num-traits", - "num_enum", "serde", "serde_json", "solana-address 2.2.0", @@ -1668,7 +1669,6 @@ dependencies = [ "solana-program-option", "spl-pod 0.7.2", "test-case", - "thiserror 2.0.18", "wincode", ] diff --git a/list-view/Cargo.toml b/list-view/Cargo.toml index ce5bc821..48773f42 100644 --- a/list-view/Cargo.toml +++ b/list-view/Cargo.toml @@ -9,8 +9,12 @@ edition = "2021" [dependencies] bytemuck = "1.25.0" +num-derive = "0.4.2" +num_enum = "0.7.5" +num-traits = "0.2.19" solana-program-error = "3.0.0" spl-pod = { version = "0.7.2", path = "../pod" } +thiserror = "2.0.18" [dev-dependencies] bytemuck_derive = "1.10.2" diff --git a/pod/src/error.rs b/list-view/src/error.rs similarity index 59% rename from pod/src/error.rs rename to list-view/src/error.rs index 19b5bcb4..d14e9738 100644 --- a/pod/src/error.rs +++ b/list-view/src/error.rs @@ -4,7 +4,7 @@ use { solana_program_error::{ProgramError, ToStr}, }; -/// Errors that may be returned by the spl-pod library. +/// Errors that may be returned by the spl-list-view library. #[repr(u32)] #[derive( Debug, @@ -15,40 +15,36 @@ use { num_enum::TryFromPrimitive, num_derive::FromPrimitive, )] -pub enum PodSliceError { +pub enum ListViewError { /// Error in checked math operation #[error("Error in checked math operation")] CalculationFailure, /// Provided byte buffer too small for expected type #[error("Provided byte buffer too small for expected type")] BufferTooSmall, - /// Provided byte buffer too large for expected type - #[error("Provided byte buffer too large for expected type")] - BufferTooLarge, /// An integer conversion failed because the value was out of range for the target type #[error("An integer conversion failed because the value was out of range for the target type")] ValueOutOfRange, } -impl From for ProgramError { - fn from(e: PodSliceError) -> Self { +impl From for ProgramError { + fn from(e: ListViewError) -> Self { ProgramError::Custom(e as u32) } } -impl ToStr for PodSliceError { +impl ToStr for ListViewError { fn to_str(&self) -> &'static str { match self { - PodSliceError::CalculationFailure => "Error in checked math operation", - PodSliceError::BufferTooSmall => "Provided byte buffer too small for expected type", - PodSliceError::BufferTooLarge => "Provided byte buffer too large for expected type", - PodSliceError::ValueOutOfRange => "An integer conversion failed because the value was out of range for the target type" + ListViewError::CalculationFailure => "Error in checked math operation", + ListViewError::BufferTooSmall => "Provided byte buffer too small for expected type", + ListViewError::ValueOutOfRange => "An integer conversion failed because the value was out of range for the target type" } } } -impl From for PodSliceError { +impl From for ListViewError { fn from(_: TryFromIntError) -> Self { - PodSliceError::ValueOutOfRange + ListViewError::ValueOutOfRange } } diff --git a/list-view/src/lib.rs b/list-view/src/lib.rs index 56062237..eb89a3f8 100644 --- a/list-view/src/lib.rs +++ b/list-view/src/lib.rs @@ -1,9 +1,11 @@ +mod error; mod list_trait; mod list_view; mod list_view_mut; mod list_view_read_only; +mod pod_length; pub use { - list_trait::List, list_view::ListView, list_view_mut::ListViewMut, - list_view_read_only::ListViewReadOnly, + error::ListViewError, list_trait::List, list_view::ListView, list_view_mut::ListViewMut, + list_view_read_only::ListViewReadOnly, pod_length::PodLength, }; diff --git a/list-view/src/list_trait.rs b/list-view/src/list_trait.rs index b3731268..b9265c24 100644 --- a/list-view/src/list_trait.rs +++ b/list-view/src/list_trait.rs @@ -1,6 +1,8 @@ use { - crate::ListView, bytemuck::Pod, core::ops::Deref, solana_program_error::ProgramError, - spl_pod::pod_length::PodLength, + crate::{pod_length::PodLength, ListView}, + bytemuck::Pod, + core::ops::Deref, + solana_program_error::ProgramError, }; /// A trait to abstract the shared, read-only behavior diff --git a/list-view/src/list_view.rs b/list-view/src/list_view.rs index e45da26f..aef4fdc9 100644 --- a/list-view/src/list_view.rs +++ b/list-view/src/list_view.rs @@ -1,7 +1,10 @@ //! `ListView`, a compact, zero-copy array wrapper. use { - crate::{list_view_mut::ListViewMut, list_view_read_only::ListViewReadOnly}, + crate::{ + error::ListViewError, list_view_mut::ListViewMut, list_view_read_only::ListViewReadOnly, + pod_length::PodLength, + }, bytemuck::Pod, core::{ marker::PhantomData, @@ -13,8 +16,6 @@ use { bytemuck::{ pod_from_bytes, pod_from_bytes_mut, pod_slice_from_bytes, pod_slice_from_bytes_mut, }, - error::PodSliceError, - pod_length::PodLength, primitives::PodU32, }, }; @@ -57,7 +58,7 @@ impl ListView { .checked_mul(num_items) .and_then(|curr| curr.checked_add(size_of::())) .and_then(|curr| curr.checked_add(header_padding)) - .ok_or_else(|| PodSliceError::CalculationFailure.into()) + .ok_or_else(|| ListViewError::CalculationFailure.into()) } /// Unpack a read-only buffer into a `ListViewReadOnly` @@ -79,7 +80,7 @@ impl ListView { let capacity = data.len(); if (*length).into() > capacity { - return Err(PodSliceError::BufferTooSmall.into()); + return Err(ListViewError::BufferTooSmall.into()); } Ok(ListViewReadOnly { @@ -93,7 +94,7 @@ impl ListView { pub fn unpack_mut(buf: &mut [u8]) -> Result, ProgramError> { let view = Self::build_mut_view(buf)?; if (*view.length).into() > view.capacity { - return Err(PodSliceError::BufferTooSmall.into()); + return Err(ListViewError::BufferTooSmall.into()); } Ok(view) } @@ -101,7 +102,7 @@ impl ListView { /// Initialize a buffer: sets `length = 0` and returns a mutable `ListViewMut`. pub fn init(buf: &mut [u8]) -> Result, ProgramError> { let view = Self::build_mut_view(buf)?; - *view.length = L::try_from(0)?; + *view.length = L::try_from(0usize).map_err(ListViewError::from)?; Ok(view) } @@ -139,7 +140,7 @@ impl ListView { let data_start = len_field_end.saturating_add(header_padding); if buf_len < data_start { - return Err(PodSliceError::BufferTooSmall.into()); + return Err(ListViewError::BufferTooSmall.into()); } Ok(Layout { @@ -238,12 +239,12 @@ mod tests { // Case 1: Multiplication overflows. // `size_of::() * usize::MAX` will overflow. let err = ListView::::size_of(usize::MAX).unwrap_err(); - assert_eq!(err, PodSliceError::CalculationFailure.into()); + assert_eq!(err, ListViewError::CalculationFailure.into()); // Case 2: Multiplication does not overflow, but subsequent addition does. // `size_of::() * usize::MAX` does not overflow, but adding `size_of` will. let err = ListView::::size_of(usize::MAX).unwrap_err(); - assert_eq!(err, PodSliceError::CalculationFailure.into()); + assert_eq!(err, ListViewError::CalculationFailure.into()); } #[test] @@ -260,7 +261,7 @@ mod tests { } } impl TryFrom for TestPodU32 { - type Error = PodSliceError; + type Error = core::num::TryFromIntError; fn try_from(val: usize) -> Result { Ok(Self(u32::try_from(val)?)) } @@ -443,10 +444,10 @@ mod tests { let mut buf = vec![0u8; header_size - 1]; // 7 bytes let err = ListView::::unpack(&buf).unwrap_err(); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); + assert_eq!(err, ListViewError::BufferTooSmall.into()); let err = ListView::::unpack_mut(&mut buf).unwrap_err(); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); + assert_eq!(err, ListViewError::BufferTooSmall.into()); } #[test] @@ -463,10 +464,10 @@ mod tests { buf[0..len_size].copy_from_slice(bytemuck::bytes_of(&pod_len)); let err = ListView::::unpack(&buf).unwrap_err(); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); + assert_eq!(err, ListViewError::BufferTooSmall.into()); let err = ListView::::unpack_mut(&mut buf).unwrap_err(); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); + assert_eq!(err, ListViewError::BufferTooSmall.into()); } #[test] @@ -490,10 +491,10 @@ mod tests { fn test_unpack_empty_buffer() { let mut buf = []; let err = ListView::::unpack(&buf).unwrap_err(); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); + assert_eq!(err, ListViewError::BufferTooSmall.into()); let err = ListView::::unpack_mut(&mut buf).unwrap_err(); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); + assert_eq!(err, ListViewError::BufferTooSmall.into()); } #[test] @@ -560,12 +561,12 @@ mod tests { // Header requires 4 bytes (size_of) let mut buf = vec![0u8; 3]; let err = ListView::::init(&mut buf).unwrap_err(); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); + assert_eq!(err, ListViewError::BufferTooSmall.into()); // With padding, header requires 8 bytes (4 for len, 4 for pad) let mut buf_padded = vec![0u8; 7]; let err_padded = ListView::::init(&mut buf_padded).unwrap_err(); - assert_eq!(err_padded, PodSliceError::BufferTooSmall.into()); + assert_eq!(err_padded, ListViewError::BufferTooSmall.into()); } #[test] diff --git a/list-view/src/list_view_mut.rs b/list-view/src/list_view_mut.rs index 7cdc3aed..210ccba2 100644 --- a/list-view/src/list_view_mut.rs +++ b/list-view/src/list_view_mut.rs @@ -1,11 +1,11 @@ //! `ListViewMut`, a mutable, compact, zero-copy array wrapper. use { - crate::list_trait::List, + crate::{error::ListViewError, list_trait::List, pod_length::PodLength}, bytemuck::Pod, core::ops::{Deref, DerefMut}, solana_program_error::ProgramError, - spl_pod::{error::PodSliceError, pod_length::PodLength, primitives::PodU32}, + spl_pod::primitives::PodU32, }; #[derive(Debug)] @@ -20,10 +20,10 @@ impl ListViewMut<'_, T, L> { pub fn push(&mut self, item: T) -> Result<(), ProgramError> { let length = (*self.length).into(); if length >= self.capacity { - Err(PodSliceError::BufferTooSmall.into()) + Err(ListViewError::BufferTooSmall.into()) } else { self.data[length] = item; - *self.length = L::try_from(length.saturating_add(1))?; + *self.length = L::try_from(length.saturating_add(1)).map_err(ListViewError::from)?; Ok(()) } } @@ -46,7 +46,7 @@ impl ListViewMut<'_, T, L> { // Store the new length (len - 1) let new_len = len.checked_sub(1).unwrap(); - *self.length = L::try_from(new_len)?; + *self.length = L::try_from(new_len).map_err(ListViewError::from)?; Ok(removed_item) } @@ -144,7 +144,7 @@ mod tests { // Try to push beyond capacity let item4 = TestStruct::new(4, 40); let err = view.push(item4).unwrap_err(); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); + assert_eq!(err, ListViewError::BufferTooSmall.into()); // Ensure state is unchanged assert_eq!(view.len(), 3); @@ -277,7 +277,7 @@ mod tests { assert!(view.is_empty()); let err = view.push(TestStruct::new(1, 1)).unwrap_err(); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); + assert_eq!(err, ListViewError::BufferTooSmall.into()); let err = view.remove(0).unwrap_err(); assert_eq!(err, ProgramError::InvalidArgument); diff --git a/list-view/src/list_view_read_only.rs b/list-view/src/list_view_read_only.rs index 304f3be5..122d2224 100644 --- a/list-view/src/list_view_read_only.rs +++ b/list-view/src/list_view_read_only.rs @@ -1,10 +1,10 @@ //! `ListViewReadOnly`, a read-only, compact, zero-copy array wrapper. use { - crate::list_trait::List, + crate::{list_trait::List, pod_length::PodLength}, bytemuck::Pod, core::ops::Deref, - spl_pod::{pod_length::PodLength, primitives::PodU32}, + spl_pod::primitives::PodU32, }; #[derive(Debug)] @@ -39,10 +39,7 @@ mod tests { crate::ListView, bytemuck_derive::{Pod as DerivePod, Zeroable}, core::mem::size_of, - spl_pod::{ - pod_length::PodLength, - primitives::{PodU32, PodU64}, - }, + spl_pod::primitives::{PodU32, PodU64}, }; #[repr(C, align(16))] diff --git a/list-view/src/pod_length.rs b/list-view/src/pod_length.rs new file mode 100644 index 00000000..5a475dc8 --- /dev/null +++ b/list-view/src/pod_length.rs @@ -0,0 +1,8 @@ +use {bytemuck::Pod, core::num::TryFromIntError}; + +/// Marker trait for converting to/from Pod `uint`'s and `usize` +pub trait PodLength: Pod + TryFrom + Into {} + +/// Blanket implementation to automatically implement `PodLength` for any type +/// that satisfies the required bounds. +impl PodLength for T where T: Pod + TryFrom + Into {} diff --git a/pod/Cargo.toml b/pod/Cargo.toml index 9cb09c03..15ef8517 100644 --- a/pod/Cargo.toml +++ b/pod/Cargo.toml @@ -16,14 +16,10 @@ wincode = ["dep:wincode"] borsh = { version = "1.5.7", features = ["derive", "unstable__schema"], optional = true } bytemuck = { version = "1.23.2" } bytemuck_derive = { version = "1.10.1" } -num-derive = "0.4" -num_enum = "0.7" -num-traits = "0.2" serde = { version = "1.0.228", optional = true, features = ["derive"] } solana-address = { version = "2.2.0", features = ["bytemuck"] } solana-program-error = "3.0.0" solana-program-option = "3.0.0" -thiserror = "2.0" wincode = { version = "0.4.4", features = ["derive"], optional = true } [dev-dependencies] diff --git a/pod/src/lib.rs b/pod/src/lib.rs index 46820be4..67b8f30e 100644 --- a/pod/src/lib.rs +++ b/pod/src/lib.rs @@ -1,10 +1,8 @@ //! Crate containing `Pod` types and `bytemuck` utilities used in SPL pub mod bytemuck; -pub mod error; pub mod option; pub mod optional_keys; -pub mod pod_length; pub mod primitives; // Export current sdk types for downstream users building with a different sdk diff --git a/pod/src/pod_length.rs b/pod/src/pod_length.rs deleted file mode 100644 index 0f46b574..00000000 --- a/pod/src/pod_length.rs +++ /dev/null @@ -1,41 +0,0 @@ -use { - crate::{ - error::PodSliceError, - primitives::{PodU128, PodU16, PodU32, PodU64}, - }, - bytemuck::Pod, -}; - -/// Marker trait for converting to/from Pod `uint`'s and `usize` -pub trait PodLength: Pod + Into + TryFrom {} - -/// Blanket implementation to automatically implement `PodLength` for any type -/// that satisfies the required bounds. -impl PodLength for T where T: Pod + Into + TryFrom {} - -/// Implements the `TryFrom` and `From for usize` conversions for a Pod integer type -macro_rules! impl_pod_length_for { - ($PodType:ty, $PrimitiveType:ty) => { - impl TryFrom for $PodType { - type Error = PodSliceError; - - fn try_from(val: usize) -> Result { - let primitive_val = <$PrimitiveType>::try_from(val)?; - Ok(primitive_val.into()) - } - } - - impl From<$PodType> for usize { - fn from(pod_val: $PodType) -> Self { - let primitive_val = <$PrimitiveType>::from(pod_val); - Self::try_from(primitive_val) - .expect("value out of range for usize on this platform") - } - } - }; -} - -impl_pod_length_for!(PodU16, u16); -impl_pod_length_for!(PodU32, u32); -impl_pod_length_for!(PodU64, u64); -impl_pod_length_for!(PodU128, u128); diff --git a/pod/src/primitives.rs b/pod/src/primitives.rs index ccd2e715..c7d62d74 100644 --- a/pod/src/primitives.rs +++ b/pod/src/primitives.rs @@ -143,6 +143,33 @@ impl_int_conversion!(PodI64, i64); pub struct PodU128(pub [u8; 16]); impl_int_conversion!(PodU128, u128); +/// Implements the `TryFrom` and `From for usize` conversions for a Pod integer type +macro_rules! impl_usize_conversion { + ($PodType:ty, $PrimitiveType:ty) => { + impl TryFrom for $PodType { + type Error = core::num::TryFromIntError; + + fn try_from(val: usize) -> Result { + let primitive_val = <$PrimitiveType>::try_from(val)?; + Ok(primitive_val.into()) + } + } + + impl From<$PodType> for usize { + fn from(pod_val: $PodType) -> Self { + let primitive_val = <$PrimitiveType>::from(pod_val); + Self::try_from(primitive_val) + .expect("value out of range for usize on this platform") + } + } + }; +} + +impl_usize_conversion!(PodU16, u16); +impl_usize_conversion!(PodU32, u32); +impl_usize_conversion!(PodU64, u64); +impl_usize_conversion!(PodU128, u128); + #[cfg(test)] mod tests { use {super::*, crate::bytemuck::pod_from_bytes}; @@ -283,6 +310,31 @@ mod tests { assert_eq!(pod_u128, deserialized); } + macro_rules! test_usize_roundtrip { + ($test_name:ident, $PodType:ty, $max:expr) => { + #[test] + fn $test_name() { + // zero + let pod = <$PodType>::try_from(0usize).unwrap(); + assert_eq!(usize::from(pod), 0); + + // mid-range + let pod = <$PodType>::try_from(42usize).unwrap(); + assert_eq!(usize::from(pod), 42); + + // max + let max = $max as usize; + let pod = <$PodType>::try_from(max).unwrap(); + assert_eq!(usize::from(pod), max); + } + }; + } + + test_usize_roundtrip!(test_usize_roundtrip_u16, PodU16, u16::MAX); + test_usize_roundtrip!(test_usize_roundtrip_u32, PodU32, u32::MAX); + test_usize_roundtrip!(test_usize_roundtrip_u64, PodU64, u64::MAX); + test_usize_roundtrip!(test_usize_roundtrip_u128, PodU128, u128::MAX); + #[cfg(feature = "wincode")] mod wincode_tests { use {super::*, test_case::test_case};