1pub mod usn_extractor;
8
9pub use usn_extractor::{extract_usn_from_logfile, LogFileRecordSource, LogFileUsnRecord};
10
11use crate::error::Result;
12
13const RSTR_SIGNATURE: &[u8; 4] = b"RSTR";
17
18const RCRD_SIGNATURE: &[u8; 4] = b"RCRD";
20
21const LOG_PAGE_SIZE: usize = 0x1000; #[derive(Debug, Clone)]
28pub struct RestartArea {
29 pub offset: usize,
30 pub current_lsn: u64,
31 pub log_clients: u16,
32 pub system_page_size: u32,
33 pub log_page_size: u32,
34}
35
36#[derive(Debug, Clone)]
38pub struct LogFileSummary {
39 pub restart_areas: Vec<RestartArea>,
40 pub record_page_count: usize,
41 pub has_gaps: bool,
42 pub highest_lsn: u64,
43}
44
45pub fn parse_logfile(data: &[u8]) -> Result<LogFileSummary> {
50 let mut restart_areas = Vec::new();
51 let mut record_page_count = 0;
52 let mut highest_lsn: u64 = 0;
53 let mut has_gaps = false;
54 let mut last_page_had_rcrd = false;
55
56 let page_count = data.len() / LOG_PAGE_SIZE;
57
58 for page_idx in 0..page_count {
59 let page_offset = page_idx * LOG_PAGE_SIZE;
60
61 let sig = &data[page_offset..page_offset + 4];
63
64 if sig == RSTR_SIGNATURE {
65 if page_offset + 0x28 <= data.len() {
66 let current_lsn = u64::from_le_bytes(
67 data[page_offset + 0x08..page_offset + 0x10]
68 .try_into()
69 .unwrap_or([0; 8]),
70 );
71 let log_clients = u16::from_le_bytes(
72 data[page_offset + 0x10..page_offset + 0x12]
73 .try_into()
74 .unwrap_or([0; 2]),
75 );
76 let system_page_size = u32::from_le_bytes(
77 data[page_offset + 0x20..page_offset + 0x24]
78 .try_into()
79 .unwrap_or([0; 4]),
80 );
81 let log_page_size = u32::from_le_bytes(
82 data[page_offset + 0x24..page_offset + 0x28]
83 .try_into()
84 .unwrap_or([0; 4]),
85 );
86
87 if current_lsn > highest_lsn {
88 highest_lsn = current_lsn;
89 }
90
91 restart_areas.push(RestartArea {
92 offset: page_offset,
93 current_lsn,
94 log_clients,
95 system_page_size,
96 log_page_size,
97 });
98 } last_page_had_rcrd = false;
100 } else if sig == RCRD_SIGNATURE {
101 record_page_count += 1;
102
103 if page_offset + 0x20 <= data.len() {
105 let page_lsn = u64::from_le_bytes(
106 data[page_offset + 0x18..page_offset + 0x20]
107 .try_into()
108 .unwrap_or([0; 8]),
109 );
110 if page_lsn > highest_lsn {
111 highest_lsn = page_lsn;
112 }
113 } last_page_had_rcrd = true;
116 } else {
117 if last_page_had_rcrd && page_idx > 2 {
119 let is_zeroed = data[page_offset..page_offset + 4] == [0, 0, 0, 0];
121 if !is_zeroed {
122 has_gaps = true;
123 }
124 }
125 last_page_had_rcrd = false;
126 }
127 }
128
129 Ok(LogFileSummary {
130 restart_areas,
131 record_page_count,
132 has_gaps,
133 highest_lsn,
134 })
135}
136
137pub fn detect_journal_clearing(logfile_summary: &LogFileSummary) -> bool {
143 if logfile_summary.has_gaps {
149 return true;
150 }
151
152 if logfile_summary.restart_areas.len() != 2 {
153 return logfile_summary.restart_areas.is_empty();
154 }
155
156 false
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn make_rstr_page(lsn: u64) -> Vec<u8> {
164 let mut page = vec![0u8; LOG_PAGE_SIZE];
165 page[0..4].copy_from_slice(RSTR_SIGNATURE);
166 page[0x08..0x10].copy_from_slice(&lsn.to_le_bytes());
167 page[0x10..0x12].copy_from_slice(&1u16.to_le_bytes()); page[0x20..0x24].copy_from_slice(&4096u32.to_le_bytes());
169 page[0x24..0x28].copy_from_slice(&4096u32.to_le_bytes());
170 page
171 }
172
173 fn make_rcrd_page(lsn: u64) -> Vec<u8> {
174 let mut page = vec![0u8; LOG_PAGE_SIZE];
175 page[0..4].copy_from_slice(RCRD_SIGNATURE);
176 page[0x18..0x20].copy_from_slice(&lsn.to_le_bytes());
177 page
178 }
179
180 #[test]
181 fn test_parse_logfile_with_restart_areas() {
182 let mut data = Vec::new();
183 data.extend_from_slice(&make_rstr_page(1000));
184 data.extend_from_slice(&make_rstr_page(2000));
185 data.extend_from_slice(&make_rcrd_page(3000));
186
187 let summary = parse_logfile(&data).unwrap();
188 assert_eq!(summary.restart_areas.len(), 2);
189 assert_eq!(summary.record_page_count, 1);
190 assert_eq!(summary.highest_lsn, 3000);
191 assert!(!summary.has_gaps);
192 }
193
194 #[test]
195 fn test_detect_journal_clearing_with_gaps() {
196 let summary = LogFileSummary {
197 restart_areas: vec![],
198 record_page_count: 0,
199 has_gaps: true,
200 highest_lsn: 0,
201 };
202 assert!(detect_journal_clearing(&summary));
203 }
204
205 #[test]
206 fn test_normal_logfile_no_clearing() {
207 let summary = LogFileSummary {
208 restart_areas: vec![
209 RestartArea {
210 offset: 0,
211 current_lsn: 1000,
212 log_clients: 1,
213 system_page_size: 4096,
214 log_page_size: 4096,
215 },
216 RestartArea {
217 offset: 4096,
218 current_lsn: 2000,
219 log_clients: 1,
220 system_page_size: 4096,
221 log_page_size: 4096,
222 },
223 ],
224 record_page_count: 100,
225 has_gaps: false,
226 highest_lsn: 5000,
227 };
228 assert!(!detect_journal_clearing(&summary));
229 }
230
231 #[test]
232 fn test_detect_journal_clearing_empty_restart_areas() {
233 let summary = LogFileSummary {
234 restart_areas: vec![],
235 record_page_count: 0,
236 has_gaps: false,
237 highest_lsn: 0,
238 };
239 assert!(detect_journal_clearing(&summary));
240 }
241
242 #[test]
243 fn test_detect_journal_clearing_one_restart_area() {
244 let summary = LogFileSummary {
246 restart_areas: vec![RestartArea {
247 offset: 0,
248 current_lsn: 1000,
249 log_clients: 1,
250 system_page_size: 4096,
251 log_page_size: 4096,
252 }],
253 record_page_count: 50,
254 has_gaps: false,
255 highest_lsn: 5000,
256 };
257 assert!(!detect_journal_clearing(&summary));
258 }
259
260 #[test]
261 fn test_detect_journal_clearing_three_restart_areas() {
262 let summary = LogFileSummary {
264 restart_areas: vec![
265 RestartArea {
266 offset: 0,
267 current_lsn: 1000,
268 log_clients: 1,
269 system_page_size: 4096,
270 log_page_size: 4096,
271 },
272 RestartArea {
273 offset: 4096,
274 current_lsn: 2000,
275 log_clients: 1,
276 system_page_size: 4096,
277 log_page_size: 4096,
278 },
279 RestartArea {
280 offset: 8192,
281 current_lsn: 3000,
282 log_clients: 1,
283 system_page_size: 4096,
284 log_page_size: 4096,
285 },
286 ],
287 record_page_count: 50,
288 has_gaps: false,
289 highest_lsn: 5000,
290 };
291 assert!(!detect_journal_clearing(&summary));
292 }
293
294 #[test]
295 fn test_parse_logfile_empty() {
296 let summary = parse_logfile(&[]).unwrap();
297 assert_eq!(summary.restart_areas.len(), 0);
298 assert_eq!(summary.record_page_count, 0);
299 assert!(!summary.has_gaps);
300 assert_eq!(summary.highest_lsn, 0);
301 }
302
303 #[test]
304 fn test_parse_logfile_only_rcrd_pages() {
305 let mut data = Vec::new();
306 data.extend_from_slice(&make_rcrd_page(1000));
307 data.extend_from_slice(&make_rcrd_page(2000));
308 data.extend_from_slice(&make_rcrd_page(3000));
309
310 let summary = parse_logfile(&data).unwrap();
311 assert_eq!(summary.restart_areas.len(), 0);
312 assert_eq!(summary.record_page_count, 3);
313 assert_eq!(summary.highest_lsn, 3000);
314 }
315
316 #[test]
317 fn test_parse_logfile_gap_detection() {
318 let mut data = Vec::new();
321 data.extend_from_slice(&make_rstr_page(1000));
322 data.extend_from_slice(&make_rstr_page(2000));
323 data.extend_from_slice(&make_rcrd_page(3000));
324
325 let mut garbage_page = vec![0xDEu8; LOG_PAGE_SIZE];
327 garbage_page[0..4].copy_from_slice(b"JUNK");
328 data.extend_from_slice(&garbage_page);
329
330 data.extend_from_slice(&make_rcrd_page(5000));
331
332 let summary = parse_logfile(&data).unwrap();
333 assert!(summary.has_gaps);
334 }
335
336 #[test]
337 fn test_parse_logfile_no_gap_for_zeroed_page() {
338 let mut data = Vec::new();
340 data.extend_from_slice(&make_rstr_page(1000));
341 data.extend_from_slice(&make_rstr_page(2000));
342 data.extend_from_slice(&make_rcrd_page(3000));
343 data.extend_from_slice(&vec![0u8; LOG_PAGE_SIZE]); let summary = parse_logfile(&data).unwrap();
346 assert!(!summary.has_gaps);
347 }
348
349 #[test]
350 fn test_parse_logfile_restart_area_lsn_tracking() {
351 let mut data = Vec::new();
352 data.extend_from_slice(&make_rstr_page(5000));
353 data.extend_from_slice(&make_rstr_page(3000));
354 data.extend_from_slice(&make_rcrd_page(4000));
355
356 let summary = parse_logfile(&data).unwrap();
357 assert_eq!(summary.highest_lsn, 5000);
358 assert_eq!(summary.restart_areas.len(), 2);
359 assert_eq!(summary.restart_areas[0].current_lsn, 5000);
360 assert_eq!(summary.restart_areas[1].current_lsn, 3000);
361 }
362
363 #[test]
364 fn test_parse_logfile_short_rstr_page() {
365 let mut data = vec![0u8; LOG_PAGE_SIZE];
367 data[0..4].copy_from_slice(RSTR_SIGNATURE);
368 let summary = parse_logfile(&data).unwrap();
373 assert_eq!(summary.restart_areas.len(), 1);
374 assert_eq!(summary.restart_areas[0].current_lsn, 0);
375 }
376
377 #[test]
378 fn test_parse_logfile_page_offset_boundary() {
379 let data = make_rcrd_page(5000);
389 assert_eq!(data.len(), LOG_PAGE_SIZE);
390 let summary = parse_logfile(&data).unwrap();
391 assert_eq!(summary.record_page_count, 1);
392 assert_eq!(summary.highest_lsn, 5000);
393 }
394
395 #[test]
396 fn test_parse_logfile_data_smaller_than_page() {
397 let data = vec![0xAAu8; 100];
399 let summary = parse_logfile(&data).unwrap();
400 assert_eq!(summary.restart_areas.len(), 0);
401 assert_eq!(summary.record_page_count, 0);
402 }
403
404 #[test]
405 fn test_parse_logfile_boundary_check_line_61() {
406 let data = vec![0u8; LOG_PAGE_SIZE];
413 let summary = parse_logfile(&data).unwrap();
414 assert_eq!(summary.restart_areas.len(), 0);
416 assert_eq!(summary.record_page_count, 0);
417 assert!(!summary.has_gaps);
418 }
419
420 #[test]
421 fn test_parse_logfile_gap_not_flagged_early_pages() {
422 let mut data = Vec::new();
426 data.extend_from_slice(&make_rcrd_page(1000)); let mut garbage = vec![0xDEu8; LOG_PAGE_SIZE];
428 garbage[0..4].copy_from_slice(b"JUNK");
429 data.extend_from_slice(&garbage); let summary = parse_logfile(&data).unwrap();
432 assert!(!summary.has_gaps);
433 }
434
435 #[test]
436 fn test_parse_logfile_rstr_too_short_for_header() {
437 let mut data = make_rstr_page(0);
442 data[0x08..0x10].copy_from_slice(&0u64.to_le_bytes());
444
445 let summary = parse_logfile(&data).unwrap();
446 assert_eq!(summary.restart_areas.len(), 1);
447 assert_eq!(summary.restart_areas[0].current_lsn, 0);
448 assert_eq!(summary.highest_lsn, 0);
449 }
450}