33use arbitrary:: { Arbitrary , Result , Unstructured } ;
44use commonware_cryptography:: { Hasher as _, Sha256 } ;
55use commonware_runtime:: { buffer:: paged:: CacheRef , deterministic, Metrics , Runner } ;
6- use commonware_storage:: journal:: contiguous:: fixed:: { Config as JournalConfig , Journal } ;
6+ use commonware_storage:: journal:: {
7+ contiguous:: {
8+ fixed:: { Config as JournalConfig , Journal } ,
9+ Contiguous as _, Many , Mutable as _, Reader ,
10+ } ,
11+ Error ,
12+ } ;
713use commonware_utils:: { NZUsize , NZU16 , NZU64 } ;
814use futures:: { pin_mut, StreamExt } ;
915use libfuzzer_sys:: fuzz_target;
@@ -12,12 +18,23 @@ use std::num::NonZeroU16;
1218const MAX_REPLAY_BUF : usize = 2048 ;
1319const MAX_WRITE_BUF : usize = 2048 ;
1420const MAX_OPERATIONS : usize = 50 ;
21+ const MAX_APPEND_MANY : u8 = 20 ;
22+ const MAX_READ_MANY : usize = 16 ;
1523
1624fn bounded_non_zero ( u : & mut Unstructured < ' _ > ) -> Result < usize > {
1725 let v = u. int_in_range ( 1 ..=MAX_REPLAY_BUF ) ?;
1826 Ok ( v)
1927}
2028
29+ fn bounded_append_count ( u : & mut Unstructured < ' _ > ) -> Result < u8 > {
30+ u. int_in_range ( 0 ..=MAX_APPEND_MANY )
31+ }
32+
33+ fn bounded_positions ( u : & mut Unstructured < ' _ > ) -> Result < Vec < u64 > > {
34+ let len = u. int_in_range ( 0 ..=MAX_READ_MANY ) ?;
35+ ( 0 ..len) . map ( |_| u64:: arbitrary ( u) ) . collect ( )
36+ }
37+
2138#[ derive( Arbitrary , Debug , Clone ) ]
2239enum JournalOperation {
2340 Append {
@@ -42,10 +59,31 @@ enum JournalOperation {
4259 } ,
4360 Restart ,
4461 Destroy ,
62+ ReadMany {
63+ #[ arbitrary( with = bounded_positions) ]
64+ positions : Vec < u64 > ,
65+ } ,
4566 AppendMany {
67+ #[ arbitrary( with = bounded_append_count) ]
4668 count : u8 ,
4769 } ,
70+ AppendNested {
71+ #[ arbitrary( with = bounded_append_count) ]
72+ count_a : u8 ,
73+ #[ arbitrary( with = bounded_append_count) ]
74+ count_b : u8 ,
75+ } ,
76+ RewindTo {
77+ keep_value : u64 ,
78+ } ,
4879 MultipleSync ,
80+ TryReadSync {
81+ pos : u64 ,
82+ } ,
83+ PruningBoundary ,
84+ InitAtSize {
85+ size : u64 ,
86+ } ,
4987}
5088
5189#[ derive( Debug ) ]
@@ -99,6 +137,33 @@ fn fuzz(input: FuzzInput) {
99137 }
100138 }
101139
140+ JournalOperation :: ReadMany { positions } => {
141+ let reader = journal. reader ( ) . await ;
142+ let bounds = reader. bounds ( ) ;
143+ // Map fuzz positions into valid, sorted, deduplicated positions
144+ let mut mapped: Vec < u64 > = positions
145+ . iter ( )
146+ . filter_map ( |p| {
147+ if bounds. is_empty ( ) {
148+ return None ;
149+ }
150+ let len = bounds. end - bounds. start ;
151+ Some ( bounds. start + ( * p % len) )
152+ } )
153+ . collect ( ) ;
154+ mapped. sort_unstable ( ) ;
155+ mapped. dedup ( ) ;
156+ if !mapped. is_empty ( ) {
157+ let batch = reader. read_many ( & mapped) . await . unwrap ( ) ;
158+ assert_eq ! ( batch. len( ) , mapped. len( ) ) ;
159+ // Cross-check against individual reads
160+ for ( i, & pos) in mapped. iter ( ) . enumerate ( ) {
161+ let single = reader. read ( pos) . await . unwrap ( ) ;
162+ assert_eq ! ( batch[ i] , single) ;
163+ }
164+ }
165+ }
166+
102167 JournalOperation :: Size => {
103168 let size = journal. size ( ) ;
104169 assert_eq ! ( journal_size, size, "unexpected size" ) ;
@@ -172,11 +237,20 @@ fn fuzz(input: FuzzInput) {
172237 }
173238
174239 JournalOperation :: AppendMany { count } => {
175- for _ in 0 ..* count {
176- let digest = Sha256 :: hash ( & next_value. to_be_bytes ( ) ) ;
177- journal. append ( & digest) . await . unwrap ( ) ;
178- next_value += 1 ;
179- journal_size += 1 ;
240+ if * count == 0 {
241+ // Exercise the EmptyAppend error path
242+ let err = journal. append_many ( Many :: Flat ( & [ ] ) ) . await ;
243+ assert ! ( matches!( err, Err ( Error :: EmptyAppend ) ) ) ;
244+ } else {
245+ let items: Vec < _ > = ( 0 ..* count)
246+ . map ( |_| {
247+ let d = Sha256 :: hash ( & next_value. to_be_bytes ( ) ) ;
248+ next_value += 1 ;
249+ d
250+ } )
251+ . collect ( ) ;
252+ journal. append_many ( Many :: Flat ( & items) ) . await . unwrap ( ) ;
253+ journal_size += * count as u64 ;
180254 }
181255 }
182256
@@ -185,6 +259,76 @@ fn fuzz(input: FuzzInput) {
185259 journal. sync ( ) . await . unwrap ( ) ;
186260 journal. sync ( ) . await . unwrap ( ) ;
187261 }
262+
263+ JournalOperation :: AppendNested { count_a, count_b } => {
264+ if * count_a == 0 && * count_b == 0 {
265+ let err = journal. append_many ( Many :: Nested ( & [ & [ ] , & [ ] ] ) ) . await ;
266+ assert ! ( matches!( err, Err ( Error :: EmptyAppend ) ) ) ;
267+ } else {
268+ let items_a: Vec < _ > = ( 0 ..* count_a)
269+ . map ( |_| {
270+ let d = Sha256 :: hash ( & next_value. to_be_bytes ( ) ) ;
271+ next_value += 1 ;
272+ d
273+ } )
274+ . collect ( ) ;
275+ let items_b: Vec < _ > = ( 0 ..* count_b)
276+ . map ( |_| {
277+ let d = Sha256 :: hash ( & next_value. to_be_bytes ( ) ) ;
278+ next_value += 1 ;
279+ d
280+ } )
281+ . collect ( ) ;
282+ let slices: & [ & [ _ ] ] = & [ & items_a, & items_b] ;
283+ journal. append_many ( Many :: Nested ( slices) ) . await . unwrap ( ) ;
284+ journal_size += * count_a as u64 + * count_b as u64 ;
285+ }
286+ }
287+
288+ JournalOperation :: RewindTo { keep_value } => {
289+ if journal_size > oldest_retained_pos {
290+ let target = Sha256 :: hash ( & keep_value. to_be_bytes ( ) ) ;
291+ let new_size = journal. rewind_to ( |item| * item == target) . await . unwrap ( ) ;
292+ journal. sync ( ) . await . unwrap ( ) ;
293+ journal_size = new_size;
294+ oldest_retained_pos = journal. reader ( ) . await . bounds ( ) . start ;
295+ }
296+ }
297+
298+ JournalOperation :: TryReadSync { pos } => {
299+ let reader = journal. reader ( ) . await ;
300+ let bounds = reader. bounds ( ) ;
301+ if bounds. contains ( pos) {
302+ // Cross-check: sync result must match async result
303+ if let Some ( sync_val) = reader. try_read_sync ( * pos) {
304+ let async_val = reader. read ( * pos) . await . unwrap ( ) ;
305+ assert_eq ! ( sync_val, async_val) ;
306+ }
307+ }
308+ }
309+
310+ JournalOperation :: PruningBoundary => {
311+ let boundary = journal. pruning_boundary ( ) ;
312+ assert_eq ! ( boundary, oldest_retained_pos) ;
313+ }
314+
315+ JournalOperation :: InitAtSize { size } => {
316+ // Cap to avoid excessive memory use
317+ let target_size = * size % 256 ;
318+ drop ( journal) ;
319+ journal = Journal :: init_at_size (
320+ context
321+ . with_label ( "journal" )
322+ . with_attribute ( "instance" , restarts) ,
323+ cfg. clone ( ) ,
324+ target_size,
325+ )
326+ . await
327+ . unwrap ( ) ;
328+ restarts += 1 ;
329+ journal_size = journal. size ( ) ;
330+ oldest_retained_pos = journal. reader ( ) . await . bounds ( ) . start ;
331+ }
188332 }
189333 }
190334 } ) ;
0 commit comments