1use byteorder::{ByteOrder, LittleEndian};
5
6use crate::block_handle::BlockHandle;
7use crate::error::{Error, Result};
8use crate::types::{
9 ChecksumType, LEGACY_FOOTER_SIZE, LEGACY_MAGIC_NUMBER, ROCKSDB_FOOTER_SIZE,
10 ROCKSDB_MAGIC_NUMBER, checksum_modifier_for_context,
11};
12use std::io::{Cursor, Read, Seek, SeekFrom};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Footer {
16 pub checksum_type: ChecksumType,
17 pub metaindex_handle: BlockHandle,
18 pub index_handle: BlockHandle,
19 pub format_version: u32,
20 pub base_context_checksum: Option<u32>,
25}
26
27struct ReverseCursor<'a> {
28 data: &'a [u8],
29 pos: usize,
30}
31
32impl<'a> ReverseCursor<'a> {
33 pub fn new(data: &'a [u8]) -> Self {
34 Self {
35 data,
36 pos: data.len(),
37 }
38 }
39
40 pub fn read_u64(&mut self) -> Result<u64> {
41 if self.pos < 8 {
42 return Err(Error::DataCorruption(
43 "Unable to read data from cursor, because it's end".to_string(),
44 ));
45 }
46
47 self.pos -= 8;
48
49 Ok(LittleEndian::read_u64(&self.data[self.pos..self.pos + 8]))
50 }
51
52 pub fn read_i32(&mut self) -> Result<i32> {
53 if self.pos < 4 {
54 return Err(Error::DataCorruption(
55 "Unable to read data from cursor, because it's end".to_string(),
56 ));
57 }
58
59 self.pos -= 4;
60
61 Ok(LittleEndian::read_i32(&self.data[self.pos..self.pos + 4]))
62 }
63
64 pub fn read_u32(&mut self) -> Result<u32> {
65 if self.pos < 4 {
66 return Err(Error::DataCorruption(
67 "Unable to read data from cursor, because it's end".to_string(),
68 ));
69 }
70
71 self.pos -= 4;
72
73 Ok(LittleEndian::read_u32(&self.data[self.pos..self.pos + 4]))
74 }
75
76 pub fn read_u8(&mut self) -> Result<u8> {
77 if self.pos < 1 {
78 return Err(Error::DataCorruption(
79 "Unable to read data from cursor, because it's end".to_string(),
80 ));
81 }
82
83 self.pos -= 1;
84
85 Ok(self.data[self.pos])
86 }
87
88 pub fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> {
89 if self.pos < buf.len() {
90 return Err(Error::DataCorruption(
91 "Unable to read data from cursor, because it's end".to_string(),
92 ));
93 }
94
95 self.pos -= buf.len();
96 buf.copy_from_slice(&self.data[self.pos..self.pos + buf.len()]);
97
98 Ok(())
99 }
100}
101
102impl Footer {
103 pub fn read_from<R: Read + Seek>(reader: &mut R) -> Result<Self> {
104 let file_size = reader.seek(SeekFrom::End(0))?;
105
106 if file_size < LEGACY_FOOTER_SIZE as u64 {
108 return Err(Error::FileTooSmall);
109 }
110
111 reader.seek(SeekFrom::End(-8))?;
113 let mut magic_bytes = [0u8; 8];
114 reader.read_exact(&mut magic_bytes)?;
115 let magic = u64::from_le_bytes(magic_bytes);
116
117 if magic == ROCKSDB_MAGIC_NUMBER {
118 reader.seek(SeekFrom::End(-12))?;
120 let mut version_bytes = [0u8; 4];
121 reader.read_exact(&mut version_bytes)?;
122
123 if file_size < ROCKSDB_FOOTER_SIZE as u64 {
125 return Err(Error::FileTooSmall);
126 }
127 reader.seek(SeekFrom::End(-(ROCKSDB_FOOTER_SIZE as i64)))?;
128 let mut footer_data = vec![0u8; ROCKSDB_FOOTER_SIZE];
129 reader.read_exact(&mut footer_data)?;
130
131 let input_offset = file_size - (ROCKSDB_FOOTER_SIZE as u64);
132 Self::decode_from_bytes(&footer_data, input_offset)
133 } else {
134 reader.seek(SeekFrom::End(-(LEGACY_FOOTER_SIZE as i64)))?;
136 let mut legacy_magic_bytes = [0u8; 8];
137 reader.read_exact(&mut legacy_magic_bytes)?;
138 let legacy_magic = u64::from_le_bytes(legacy_magic_bytes);
139
140 if legacy_magic == LEGACY_MAGIC_NUMBER {
141 reader.seek(SeekFrom::End(-(LEGACY_FOOTER_SIZE as i64)))?;
143 let mut footer_data = vec![0u8; LEGACY_FOOTER_SIZE];
144 reader.read_exact(&mut footer_data)?;
145 let input_offset = file_size - (LEGACY_FOOTER_SIZE as u64);
146 Self::decode_from_bytes(&footer_data, input_offset)
147 } else {
148 Err(Error::InvalidMagicNumber(magic))
149 }
150 }
151 }
152
153 pub fn decode_from_bytes(data: &[u8], input_offset: u64) -> Result<Self> {
154 if data.len() < 12 {
156 return Err(Error::InvalidFooterSize(data.len()));
157 }
158
159 let mut cursor = ReverseCursor::new(&data);
163 let magic = cursor.read_u64()?;
164
165 if magic == LEGACY_MAGIC_NUMBER {
167 if data.len() != LEGACY_FOOTER_SIZE {
168 return Err(Error::InvalidFooterSize(data.len()));
169 }
170
171 let mut cursor = Cursor::new(&data);
173 let metaindex_handle = BlockHandle::decode_from(&mut cursor)?;
174 let index_handle = BlockHandle::decode_from(&mut cursor)?;
175
176 return Ok(Footer {
177 checksum_type: ChecksumType::CRC32c, metaindex_handle,
179 index_handle,
180 format_version: 0,
181 base_context_checksum: None,
182 });
183 }
184
185 if magic != ROCKSDB_MAGIC_NUMBER {
186 return Err(Error::InvalidMagicNumber(magic));
187 }
188
189 let format_version = cursor.read_u32()?;
190 if format_version >= 6 {
191 {
194 let mut skip_bytes = [0u8; 16];
196 cursor.read_exact(&mut skip_bytes).map_err(|err| {
197 Error::DataCorruption(format!(
198 "Unable to read 16 bytes for reserved padding: {:?}",
199 err
200 ))
201 })?;
202
203 let reserved = cursor.read_u64().map_err(|err| {
206 Error::DataCorruption(format!("Unable to read reserved 8 bytes: {:?}", err))
207 })?;
208 if reserved != 0 {
209 return Err(Error::Unsupported(format!(
210 "File uses a future feature not supported in this version: {}",
211 reserved
212 )));
213 }
214 }
215
216 let adjustment = 5;
218 let footer_offset = input_offset - adjustment;
219
220 let metaindex_size = cursor.read_i32()? as u64;
221 let metaindex_handle = BlockHandle::new(footer_offset - metaindex_size, metaindex_size);
222
223 let index_handle = BlockHandle::new(0, 0);
225
226 let base_context_checksum = cursor.read_i32().map_err(|err| {
227 Error::DataCorruption(format!("Unable to read base context checksum: {:?}", err))
228 })? as u32;
229
230 let stored_checksum = cursor.read_i32().map_err(|err| {
231 Error::DataCorruption(format!("Unable to read stored checksum: {:?}", err))
232 })? as u32;
233
234 {
235 let mut magic_bytes = [0u8; 4];
236 cursor.read_exact(&mut magic_bytes).map_err(|err| {
237 Error::DataCorruption(format!("Unable to read footer magic bytes: {:?}", err))
238 })?;
239
240 if magic_bytes != [0x3e, 0x00, 0x7a, 0x00] {
242 return Err(Error::DataCorruption(format!(
243 "Invalid extended magic, actual: {:?}",
244 magic_bytes
245 )));
246 }
247 }
248
249 let checksum_type = ChecksumType::try_from(cursor.read_u8()?)?;
250
251 let mut footer_copy = data.to_vec();
253 footer_copy[5..9].fill(0);
255
256 let computed_checksum = checksum_type.calculate(&footer_copy);
257 let modified_checksum = computed_checksum.wrapping_add(checksum_modifier_for_context(
258 base_context_checksum,
259 input_offset,
260 ));
261
262 if modified_checksum != stored_checksum {
263 return Err(Error::DataCorruption(format!(
264 "Footer checksum mismatch at offset {}: expected {:#x}, computed {:#x}",
265 input_offset, stored_checksum, modified_checksum
266 )));
267 }
268
269 Ok(Footer {
270 checksum_type,
271 metaindex_handle,
272 index_handle,
273 format_version,
274 base_context_checksum: Some(base_context_checksum),
275 })
276 } else {
277 let version_start = data.len() - 12;
278
279 let (checksum_type, phase2_data) = if data[0] <= 0x7F && format_version >= 1 {
283 match ChecksumType::try_from(data[0]) {
285 Ok(ct) => (ct, &data[1..version_start]),
286 Err(_) => (ChecksumType::CRC32c, &data[..version_start]),
287 }
288 } else {
289 (ChecksumType::CRC32c, &data[..version_start])
291 };
292
293 let mut padded_cursor: Cursor<&[u8]> = Cursor::new(phase2_data);
295 let metaindex_handle = BlockHandle::decode_from(&mut padded_cursor)?;
296 let index_handle = BlockHandle::decode_from(&mut padded_cursor)?;
297
298 Ok(Footer {
299 checksum_type,
300 metaindex_handle,
301 index_handle,
302 format_version,
303 base_context_checksum: None,
304 })
305 }
306 }
307
308 pub fn encode_to_bytes(&self, offset: u64) -> Result<Vec<u8>> {
309 if self.format_version >= 6 {
310 let mut data = Vec::with_capacity(53);
312
313 data.push(self.checksum_type as u8);
315 data.extend(&[0x3e, 0x00, 0x7a, 0x00]);
317
318 data.extend(&[0u8; 4]);
320
321 let base_context_checksum = self.base_context_checksum.unwrap_or(0);
323 data.extend(&(base_context_checksum as i32).to_le_bytes());
324 data.extend(&(self.metaindex_handle.size as i32).to_le_bytes());
326
327 data.extend(&[0u8; 8]);
329 data.extend(&[0u8; 16]);
331
332 data.extend(&self.format_version.to_le_bytes());
333 data.extend(&ROCKSDB_MAGIC_NUMBER.to_le_bytes());
334
335 let computed_checksum = self.checksum_type.calculate(&data);
337 let modified_checksum = computed_checksum
338 .wrapping_add(checksum_modifier_for_context(base_context_checksum, offset));
339
340 data[5..9].copy_from_slice(&(modified_checksum as i32).to_le_bytes());
342
343 Ok(data)
344 } else {
345 let mut data = Vec::with_capacity(ROCKSDB_FOOTER_SIZE);
347
348 data.push(self.checksum_type as u8);
350
351 self.metaindex_handle.encode_to(&mut data)?;
353 self.index_handle.encode_to(&mut data)?;
354
355 let used_bytes = data.len();
356
357 let padding_size = ROCKSDB_FOOTER_SIZE - used_bytes - 12; data.extend(vec![0u8; padding_size]);
360 data.extend(&self.format_version.to_le_bytes());
361 data.extend(&ROCKSDB_MAGIC_NUMBER.to_le_bytes());
362
363 assert_eq!(data.len(), ROCKSDB_FOOTER_SIZE);
364 Ok(data)
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_footer_magic_number_validation() -> Result<()> {
375 let footer = Footer {
376 checksum_type: ChecksumType::CRC32c,
377 metaindex_handle: BlockHandle::new(1000, 500),
378 index_handle: BlockHandle::new(1500, 200),
379 format_version: 5,
380 base_context_checksum: None,
381 };
382
383 let mut encoded = footer.encode_to_bytes(1500)?; encoded[ROCKSDB_FOOTER_SIZE - 1] = 0xFF;
386
387 let footer_offset = 1500; let result = Footer::decode_from_bytes(&encoded, footer_offset);
389 assert!(matches!(result, Err(Error::InvalidMagicNumber(_))));
390 Ok(())
391 }
392
393 #[test]
394 fn test_footer_size_validation() -> Result<()> {
395 let data = vec![0u8; 10]; let footer_offset = 0; let result = Footer::decode_from_bytes(&data, footer_offset);
398 assert!(result.is_err());
400 Ok(())
401 }
402
403 #[test]
404 fn test_footer_roundtrip_v5() -> Result<()> {
405 let original = Footer {
406 checksum_type: ChecksumType::CRC32c,
407 metaindex_handle: BlockHandle::new(1000, 500),
408 index_handle: BlockHandle::new(1500, 200),
409 format_version: 5,
410 base_context_checksum: None,
411 };
412
413 let footer_offset = 1000; let encoded = original.encode_to_bytes(footer_offset)?;
415 assert_eq!(encoded.len(), ROCKSDB_FOOTER_SIZE);
416 let decoded = Footer::decode_from_bytes(&encoded, footer_offset)?;
417
418 assert_eq!(decoded.checksum_type, original.checksum_type);
420 assert_eq!(
421 decoded.metaindex_handle.size,
422 original.metaindex_handle.size
423 );
424 assert_eq!(
425 decoded.metaindex_handle.offset,
426 original.metaindex_handle.offset
427 );
428 assert_eq!(decoded.index_handle.size, original.index_handle.size);
429 assert_eq!(decoded.index_handle.offset, original.index_handle.offset);
430 assert_eq!(decoded.format_version, original.format_version);
431 assert_eq!(
432 decoded.base_context_checksum,
433 original.base_context_checksum
434 );
435 Ok(())
436 }
437
438 #[test]
439 fn test_footer_v6_roundtrip() -> Result<()> {
440 let input_offset = 100000; let metaindex_size = 500;
444 let expected_metaindex_offset = (input_offset - 5) - metaindex_size; let original = Footer {
447 checksum_type: ChecksumType::CRC32c,
448 metaindex_handle: BlockHandle::new(expected_metaindex_offset, metaindex_size),
449 index_handle: BlockHandle::new(0, 0), format_version: 6,
451 base_context_checksum: Some(0x12345678),
452 };
453
454 let encoded = original.encode_to_bytes(input_offset)?;
455 assert_eq!(encoded.len(), 53); let decoded = Footer::decode_from_bytes(&encoded, input_offset)?;
458
459 assert_eq!(decoded.checksum_type, original.checksum_type);
461 assert_eq!(
462 decoded.metaindex_handle.size,
463 original.metaindex_handle.size
464 );
465 assert_eq!(decoded.metaindex_handle.offset, expected_metaindex_offset);
466 assert_eq!(decoded.index_handle, original.index_handle);
467 assert_eq!(decoded.format_version, original.format_version);
468 assert_eq!(
469 decoded.base_context_checksum,
470 original.base_context_checksum
471 );
472 Ok(())
473 }
474
475 #[test]
476 fn test_footer_v6_with_different_checksum_types() -> Result<()> {
477 let checksum_types = [
478 ChecksumType::None,
479 ChecksumType::CRC32c,
480 ChecksumType::Hash,
481 ChecksumType::Hash64,
482 ChecksumType::XXH3,
483 ];
484
485 for checksum_type in checksum_types {
486 let input_offset = 50000; let metaindex_size = 1024;
488
489 let footer = Footer {
490 checksum_type,
491 metaindex_handle: BlockHandle::new(
492 (input_offset - 5) - metaindex_size,
493 metaindex_size,
494 ),
495 index_handle: BlockHandle::new(0, 0),
496 format_version: 6,
497 base_context_checksum: Some(0xABCDEF12),
498 };
499
500 let encoded = footer.encode_to_bytes(input_offset)?;
501 assert_eq!(encoded.len(), 53);
502
503 let decoded = Footer::decode_from_bytes(&encoded, input_offset)?;
504
505 assert_eq!(decoded.checksum_type, checksum_type);
506 assert_eq!(decoded.format_version, 6);
507 assert_eq!(decoded.base_context_checksum, Some(0xABCDEF12));
508 }
509 Ok(())
510 }
511
512 #[test]
513 fn test_footer_v7_roundtrip() -> Result<()> {
514 let input_offset = 75000; let metaindex_size = 2048;
516
517 let original = Footer {
518 checksum_type: ChecksumType::XXH3,
519 metaindex_handle: BlockHandle::new((input_offset - 5) - metaindex_size, metaindex_size),
520 index_handle: BlockHandle::new(0, 0), format_version: 7,
522 base_context_checksum: Some(0x87654321),
523 };
524
525 let encoded = original.encode_to_bytes(input_offset)?;
526 assert_eq!(encoded.len(), 53); let decoded = Footer::decode_from_bytes(&encoded, input_offset)?;
529
530 assert_eq!(decoded.checksum_type, original.checksum_type);
531 assert_eq!(
532 decoded.metaindex_handle.size,
533 original.metaindex_handle.size
534 );
535 assert_eq!(
536 decoded.metaindex_handle.offset,
537 original.metaindex_handle.offset
538 );
539 assert_eq!(decoded.index_handle, original.index_handle);
540 assert_eq!(decoded.format_version, original.format_version);
541 assert_eq!(
542 decoded.base_context_checksum,
543 original.base_context_checksum
544 );
545 Ok(())
546 }
547
548 #[test]
549 fn test_footer_v6_no_base_context_checksum() -> Result<()> {
550 let input_offset = 25000; let metaindex_size = 512;
553
554 let footer = Footer {
555 checksum_type: ChecksumType::CRC32c,
556 metaindex_handle: BlockHandle::new((input_offset - 5) - metaindex_size, metaindex_size),
557 index_handle: BlockHandle::new(0, 0),
558 format_version: 6,
559 base_context_checksum: None,
560 };
561
562 let encoded = footer.encode_to_bytes(input_offset)?;
563 assert_eq!(encoded.len(), 53);
564
565 let decoded = Footer::decode_from_bytes(&encoded, input_offset)?;
566
567 assert_eq!(decoded.base_context_checksum, Some(0));
570 assert_eq!(decoded.format_version, 6);
571 Ok(())
572 }
573
574 #[test]
575 fn test_footer_v6_encoding_with_offset() -> Result<()> {
576 let footer = Footer {
578 checksum_type: ChecksumType::CRC32c,
579 metaindex_handle: BlockHandle::new(0, 256),
580 index_handle: BlockHandle::new(0, 0),
581 format_version: 6,
582 base_context_checksum: Some(0x11223344),
583 };
584
585 let encoded_offset_0 = footer.encode_to_bytes(0)?;
586 let encoded_offset_1000 = footer.encode_to_bytes(1000)?;
587
588 assert_ne!(encoded_offset_0, encoded_offset_1000);
590 assert_eq!(encoded_offset_0.len(), 53);
591 assert_eq!(encoded_offset_1000.len(), 53);
592 Ok(())
593 }
594}