33
44#![ cfg( not( target_arch = "wasm32" ) ) ]
55
6- use std:: path:: Path ;
6+ use std:: { collections :: BTreeSet , path:: Path } ;
77
88use tokio:: fs;
99use tokio:: io:: { AsyncReadExt , AsyncSeekExt , AsyncWriteExt } ;
1010
1111use crate :: Result ;
1212use crate :: errors:: PagedbError ;
1313
14+ /// Pages consumed from one `pages.delta` stream during an incremental apply.
15+ pub struct AppliedDeltaPages {
16+ /// Number of page records written to `main.db`.
17+ pub pages_applied : u64 ,
18+ /// Page IDs supplied by this exact delta stream.
19+ pub page_ids : BTreeSet < u64 > ,
20+ }
21+
1422/// Apply an incremental snapshot directory (`src_path`) to the Follower's
1523/// `main.db` file at `main_db_path` (absolute filesystem path). Returns stats.
1624///
@@ -20,54 +28,103 @@ pub async fn apply_delta_pages(
2028 src_path : & Path ,
2129 dst_main_db_path : & Path ,
2230 page_size : usize ,
23- ) -> Result < u64 > {
31+ protected_page_ids : & BTreeSet < u64 > ,
32+ base_allocation_floor : u64 ,
33+ target_next_page_id : u64 ,
34+ ) -> Result < AppliedDeltaPages > {
2435 let delta_path = src_path. join ( "pages.delta" ) ;
2536 let mut delta = match fs:: File :: open ( & delta_path) . await {
2637 Ok ( f) => f,
27- Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => return Ok ( 0 ) ,
38+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => {
39+ return Ok ( AppliedDeltaPages {
40+ pages_applied : 0 ,
41+ page_ids : BTreeSet :: new ( ) ,
42+ } ) ;
43+ }
2844 Err ( e) => return Err ( PagedbError :: Io ( e) ) ,
2945 } ;
3046
47+ let page_size_u64 = u64:: try_from ( page_size)
48+ . map_err ( |_| PagedbError :: snapshot_artifact_invalid ( "pages.delta.page_size" ) ) ?;
49+ let record_size = 8u64
50+ . checked_add ( page_size_u64)
51+ . ok_or_else ( || PagedbError :: snapshot_artifact_invalid ( "pages.delta.record_size" ) ) ?;
52+ let delta_len = delta. metadata ( ) . await . map_err ( PagedbError :: Io ) ?. len ( ) ;
53+ if delta_len % record_size != 0 {
54+ return Err ( PagedbError :: snapshot_artifact_invalid ( "pages.delta.length" ) ) ;
55+ }
56+
57+ // Validate every record before opening main.db for writes. A malformed
58+ // record late in the stream must not leave a valid prefix written into the
59+ // follower's future page range.
60+ let record_count = delta_len / record_size;
61+ let mut page_ids = BTreeSet :: new ( ) ;
62+ let mut id_buf = [ 0u8 ; 8 ] ;
63+ let page_skip = i64:: try_from ( page_size)
64+ . map_err ( |_| PagedbError :: snapshot_artifact_invalid ( "pages.delta.page_size" ) ) ?;
65+ for _ in 0 ..record_count {
66+ delta
67+ . read_exact ( & mut id_buf)
68+ . await
69+ . map_err ( PagedbError :: Io ) ?;
70+ let page_id = u64:: from_be_bytes ( id_buf) ;
71+ if page_id < base_allocation_floor
72+ || page_id >= target_next_page_id
73+ || protected_page_ids. contains ( & page_id)
74+ {
75+ return Err ( PagedbError :: snapshot_artifact_invalid (
76+ "pages.delta.page_id" ,
77+ ) ) ;
78+ }
79+ if !page_ids. insert ( page_id) {
80+ return Err ( PagedbError :: snapshot_artifact_invalid (
81+ "pages.delta.duplicate_page_id" ,
82+ ) ) ;
83+ }
84+ delta
85+ . seek ( std:: io:: SeekFrom :: Current ( page_skip) )
86+ . await
87+ . map_err ( PagedbError :: Io ) ?;
88+ }
89+ delta
90+ . seek ( std:: io:: SeekFrom :: Start ( 0 ) )
91+ . await
92+ . map_err ( PagedbError :: Io ) ?;
93+
3194 let mut dst = fs:: OpenOptions :: new ( )
3295 . read ( true )
3396 . write ( true )
3497 . open ( dst_main_db_path)
3598 . await
3699 . map_err ( PagedbError :: Io ) ?;
37-
38- let mut pages_applied: u64 = 0 ;
39- let mut id_buf = [ 0u8 ; 8 ] ;
40100 let mut page_buf = vec ! [ 0u8 ; page_size] ;
41-
42- loop {
43- // Read page_id (8 bytes BE).
44- match delta. read_exact ( & mut id_buf) . await {
45- Ok ( _) => { }
46- Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: UnexpectedEof => break ,
47- Err ( e) => return Err ( PagedbError :: Io ( e) ) ,
48- }
101+ for _ in 0 ..record_count {
102+ delta
103+ . read_exact ( & mut id_buf)
104+ . await
105+ . map_err ( PagedbError :: Io ) ?;
49106 let page_id = u64:: from_be_bytes ( id_buf) ;
50-
51- // Read page bytes.
52- match delta. read_exact ( & mut page_buf) . await {
53- Ok ( _) => { }
54- Err ( e) => return Err ( PagedbError :: Io ( e) ) ,
55- }
107+ delta
108+ . read_exact ( & mut page_buf)
109+ . await
110+ . map_err ( PagedbError :: Io ) ?;
56111
57112 // Write page to main.db at the correct offset.
58113 let offset = page_id
59- . checked_mul ( page_size as u64 )
60- . ok_or_else ( || PagedbError :: Io ( std :: io :: Error :: other ( "page offset overflow" ) ) ) ?;
114+ . checked_mul ( page_size_u64 )
115+ . ok_or_else ( || PagedbError :: snapshot_artifact_invalid ( "pages.delta.page_offset" ) ) ?;
61116 dst. seek ( std:: io:: SeekFrom :: Start ( offset) )
62117 . await
63118 . map_err ( PagedbError :: Io ) ?;
64119 dst. write_all ( & page_buf) . await . map_err ( PagedbError :: Io ) ?;
65- pages_applied += 1 ;
66120 }
67121
68122 dst. flush ( ) . await . map_err ( PagedbError :: Io ) ?;
69123 dst. sync_all ( ) . await . map_err ( PagedbError :: Io ) ?;
70- Ok ( pages_applied)
124+ Ok ( AppliedDeltaPages {
125+ pages_applied : record_count,
126+ page_ids,
127+ } )
71128}
72129
73130/// Verify that the snapshot's segment directory has exactly the count claimed
@@ -90,9 +147,20 @@ pub(crate) async fn validate_snapshot_segment_count(src_path: &Path, expected: u
90147pub async fn stage_snapshot_segments (
91148 src_path : & Path ,
92149 dst_seg_root : & Path ,
150+ expected_segment_ids : & BTreeSet < [ u8 ; 16 ] > ,
93151) -> Result < Vec < [ u8 ; 16 ] > > {
94152 let entries = snapshot_segment_entries ( src_path) . await ?;
95153 let seg_src = src_path. join ( "seg" ) ;
154+ let actual_segment_ids: BTreeSet < [ u8 ; 16 ] > = entries
155+ . iter ( )
156+ . map ( |name| {
157+ crate :: hex:: parse_hex :: < 16 > ( name)
158+ . ok_or_else ( || PagedbError :: snapshot_artifact_invalid ( "segment_file_name" ) )
159+ } )
160+ . collect :: < Result < _ > > ( ) ?;
161+ if & actual_segment_ids != expected_segment_ids {
162+ return Err ( PagedbError :: snapshot_artifact_invalid ( "segments" ) ) ;
163+ }
96164
97165 let staging_dir = dst_seg_root. join ( ".staging" ) ;
98166 fs:: create_dir_all ( & staging_dir)
@@ -103,7 +171,6 @@ pub async fn stage_snapshot_segments(
103171 let mut copy_buf = vec ! [ 0u8 ; 64 * 1024 ] ;
104172
105173 for name in & entries {
106- // Each name in seg/ is 32 hex chars encoding the 16-byte segment id.
107174 let segment_id = crate :: hex:: parse_hex :: < 16 > ( name)
108175 . ok_or_else ( || PagedbError :: snapshot_artifact_invalid ( "segment_file_name" ) ) ?;
109176 let src_file = seg_src. join ( name) ;
0 commit comments