@@ -44,14 +44,41 @@ impl<V: Vfs + Clone> Db<V> {
4444 let Some ( key) = hist. first_key ( ) . await ? else {
4545 return Ok ( None ) ;
4646 } ;
47- if key. len ( ) < 8 {
48- return Ok ( None ) ;
47+ if key. len ( ) != 8 {
48+ return Err ( PagedbError :: catalog_row_invalid ( "commit_history.key" ) ) ;
4949 }
5050 let mut b = [ 0u8 ; 8 ] ;
5151 b. copy_from_slice ( & key[ ..8 ] ) ;
5252 Ok ( Some ( u64:: from_be_bytes ( b) ) )
5353 }
5454
55+ /// Authenticate and decode every persisted named-counter row during open.
56+ ///
57+ /// Named counters are already atomic with catalog-root publication, so
58+ /// recovery validates their encoding but never rewrites their values.
59+ pub ( super ) async fn validate_counter_rows (
60+ & self ,
61+ catalog_root_page_id : u64 ,
62+ next_page_id : u64 ,
63+ ) -> Result < ( ) > {
64+ if catalog_root_page_id == 0 {
65+ return Ok ( ( ) ) ;
66+ }
67+
68+ let prefix = [ crate :: catalog:: codec:: CatalogRowKind :: Counter as u8 ] ;
69+ let tree = BTree :: open (
70+ self . pager . clone ( ) ,
71+ self . realm_id ,
72+ catalog_root_page_id,
73+ next_page_id,
74+ self . page_size ,
75+ ) ;
76+ for ( _key, value) in tree. scan_prefix ( & prefix) . await ? {
77+ Catalog :: decode_counter ( & value) ?;
78+ }
79+ Ok ( ( ) )
80+ }
81+
5582 /// Write per-realm quota caps into the catalog B+ tree and persist the
5683 /// updated catalog root to the A/B header.
5784 pub async fn set_realm_quotas ( & self , realm : RealmId , quotas : RealmQuotas ) -> Result < ( ) > {
@@ -307,3 +334,73 @@ impl<V: Vfs + Clone> Db<V> {
307334 Ok ( freed)
308335 }
309336}
337+
338+ #[ cfg( test) ]
339+ mod tests {
340+ use crate :: vfs:: memory:: MemVfs ;
341+ use crate :: { Db , PagedbError , RealmId } ;
342+
343+ use super :: * ;
344+
345+ const PAGE : usize = 4096 ;
346+ const REALM : RealmId = RealmId :: new ( [ 0xA7 ; 16 ] ) ;
347+
348+ #[ tokio:: test( flavor = "current_thread" ) ]
349+ async fn counter_recovery_surfaces_malformed_counter_row ( ) {
350+ let db = Db :: open_internal ( MemVfs :: new ( ) , [ 9u8 ; 32 ] , PAGE , REALM )
351+ . await
352+ . unwrap ( ) ;
353+ {
354+ let mut txn = db. begin_write ( ) . await . unwrap ( ) ;
355+ let mut counter = txn. counter ( "bad-counter" ) . unwrap ( ) ;
356+ counter. set ( 5 ) . await . unwrap ( ) ;
357+ drop ( counter) ;
358+ txn. commit ( ) . await . unwrap ( ) ;
359+ }
360+
361+ let ( catalog_root, next_page_id) = {
362+ let state = db. writer . lock ( ) . await ;
363+ ( state. catalog_root_page_id , state. next_page_id )
364+ } ;
365+ let mut tree = BTree :: open (
366+ db. pager . clone ( ) ,
367+ db. realm_id ,
368+ catalog_root,
369+ next_page_id,
370+ db. page_size ,
371+ ) ;
372+ tree. put ( & Catalog :: counter_key ( & [ 0xFF ] ) . unwrap ( ) , b"bad" )
373+ . await
374+ . unwrap ( ) ;
375+ tree. flush ( ) . await . unwrap ( ) ;
376+
377+ let err = db
378+ . validate_counter_rows ( tree. root_page_id ( ) , tree. next_page_id ( ) )
379+ . await
380+ . expect_err ( "malformed counter row must surface during recovery validation" ) ;
381+ assert ! ( matches!( err, PagedbError :: Corruption ( _) ) ) ;
382+ }
383+
384+ #[ tokio:: test( flavor = "current_thread" ) ]
385+ async fn oldest_retained_history_commit_surfaces_malformed_history_key ( ) {
386+ for malformed_key in [ b"x" . as_slice ( ) , b"123456789" . as_slice ( ) ] {
387+ let db = Db :: open_internal ( MemVfs :: new ( ) , [ 9u8 ; 32 ] , PAGE , REALM )
388+ . await
389+ . unwrap ( ) ;
390+ let next_page_id = db. writer . lock ( ) . await . next_page_id ;
391+ let mut history =
392+ BTree :: open ( db. pager . clone ( ) , db. realm_id , 0 , next_page_id, db. page_size ) ;
393+ history
394+ . put ( malformed_key, b"malformed history" )
395+ . await
396+ . unwrap ( ) ;
397+ history. flush ( ) . await . unwrap ( ) ;
398+
399+ let err = db
400+ . oldest_retained_history_commit ( history. root_page_id ( ) , history. next_page_id ( ) )
401+ . await
402+ . expect_err ( "malformed history key must surface" ) ;
403+ assert ! ( matches!( err, PagedbError :: Corruption ( _) ) ) ;
404+ }
405+ }
406+ }
0 commit comments