Skip to content

Commit bdf3cf6

Browse files
committed
feat: zero-copy-derive
fix: light-zero-copy tests comment derive mut commented byte len fix: derive macro for non mut pre bytelen refactor: detach bytelen trait stash adding config simple config derive works stash stash new at stash new at Compressed Account stash man InstructionDataInvoke new_zero_copy works stash simple config zero_copy_new tests stash refactor fixed lifetime issue stash instruction data tests work move byte_len to init_mut added randomized tests stash got failing random tests fixed u8 and bool remove bytelen renamed trait fix lint fix tests apply feedback meta_struct use syn to parse options instead of strings primitive types Replace string-based type comparisons with proper syn AST matching replace parse_str with parse_quote replace empty quote with unreachable! add byte len check borsh_vec_u8_as_slice_mut converted unimplemented to panic cleanup redundant as u64 etc fix docs cleanup cleanup commtend code cleanup mut conditionals remove bytelen derive cleanup refactor: replace duplicate code with generate_deserialize_call refactor detecting copy moved to internal refactor: add error handling cleanup cleanup file structure stash wip transform all primitive types to zero copy types simplify analyze_struct_fields fix empty meta struct generation stash zero copy changes unified some with Deserialize::Output unified integer field type enum renam VecNonStaticZeroCopy -> VecDynamicZeroCopy Simplify Option inner type extraction using syn utilities. Add bounds check before writing discriminant byte. improve generate_field_initialization remove debug test Incorrect type conversion from u8 to u32, add note options in arrays are not supported Error context lost in conversion format and add heap allocation check Check the last path segment for accurate type detection fix: test fix: test improve cache robustness
1 parent 5fb5198 commit bdf3cf6

32 files changed

+8790
-21
lines changed

.github/workflows/rust.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ jobs:
5353
cargo test -p light-account-checks --all-features
5454
cargo test -p light-verifier --all-features
5555
cargo test -p light-merkle-tree-metadata --all-features
56-
cargo test -p light-zero-copy --features std
56+
cargo test -p light-zero-copy --features "std, mut, derive"
57+
cargo test -p light-zero-copy-derive --features "mut"
5758
cargo test -p light-hash-set --all-features
5859
- name: program-libs-slow
5960
packages: light-bloom-filter light-indexed-merkle-tree light-batched-merkle-tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,5 @@ output1.txt
8686
.zed
8787

8888
**/.claude/**/*
89+
90+
expand.rs

