diff --git a/Cargo.lock b/Cargo.lock index f3ab7ac..3348f79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6667,7 +6667,7 @@ dependencies = [ "solana-system-interface", "solana-sysvar", "solana-zk-sdk", - "spl-pod 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-pod 0.5.1", "spl-token-confidential-transfer-proof-extraction", ] @@ -6708,15 +6708,14 @@ dependencies = [ [[package]] name = "spl-pod" version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d994afaf86b779104b4a95ba9ca75b8ced3fdb17ee934e38cb69e72afbe17799" dependencies = [ - "base64 0.22.1", "borsh 1.5.7", "bytemuck", "bytemuck_derive", "num-derive", "num-traits", - "serde", - "serde_json", "solana-decode-error", "solana-msg", "solana-program-error", @@ -6728,15 +6727,16 @@ dependencies = [ [[package]] name = "spl-pod" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d994afaf86b779104b4a95ba9ca75b8ced3fdb17ee934e38cb69e72afbe17799" +version = "0.6.0" dependencies = [ + "base64 0.22.1", "borsh 1.5.7", "bytemuck", "bytemuck_derive", "num-derive", "num-traits", + "serde", + "serde_json", "solana-decode-error", "solana-msg", "solana-program-error", @@ -6820,7 +6820,7 @@ dependencies = [ "solana-pubkey", "solana-sdk", "spl-discriminator 0.4.1", - "spl-pod 0.5.1", + "spl-pod 0.6.0", "spl-program-error 0.7.0", "spl-type-length-value 0.8.0", "thiserror 2.0.12", @@ -6842,7 +6842,7 @@ dependencies = [ "solana-program-error", "solana-pubkey", "spl-discriminator 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "spl-pod 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-pod 0.5.1", "spl-program-error 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "spl-type-length-value 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 2.0.12", @@ -6908,7 +6908,7 @@ dependencies = [ "solana-zk-sdk", "spl-elgamal-registry", "spl-memo", - "spl-pod 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-pod 0.5.1", "spl-token", "spl-token-confidential-transfer-ciphertext-arithmetic", "spl-token-confidential-transfer-proof-extraction", @@ -6948,7 +6948,7 @@ dependencies = [ "solana-pubkey", "solana-sdk-ids", "solana-zk-sdk", - "spl-pod 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-pod 0.5.1", "thiserror 2.0.12", ] @@ -6978,7 +6978,7 @@ dependencies = [ "solana-program-error", "solana-pubkey", "spl-discriminator 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "spl-pod 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-pod 0.5.1", "thiserror 2.0.12", ] @@ -6998,7 +6998,7 @@ dependencies = [ "solana-program-error", "solana-pubkey", "spl-discriminator 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "spl-pod 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-pod 0.5.1", "spl-type-length-value 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 2.0.12", ] @@ -7021,7 +7021,7 @@ dependencies = [ "solana-program-error", "solana-pubkey", "spl-discriminator 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "spl-pod 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-pod 0.5.1", "spl-program-error 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "spl-tlv-account-resolution 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "spl-type-length-value 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -7040,7 +7040,7 @@ dependencies = [ "solana-msg", "solana-program-error", "spl-discriminator 0.4.1", - "spl-pod 0.5.1", + "spl-pod 0.6.0", "spl-type-length-value-derive", "thiserror 2.0.12", ] @@ -7059,7 +7059,7 @@ dependencies = [ "solana-msg", "solana-program-error", "spl-discriminator 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "spl-pod 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-pod 0.5.1", "thiserror 2.0.12", ] diff --git a/pod/Cargo.toml b/pod/Cargo.toml index 4b8d7d5..4ea32da 100644 --- a/pod/Cargo.toml +++ b/pod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spl-pod" -version = "0.5.1" +version = "0.6.0" description = "Solana Program Library Plain Old Data (Pod)" authors = ["Anza Maintainers "] repository = "https://github.com/solana-program/libraries" diff --git a/pod/src/lib.rs b/pod/src/lib.rs index 292f1c0..ab6ec11 100644 --- a/pod/src/lib.rs +++ b/pod/src/lib.rs @@ -2,6 +2,7 @@ pub mod bytemuck; pub mod error; +pub mod list; pub mod option; pub mod optional_keys; pub mod primitives; diff --git a/pod/src/list.rs b/pod/src/list.rs new file mode 100644 index 0000000..eae3e1d --- /dev/null +++ b/pod/src/list.rs @@ -0,0 +1,334 @@ +use crate::bytemuck::{pod_from_bytes_mut, pod_slice_from_bytes_mut}; +use crate::error::PodSliceError; +use crate::primitives::PodU32; +use crate::slice::max_len_for_type; +use bytemuck::Pod; +use solana_program_error::ProgramError; + +const LENGTH_SIZE: usize = std::mem::size_of::(); + +/// A mutable, variable-length collection of `Pod` types backed by a byte buffer. +/// +/// `PodList` provides a safe, zero-copy, `Vec`-like interface for a slice of +/// `Pod` data that resides in an external, pre-allocated `&mut [u8]` buffer. +/// It does not own the buffer itself, but acts as a mutable view over it. +/// +/// This is useful in environments where allocations are restricted or expensive, +/// such as Solana programs, allowing for dynamic-length data structures within a +/// fixed-size account. +/// +/// ## Memory Layout +/// +/// The structure assumes the underlying byte buffer is formatted as follows: +/// 1. **Length**: A `u32` value (`PodU32`) at the beginning of the buffer, +/// indicating the number of currently active elements in the collection. +/// 2. **Data**: The remaining part of the buffer, which is treated as a slice +/// of `T` elements. The capacity of the collection is the number of `T` +/// elements that can fit into this data portion. +pub struct PodList<'data, T: Pod> { + length: &'data mut PodU32, + data: &'data mut [T], + max_length: usize, +} + +impl<'data, T: Pod> PodList<'data, T> { + /// Unpack the mutable buffer into a mutable slice, with the option to + /// initialize the data + fn unpack_internal<'a>(data: &'a mut [u8], init: bool) -> Result + where + 'a: 'data, + { + if data.len() < LENGTH_SIZE { + return Err(PodSliceError::BufferTooSmall.into()); + } + let (length, data) = data.split_at_mut(LENGTH_SIZE); + let length = pod_from_bytes_mut::(length)?; + if init { + *length = 0.into(); + } + let max_length = max_len_for_type::(data.len(), u32::from(*length) as usize)?; + let data = pod_slice_from_bytes_mut(data)?; + Ok(Self { + length, + data, + max_length, + }) + } + + /// Unpack the mutable buffer into a mutable slice + pub fn unpack<'a>(data: &'a mut [u8]) -> Result + where + 'a: 'data, + { + Self::unpack_internal(data, /* init */ false) + } + + /// Unpack the mutable buffer into a mutable slice, and initialize the + /// slice to 0-length + pub fn init<'a>(data: &'a mut [u8]) -> Result + where + 'a: 'data, + { + Self::unpack_internal(data, /* init */ true) + } + + /// Add another item to the slice + pub fn push(&mut self, t: T) -> Result<(), ProgramError> { + let length = u32::from(*self.length); + if length as usize == self.max_length { + Err(PodSliceError::BufferTooSmall.into()) + } else { + self.data[length as usize] = t; + *self.length = length.saturating_add(1).into(); + Ok(()) + } + } + + /// Remove and return the element at `index`, shifting all later + /// elements one position to the left. + pub fn remove_at(&mut self, index: usize) -> Result { + let len = u32::from(*self.length) as usize; + if index >= len { + return Err(ProgramError::InvalidArgument); + } + + let removed_item = self.data[index]; + + // Move the tail left by one + let tail_start = index + .checked_add(1) + .ok_or(ProgramError::ArithmeticOverflow)?; + self.data.copy_within(tail_start..len, index); + + // Zero-fill the now-unused slot at the end + let last = len.checked_sub(1).ok_or(ProgramError::ArithmeticOverflow)?; + self.data[last] = T::zeroed(); + + // Store the new length (len - 1) + *self.length = (last as u32).into(); + + Ok(removed_item) + } + + /// Find the first element that satisfies `predicate` and remove it, + /// returning the element. + pub fn remove_first_where

(&mut self, mut predicate: P) -> Result + where + P: FnMut(&T) -> bool, + { + if let Some(index) = self.data.iter().position(&mut predicate) { + self.remove_at(index) + } else { + Err(ProgramError::InvalidArgument) + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + bytemuck_derive::{Pod, Zeroable}, + }; + + #[repr(C)] + #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] + struct TestStruct { + test_field: u8, + test_pubkey: [u8; 32], + } + + #[test] + fn test_pod_collection() { + // slice can fit 2 `TestStruct` + let mut pod_slice_bytes = [0; 70]; + // set length to 1, so we have room to push 1 more item + let len_bytes = [1, 0, 0, 0]; + pod_slice_bytes[0..4].copy_from_slice(&len_bytes); + + let mut pod_slice = PodList::::unpack(&mut pod_slice_bytes).unwrap(); + + assert_eq!(*pod_slice.length, PodU32::from(1)); + pod_slice.push(TestStruct::default()).unwrap(); + assert_eq!(*pod_slice.length, PodU32::from(2)); + let err = pod_slice + .push(TestStruct::default()) + .expect_err("Expected an `PodSliceError::BufferTooSmall` error"); + assert_eq!(err, PodSliceError::BufferTooSmall.into()); + } + + fn make_buffer(capacity: usize, items: &[u8]) -> Vec { + let buff_len = LENGTH_SIZE.checked_add(capacity).unwrap(); + let mut buf = vec![0u8; buff_len]; + buf[..LENGTH_SIZE].copy_from_slice(&(items.len() as u32).to_le_bytes()); + let end = LENGTH_SIZE.checked_add(items.len()).unwrap(); + buf[LENGTH_SIZE..end].copy_from_slice(items); + buf + } + + #[test] + fn remove_at_first_item() { + let mut buff = make_buffer(15, &[10, 20, 30, 40]); + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let removed = pod_list.remove_at(0).unwrap(); + assert_eq!(removed, 10); + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 3); + assert_eq!(pod_list.data[..pod_list_len].to_vec(), &[20, 30, 40]); + assert_eq!(pod_list.data[3], 0); + } + + #[test] + fn remove_at_middle_item() { + let mut buff = make_buffer(15, &[10, 20, 30, 40]); + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let removed = pod_list.remove_at(2).unwrap(); + assert_eq!(removed, 30); + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 3); + assert_eq!(pod_list.data[..pod_list_len].to_vec(), &[10, 20, 40]); + assert_eq!(pod_list.data[3], 0); + } + + #[test] + fn remove_at_last_item() { + let mut buff = make_buffer(15, &[10, 20, 30, 40]); + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let removed = pod_list.remove_at(3).unwrap(); + assert_eq!(removed, 40); + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 3); + assert_eq!(pod_list.data[..pod_list_len].to_vec(), &[10, 20, 30]); + assert_eq!(pod_list.data[3], 0); + } + + #[test] + fn remove_at_out_of_bounds() { + let mut buff = make_buffer(3, &[1, 2, 3]); + let original_buff = buff.clone(); + + { + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let err = pod_list.remove_at(3).unwrap_err(); + assert_eq!(err, ProgramError::InvalidArgument); + + // pod_list should be unchanged + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 3); + assert_eq!(pod_list.data[..pod_list_len].to_vec(), vec![1, 2, 3]); + } + + assert_eq!(buff, original_buff); + } + + #[test] + fn remove_at_single_element() { + let mut buff = make_buffer(1, &[10]); + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let removed = pod_list.remove_at(0).unwrap(); + assert_eq!(removed, 10); + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 0); + assert_eq!(pod_list.data[..pod_list_len].to_vec(), &[] as &[u8]); + assert_eq!(pod_list.data[0], 0); + } + + #[test] + fn remove_at_empty_slice() { + let mut buff = make_buffer(0, &[]); + let original_buff = buff.clone(); + + { + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let err = pod_list.remove_at(0).unwrap_err(); + assert_eq!(err, ProgramError::InvalidArgument); + + // Assert list state is unchanged + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 0); + } + + assert_eq!(buff, original_buff); + } + + #[test] + fn remove_first_where_first_item() { + let mut buff = make_buffer(3, &[5, 10, 15]); + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let removed = pod_list.remove_first_where(|&x| x == 5).unwrap(); + assert_eq!(removed, 5); + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 2); + assert_eq!(pod_list.data[..pod_list_len].to_vec(), &[10, 15]); + assert_eq!(pod_list.data[2], 0); + } + + #[test] + fn remove_first_where_middle_item() { + let mut buff = make_buffer(4, &[1, 2, 3, 4]); + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let removed = pod_list.remove_first_where(|v| *v == 3).unwrap(); + assert_eq!(removed, 3); + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 3); + assert_eq!(pod_list.data[..pod_list_len].to_vec(), &[1, 2, 4]); + assert_eq!(pod_list.data[3], 0); + } + + #[test] + fn remove_first_where_last_item() { + let mut buff = make_buffer(3, &[5, 10, 15]); + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let removed = pod_list.remove_first_where(|&x| x == 15).unwrap(); + assert_eq!(removed, 15); + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 2); + assert_eq!(pod_list.data[..pod_list_len].to_vec(), &[5, 10]); + assert_eq!(pod_list.data[2], 0); + } + + #[test] + fn remove_first_where_multiple_matches() { + let mut buff = make_buffer(5, &[7, 8, 8, 9, 10]); + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let removed = pod_list.remove_first_where(|v| *v == 8).unwrap(); + assert_eq!(removed, 8); // Removed *first* 8 + let pod_list_len = u32::from(*pod_list.length) as usize; + assert_eq!(pod_list_len, 4); + // Should remove only the *first* match. + assert_eq!(pod_list.data[..pod_list_len].to_vec(), &[7, 8, 9, 10]); + assert_eq!(pod_list.data[4], 0); + } + + #[test] + fn remove_first_where_not_found() { + let mut buff = make_buffer(3, &[5, 6, 7]); + let original_buff = buff.clone(); + + { + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let err = pod_list.remove_first_where(|v| *v == 42).unwrap_err(); + assert_eq!(err, ProgramError::InvalidArgument); + // Assert list state is unchanged + assert_eq!(u32::from(*pod_list.length) as usize, 3); + } + + assert_eq!(buff, original_buff); + } + + #[test] + fn remove_first_where_empty_slice() { + let mut buff = make_buffer(0, &[]); + let original_buff = buff.clone(); + + { + let mut pod_list = PodList::::unpack(&mut buff).unwrap(); + let err = pod_list.remove_first_where(|_| true).unwrap_err(); + assert_eq!(err, ProgramError::InvalidArgument); + // Assert list state is unchanged + assert_eq!(u32::from(*pod_list.length) as usize, 0); + } + + assert_eq!(buff, original_buff); + } +} diff --git a/pod/src/slice.rs b/pod/src/slice.rs index ca885ab..0082c7a 100644 --- a/pod/src/slice.rs +++ b/pod/src/slice.rs @@ -49,12 +49,18 @@ impl<'data, T: Pod> PodSlice<'data, T> { } } +#[deprecated( + since = "0.6.0", + note = "This struct will be removed in the next major release (1.0.0). Please use `PodList` instead." +)] /// Special type for using a slice of mutable `Pod`s in a zero-copy way pub struct PodSliceMut<'data, T: Pod> { length: &'data mut PodU32, data: &'data mut [T], max_length: usize, } + +#[allow(deprecated)] impl<'data, T: Pod> PodSliceMut<'data, T> { /// Unpack the mutable buffer into a mutable slice, with the option to /// initialize the data @@ -109,7 +115,7 @@ impl<'data, T: Pod> PodSliceMut<'data, T> { } } -fn max_len_for_type(data_len: usize, length_val: usize) -> Result { +pub fn max_len_for_type(data_len: usize, length_val: usize) -> Result { let item_size = std::mem::size_of::(); let max_len = data_len .checked_div(item_size) @@ -136,6 +142,7 @@ fn max_len_for_type(data_len: usize, length_val: usize) -> Result::size_of(extra_account_metas.len())?; let (bytes, _) = state.alloc::(tlv_size, false)?; - let mut validation_data = PodSliceMut::init(bytes)?; + let mut validation_data = PodList::init(bytes)?; for meta in extra_account_metas { validation_data.push(*meta)?; } @@ -188,7 +189,7 @@ impl ExtraAccountMetaList { let mut state = TlvStateMut::unpack(data).unwrap(); let tlv_size = PodSlice::::size_of(extra_account_metas.len())?; let bytes = state.realloc_first::(tlv_size)?; - let mut validation_data = PodSliceMut::init(bytes)?; + let mut validation_data = PodList::init(bytes)?; for meta in extra_account_metas { validation_data.push(*meta)?; } diff --git a/type-length-value/Cargo.toml b/type-length-value/Cargo.toml index e7e33a8..c98885d 100644 --- a/type-length-value/Cargo.toml +++ b/type-length-value/Cargo.toml @@ -21,7 +21,7 @@ solana-msg = "2.2.1" solana-program-error = "2.2.1" spl-discriminator = { version = "0.4.0", path = "../discriminator" } spl-type-length-value-derive = { version = "0.2", path = "./derive", optional = true } -spl-pod = { version = "0.5.1", path = "../pod" } +spl-pod = { version = "0.6.0", path = "../pod" } thiserror = "2.0" [lib]