Cargo.lock

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ members = [
1313
"program-libs/hash-set",
1414
"program-libs/indexed-merkle-tree",
1515
"program-libs/indexed-array",
16+
"program-libs/zero-copy-derive",
1617
"programs/account-compression",
1718
"programs/system",
1819
"programs/compressed-token",
@@ -167,6 +168,7 @@ light-compressed-account = { path = "program-libs/compressed-account", version =
167168
light-account-checks = { path = "program-libs/account-checks", version = "0.3.0" }
168169
light-verifier = { path = "program-libs/verifier", version = "2.1.0" }
169170
light-zero-copy = { path = "program-libs/zero-copy", version = "0.2.0" }
171+
light-zero-copy-derive = { path = "program-libs/zero-copy-derive", version = "0.1.0" }
170172
photon-api = { path = "sdk-libs/photon-api", version = "0.51.0" }
171173
forester-utils = { path = "forester-utils", version = "2.0.0" }
172174
account-compression = { path = "programs/account-compression", version = "2.0.0", features = [

program-libs/compressed-account/src/instruction_data/with_account_info.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,13 @@ impl<'a> Deserialize<'a> for InstructionDataInvokeCpiWithAccountInfo {
399399
let (account_infos, bytes) = {
400400
let (num_slices, mut bytes) = Ref::<&[u8], U32>::from_prefix(bytes)?;
401401
let num_slices = u32::from(*num_slices) as usize;
402-
// TODO: add check that remaining data is enough to read num_slices
403-
// This prevents agains invalid data allocating a lot of heap memory
404402
let mut slices = Vec::with_capacity(num_slices);
403+
if bytes.len() < num_slices {
404+
return Err(ZeroCopyError::InsufficientMemoryAllocated(
405+
bytes.len(),
406+
num_slices,
407+
));
408+
}
405409
for _ in 0..num_slices {
406410
let (slice, _bytes) = CompressedAccountInfo::zero_copy_at_with_owner(
407411
bytes,

program-libs/compressed-account/src/instruction_data/with_readonly.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,14 @@ impl<'a> Deserialize<'a> for InstructionDataInvokeCpiWithReadOnly {
347347
let (input_compressed_accounts, bytes) = {
348348
let (num_slices, mut bytes) = Ref::<&[u8], U32>::from_prefix(bytes)?;
349349
let num_slices = u32::from(*num_slices) as usize;
350-
// TODO: add check that remaining data is enough to read num_slices
351-
// This prevents agains invalid data allocating a lot of heap memory
350+
// Prevent heap exhaustion attacks by checking if num_slices is reasonable
351+
// Each element needs at least 1 byte when serialized
352+
if bytes.len() < num_slices {
353+
return Err(ZeroCopyError::InsufficientMemoryAllocated(
354+
bytes.len(),
355+
num_slices,
356+
));
357+
}
352358
let mut slices = Vec::with_capacity(num_slices);
353359
for _ in 0..num_slices {
354360
let (slice, _bytes) =
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "light-zero-copy-derive"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "Apache-2.0"
6+
description = "Proc macro for zero-copy deserialization"
7+
8+
[features]
9+
default = []
10+
mut = []
11+
12+
[lib]
13+
proc-macro = true
14+
15+
[dependencies]
16+
proc-macro2 = "1.0"
17+
quote = "1.0"
18+
syn = { version = "2.0", features = ["full", "extra-traits"] }
19+
lazy_static = "1.4"
20+
21+
[dev-dependencies]
22+
trybuild = "1.0"
23+
rand = "0.8"
24+
borsh = { workspace = true }
25+
light-zero-copy = { workspace = true, features = ["std", "derive"] }
26+
zerocopy = { workspace = true, features = ["derive"] }
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Light-Zero-Copy-Derive
2+
3+
A procedural macro for deriving zero-copy deserialization for Rust structs used with Solana programs.
4+
5+
## Features
6+
7+
This crate provides two key derive macros:
8+
9+
1. `#[derive(ZeroCopy)]` - Implements zero-copy deserialization with:
10+
- The `zero_copy_at` and `zero_copy_at_mut` methods for deserialization
11+
- Full Borsh compatibility for serialization/deserialization
12+
- Efficient memory representation with no copying of data
13+
- `From<Z<StructName>>` and `From<Z<StructName>Mut>` implementations for easy conversion back to the original struct
14+
15+
2. `#[derive(ZeroCopyEq)]` - Adds equality comparison support:
16+
- Compare zero-copy instances with regular struct instances
17+
- Can be used alongside `ZeroCopy` for complete functionality
18+
- Derivation for Options<struct> is not robust and may not compile.
19+
20+
## Rules for Zero-Copy Deserialization
21+
22+
The macro follows these rules when generating code:
23+
24+
1. Creates a `ZStruct` for your struct that follows zero-copy principles
25+
1. Fields are extracted into a meta struct until reaching a `Vec`, `Option` or non-`Copy` type
26+
2. Vectors are represented as `ZeroCopySlice` and not included in the meta struct
27+
3. Integer types are replaced with their zerocopy equivalents (e.g., `u16``U16`)
28+
4. Fields after the first vector are directly included in the `ZStruct` and deserialized one by one
29+
5. If a vector contains a nested vector (non-`Copy` type), it must implement `Deserialize`
30+
6. Elements in an `Option` must implement `Deserialize`
31+
7. Types that don't implement `Copy` must implement `Deserialize` and are deserialized one by one
32+
33+
## Usage
34+
35+
### Basic Usage
36+
37+
```rust
38+
use borsh::{BorshDeserialize, BorshSerialize};
39+
use light_zero_copy_derive::ZeroCopy;
40+
use light_zero_copy::{borsh::Deserialize, borsh_mut::DeserializeMut};
41+
42+
#[repr(C)]
43+
#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)]
44+
pub struct MyStruct {
45+
pub a: u8,
46+
pub b: u16,
47+
pub vec: Vec<u8>,
48+
pub c: u64,
49+
}
50+
let my_struct = MyStruct {
51+
a: 1,
52+
b: 2,
53+
vec: vec![1u8; 32],
54+
c: 3,
55+
};
56+
// Use the struct with zero-copy deserialization
57+
let mut bytes = my_struct.try_to_vec().unwrap();
58+
59+
// Immutable zero-copy deserialization
60+
let (zero_copy, _remaining) = MyStruct::zero_copy_at(&bytes).unwrap();
61+
62+
// Convert back to original struct using From implementation
63+
let converted: MyStruct = zero_copy.clone().into();
64+
assert_eq!(converted, my_struct);
65+
66+
// Mutable zero-copy deserialization with modification
67+
let (mut zero_copy_mut, _remaining) = MyStruct::zero_copy_at_mut(&mut bytes).unwrap();
68+
zero_copy_mut.a = 42;
69+
70+
// The change is reflected when we convert back to the original struct
71+
let modified: MyStruct = zero_copy_mut.into();
72+
assert_eq!(modified.a, 42);
73+
74+
// And also when we deserialize directly from the modified bytes
75+
let borsh = MyStruct::try_from_slice(&bytes).unwrap();
76+
assert_eq!(borsh.a, 42u8);
77+
```
78+
79+
### With Equality Comparison
80+
81+
```rust
82+
use borsh::{BorshDeserialize, BorshSerialize};
83+
use light_zero_copy_derive::ZeroCopy;
84+
85+
#[repr(C)]
86+
#[derive(Debug, PartialEq, BorshSerialize, BorshDeserialize, ZeroCopy)]
87+
pub struct MyStruct {
88+
pub a: u8,
89+
pub b: u16,
90+
pub vec: Vec<u8>,
91+
pub c: u64,
92+
}
93+
let my_struct = MyStruct {
94+
a: 1,
95+
b: 2,
96+
vec: vec![1u8; 32],
97+
c: 3,
98+
};
99+
// Use the struct with zero-copy deserialization
100+
let mut bytes = my_struct.try_to_vec().unwrap();
101+
let (zero_copy, _remaining) = MyStruct::zero_copy_at(&bytes).unwrap();
102+
assert_eq!(zero_copy, my_struct);
103+
```

0 commit comments

Comments
 (0)