1use std::collections::BTreeMap;
19use std::ops::Range;
20
21use bytes::{BufMut, Bytes, BytesMut};
22use snafu::{OptionExt, ensure};
23use store_api::storage::RegionId;
24
25use crate::error::{CorruptedWalObjectSnafu, Result};
26
27const HEADER_MAGIC: &[u8; 8] = b"GTWALOBJ";
28const TRAILER_MAGIC: &[u8; 8] = b"GTWALTRL";
29const FORMAT_VERSION: u16 = 1;
30const NO_PREDECESSOR: u64 = u64::MAX;
33
34pub(crate) const HEADER_LEN: usize = 8 + 2 + 8 + 8 + 8 + 8 + 8 + 4;
38pub(crate) const TRAILER_LEN: usize = 8 + 8 + 4 + 4 + 8;
41const SEGMENT_HEADER_LEN: usize = 8 + 4;
42pub(crate) const FOOTER_ENTRY_LEN: usize = 8 + 8 + 8 + 4 + 8 + 8 + 4;
45const FOOTER_COUNT_LEN: usize = 4;
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub(crate) struct Header {
51 pub(crate) object_seq: u64,
52 pub(crate) epoch: u64,
56 pub(crate) prev: Option<ChainLink>,
58 pub(crate) reclaim_boundary: u64,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub(crate) struct ChainLink {
68 pub(crate) object_seq: u64,
69 pub(crate) epoch: u64,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub(crate) struct Record {
75 pub(crate) region_id: RegionId,
76 pub(crate) entry_id: u64,
77 pub(crate) payload: Bytes,
78}
79
80pub(crate) const RELEASE_REGION_ID: RegionId = RegionId::from_u64(u64::MAX);
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub(crate) struct Release {
90 pub(crate) region_id: RegionId,
91 pub(crate) entry_id: u64,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub(crate) struct FooterEntry {
97 pub(crate) region_id: RegionId,
98 pub(crate) min_entry_id: u64,
99 pub(crate) max_entry_id: u64,
100 pub(crate) entry_count: u32,
101 pub(crate) segment_offset: u64,
102 pub(crate) segment_len: u64,
103 pub(crate) segment_crc32: u32,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub(crate) struct FixedTrailer {
109 pub(crate) footer_offset: u64,
110 pub(crate) footer_len: u64,
111 pub(crate) footer_crc32: u32,
112 pub(crate) object_crc32: u32,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117pub(crate) struct EncodedObject {
118 pub(crate) bytes: Bytes,
119 pub(crate) footer: Vec<FooterEntry>,
120 pub(crate) trailer: FixedTrailer,
121}
122
123#[cfg(test)]
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub(crate) struct DecodedObject {
127 pub(crate) header: Header,
128 pub(crate) records: Vec<Record>,
129 pub(crate) footer: Vec<FooterEntry>,
130}
131
132pub(crate) fn encode_object(header: Header, records: &[Record]) -> Result<EncodedObject> {
136 let mut grouped = BTreeMap::<RegionId, Vec<&Record>>::new();
137 for record in records {
138 grouped.entry(record.region_id).or_default().push(record);
139 }
140 for (region_id, records) in &mut grouped {
141 records.sort_unstable_by_key(|record| record.entry_id);
142 ensure!(
143 !records
144 .windows(2)
145 .any(|pair| pair[0].entry_id >= pair[1].entry_id),
146 CorruptedWalObjectSnafu {
147 reason: format!("non-monotonic entry id for region {region_id}"),
148 }
149 );
150 }
151
152 let mut output = BytesMut::new();
153 encode_header(&header, &mut output);
154
155 let mut directory = Vec::with_capacity(grouped.len());
156 for (region_id, records) in grouped {
157 let segment_offset = to_u64(output.len(), "segment offset")?;
158 let entry_count = to_u32(records.len(), "segment entry count")?;
159 let segment_start = output.len();
160
161 output.put_u64(region_id.as_u64());
162 output.put_u32(entry_count);
163 for record in &records {
164 output.put_u64(record.entry_id);
165 output.put_u32(to_u32(record.payload.len(), "record payload")?);
166 output.put_slice(&record.payload);
167 }
168
169 let segment = &output[segment_start..];
170 directory.push(FooterEntry {
171 region_id,
172 min_entry_id: records[0].entry_id,
173 max_entry_id: records[records.len() - 1].entry_id,
174 entry_count,
175 segment_offset,
176 segment_len: to_u64(segment.len(), "segment length")?,
177 segment_crc32: crc32fast::hash(segment),
178 });
179 }
180
181 let footer_offset = to_u64(output.len(), "footer offset")?;
182 let footer_start = output.len();
183 encode_footer(&directory, &mut output)?;
184 let footer = &output[footer_start..];
185 let trailer = FixedTrailer {
186 footer_offset,
187 footer_len: to_u64(footer.len(), "footer length")?,
188 footer_crc32: crc32fast::hash(footer),
189 object_crc32: 0,
190 };
191 let trailer = FixedTrailer {
192 object_crc32: object_crc32(&output, trailer),
193 ..trailer
194 };
195 encode_trailer(trailer, &mut output);
196
197 Ok(EncodedObject {
198 bytes: output.freeze(),
199 footer: directory,
200 trailer,
201 })
202}
203
204pub(crate) fn object_len(footer: &[FooterEntry]) -> u64 {
208 let segments = footer.iter().map(|entry| entry.segment_len).sum::<u64>();
209 let framing = HEADER_LEN + FOOTER_COUNT_LEN + footer.len() * FOOTER_ENTRY_LEN + TRAILER_LEN;
210 segments + framing as u64
211}
212
213pub(crate) fn decode_header(bytes: &[u8]) -> Result<Header> {
216 ensure!(
217 bytes.len() >= HEADER_LEN,
218 CorruptedWalObjectSnafu {
219 reason: truncated("header", bytes.len(), HEADER_LEN),
220 }
221 );
222
223 let mut reader = Reader::new(&bytes[..HEADER_LEN]);
224 let magic = reader.take(HEADER_MAGIC.len(), "header")?;
225 ensure!(
226 magic == HEADER_MAGIC,
227 CorruptedWalObjectSnafu {
228 reason: invalid_magic("header", HEADER_MAGIC, magic),
229 }
230 );
231 let version = reader.u16("header")?;
232 ensure!(
233 version == FORMAT_VERSION,
234 CorruptedWalObjectSnafu {
235 reason: format!(
236 "unsupported format version {version}, expected version {FORMAT_VERSION}"
237 ),
238 }
239 );
240 let object_seq = reader.u64("header")?;
241 let epoch = reader.u64("header")?;
242 let prev_object_seq = reader.u64("header")?;
243 let prev_epoch = reader.u64("header")?;
244 let reclaim_boundary = reader.u64("header")?;
245 let header_crc32 = reader.u32("header")?;
246 let checksum = crc32fast::hash(&bytes[..HEADER_LEN - 4]);
247 ensure!(
248 checksum == header_crc32,
249 CorruptedWalObjectSnafu {
250 reason: checksum_mismatch("header", header_crc32, checksum),
251 }
252 );
253 ensure!(
254 reclaim_boundary <= object_seq,
255 CorruptedWalObjectSnafu {
256 reason: format!(
257 "reclaim boundary {reclaim_boundary} is above the object sequence {object_seq}"
258 ),
259 }
260 );
261 let prev = (prev_object_seq != NO_PREDECESSOR).then_some(ChainLink {
262 object_seq: prev_object_seq,
263 epoch: prev_epoch,
264 });
265 Ok(Header {
266 object_seq,
267 epoch,
268 prev,
269 reclaim_boundary,
270 })
271}
272
273pub(crate) fn decode_trailer(bytes: &[u8]) -> Result<FixedTrailer> {
275 ensure!(
276 bytes.len() >= TRAILER_LEN,
277 CorruptedWalObjectSnafu {
278 reason: truncated("trailer", bytes.len(), TRAILER_LEN),
279 }
280 );
281 ensure!(
282 bytes.len() <= TRAILER_LEN,
283 CorruptedWalObjectSnafu {
284 reason: trailing_bytes("trailer", bytes.len(), TRAILER_LEN),
285 }
286 );
287
288 let mut reader = Reader::new(bytes);
289 let trailer = FixedTrailer {
290 footer_offset: reader.u64("trailer")?,
291 footer_len: reader.u64("trailer")?,
292 footer_crc32: reader.u32("trailer")?,
293 object_crc32: reader.u32("trailer")?,
294 };
295 let magic = reader.take(TRAILER_MAGIC.len(), "trailer")?;
296 ensure!(
297 magic == TRAILER_MAGIC,
298 CorruptedWalObjectSnafu {
299 reason: invalid_magic("trailer", TRAILER_MAGIC, magic),
300 }
301 );
302 Ok(trailer)
303}
304
305pub(crate) fn decode_footer(bytes: &[u8], trailer: FixedTrailer) -> Result<Vec<FooterEntry>> {
307 let footer_len = to_usize(trailer.footer_len, "footer length")?;
308 ensure!(
309 bytes.len() >= footer_len,
310 CorruptedWalObjectSnafu {
311 reason: truncated("footer", bytes.len(), footer_len),
312 }
313 );
314 ensure!(
315 bytes.len() <= footer_len,
316 CorruptedWalObjectSnafu {
317 reason: trailing_bytes("footer", bytes.len(), footer_len),
318 }
319 );
320 let checksum = crc32fast::hash(bytes);
321 ensure!(
322 checksum == trailer.footer_crc32,
323 CorruptedWalObjectSnafu {
324 reason: checksum_mismatch("footer", trailer.footer_crc32, checksum),
325 }
326 );
327
328 let mut reader = Reader::new(bytes);
329 let count = reader.u32("footer")? as usize;
330 let expected_len = count
331 .checked_mul(FOOTER_ENTRY_LEN)
332 .and_then(|len| len.checked_add(4))
333 .with_context(|| CorruptedWalObjectSnafu {
334 reason: format!("footer declares {count} entries, which overflows its length"),
335 })?;
336 ensure!(
337 expected_len == bytes.len(),
338 CorruptedWalObjectSnafu {
339 reason: format!(
340 "footer declares {count} entries, expected length {expected_len}, actual {}",
341 bytes.len()
342 ),
343 }
344 );
345
346 let mut directory = Vec::with_capacity(count);
347 let mut previous_region = None;
348 for _ in 0..count {
349 let region_id = RegionId::from_u64(reader.u64("footer")?);
350 if let Some(previous) = previous_region {
351 ensure!(
352 previous < region_id,
353 CorruptedWalObjectSnafu {
354 reason: if previous == region_id {
355 format!("duplicate footer entry for region {region_id}")
356 } else {
357 format!(
358 "footer entries are not ordered by region, {previous} precedes {region_id}"
359 )
360 },
361 }
362 );
363 }
364 previous_region = Some(region_id);
365
366 let entry = FooterEntry {
367 region_id,
368 min_entry_id: reader.u64("footer")?,
369 max_entry_id: reader.u64("footer")?,
370 entry_count: reader.u32("footer")?,
371 segment_offset: reader.u64("footer")?,
372 segment_len: reader.u64("footer")?,
373 segment_crc32: reader.u32("footer")?,
374 };
375 ensure!(
376 entry.entry_count > 0
377 && entry.min_entry_id <= entry.max_entry_id
378 && entry.segment_len >= SEGMENT_HEADER_LEN as u64,
379 CorruptedWalObjectSnafu {
380 reason: format!(
381 "invalid footer entry for region {}, entry ids {}..={}, {} entries, segment length {}",
382 entry.region_id,
383 entry.min_entry_id,
384 entry.max_entry_id,
385 entry.entry_count,
386 entry.segment_len
387 ),
388 }
389 );
390 directory.push(entry);
391 }
392 Ok(directory)
393}
394
395pub(crate) fn decode_segment(bytes: &[u8], entry: &FooterEntry) -> Result<Vec<Record>> {
397 let segment_len = to_usize(entry.segment_len, "segment length")?;
398 ensure!(
399 bytes.len() >= segment_len,
400 CorruptedWalObjectSnafu {
401 reason: truncated("segment", bytes.len(), segment_len),
402 }
403 );
404 ensure!(
405 bytes.len() <= segment_len,
406 CorruptedWalObjectSnafu {
407 reason: trailing_bytes("segment", bytes.len(), segment_len),
408 }
409 );
410 let checksum = crc32fast::hash(bytes);
411 ensure!(
412 checksum == entry.segment_crc32,
413 CorruptedWalObjectSnafu {
414 reason: checksum_mismatch(
415 &format!("segment of region {}", entry.region_id),
416 entry.segment_crc32,
417 checksum
418 ),
419 }
420 );
421
422 let mut reader = Reader::new(bytes);
423 let region_id = RegionId::from_u64(reader.u64("segment")?);
424 let count = reader.u32("segment")?;
425 ensure!(
426 region_id == entry.region_id && count == entry.entry_count,
427 CorruptedWalObjectSnafu {
428 reason: format!(
429 "segment holds region {region_id} with {count} entries, footer expects region {} with {} entries",
430 entry.region_id, entry.entry_count
431 ),
432 }
433 );
434 let max_count = reader.remaining_len() / (8 + 4);
437 ensure!(
438 count as usize <= max_count,
439 CorruptedWalObjectSnafu {
440 reason: format!(
441 "segment declares {count} entries but only holds bytes for {max_count}"
442 ),
443 }
444 );
445
446 let mut records = Vec::with_capacity(count as usize);
447 let mut previous_id = None;
448 for _ in 0..count {
449 let entry_id = reader.u64("segment")?;
450 ensure!(
451 previous_id.is_none_or(|previous| previous < entry_id),
452 CorruptedWalObjectSnafu {
453 reason: format!("non-monotonic entry id for region {region_id}"),
454 }
455 );
456 previous_id = Some(entry_id);
457 let payload_len = reader.u32("segment")? as usize;
458 let payload = Bytes::copy_from_slice(reader.take(payload_len, "segment")?);
459 records.push(Record {
460 region_id,
461 entry_id,
462 payload,
463 });
464 }
465 ensure!(
466 reader.is_empty(),
467 CorruptedWalObjectSnafu {
468 reason: trailing_bytes("segment", bytes.len(), bytes.len() - reader.remaining_len()),
469 }
470 );
471 ensure!(
472 records.first().map(|record| record.entry_id) == Some(entry.min_entry_id)
473 && records.last().map(|record| record.entry_id) == Some(entry.max_entry_id),
474 CorruptedWalObjectSnafu {
475 reason: format!(
476 "segment of region {region_id} holds entry ids {:?}..={:?}, footer expects {}..={}",
477 records.first().map(|record| record.entry_id),
478 records.last().map(|record| record.entry_id),
479 entry.min_entry_id,
480 entry.max_entry_id
481 ),
482 }
483 );
484 Ok(records)
485}
486
487pub(crate) fn encode_releases(releases: &BTreeMap<RegionId, u64>) -> Bytes {
491 let mut output = BytesMut::with_capacity(4 + releases.len() * 16);
492 output.put_u32(releases.len() as u32);
493 for (region_id, entry_id) in releases {
494 output.put_u64(region_id.as_u64());
495 output.put_u64(*entry_id);
496 }
497 output.freeze()
498}
499
500pub(crate) fn decode_releases(payload: &[u8]) -> Result<Vec<Release>> {
502 let mut reader = Reader::new(payload);
503 let count = reader.u32("release records")? as usize;
504 ensure!(
505 count > 0 && reader.remaining_len() == count * 16,
506 CorruptedWalObjectSnafu {
507 reason: format!(
508 "release records declare {count} records in {} bytes",
509 reader.remaining_len()
510 ),
511 }
512 );
513 let mut releases = Vec::<Release>::with_capacity(count);
514 for _ in 0..count {
515 let region_id = RegionId::from_u64(reader.u64("release records")?);
516 ensure!(
517 releases
518 .last()
519 .is_none_or(|last| last.region_id < region_id),
520 CorruptedWalObjectSnafu {
521 reason: format!("release records are not strictly ordered at region {region_id}"),
522 }
523 );
524 releases.push(Release {
525 region_id,
526 entry_id: reader.u64("release records")?,
527 });
528 }
529 Ok(releases)
530}
531
532pub(crate) const MIN_OBJECT_LEN: usize = HEADER_LEN + 4 + TRAILER_LEN;
535
536pub(crate) fn footer_range(trailer: FixedTrailer, object_len: usize) -> Result<Range<usize>> {
539 ensure!(
540 object_len >= MIN_OBJECT_LEN,
541 CorruptedWalObjectSnafu {
542 reason: truncated("object", object_len, MIN_OBJECT_LEN),
543 }
544 );
545 let trailer_start = object_len - TRAILER_LEN;
546 let footer_start = to_usize(trailer.footer_offset, "footer offset")?;
547 let footer_len = to_usize(trailer.footer_len, "footer length")?;
548 let footer_end =
549 footer_start
550 .checked_add(footer_len)
551 .with_context(|| CorruptedWalObjectSnafu {
552 reason: format!("footer range {footer_start}..{footer_len} overflows the object"),
553 })?;
554 ensure!(
555 footer_start >= HEADER_LEN && footer_end == trailer_start,
556 CorruptedWalObjectSnafu {
557 reason: format!(
558 "invalid footer range {footer_start}..{footer_end}, expected {HEADER_LEN}..{trailer_start}"
559 ),
560 }
561 );
562 Ok(footer_start..footer_end)
563}
564
565pub(crate) fn verify_segment_ranges(footer: &[FooterEntry], footer_start: usize) -> Result<()> {
569 let mut expected_offset = HEADER_LEN;
570 for entry in footer {
571 let start = to_usize(entry.segment_offset, "segment offset")?;
572 let len = to_usize(entry.segment_len, "segment length")?;
573 let end = start
574 .checked_add(len)
575 .with_context(|| CorruptedWalObjectSnafu {
576 reason: format!(
577 "segment range {start}..{len} of region {} overflows the object",
578 entry.region_id
579 ),
580 })?;
581 ensure!(
582 start == expected_offset && end <= footer_start,
583 CorruptedWalObjectSnafu {
584 reason: format!(
585 "invalid segment range {start}..{end} of region {}, expected {expected_offset}..{footer_start}",
586 entry.region_id
587 ),
588 }
589 );
590 expected_offset = end;
591 }
592 ensure!(
593 expected_offset == footer_start,
594 CorruptedWalObjectSnafu {
595 reason: format!(
596 "invalid segment range, segments end at {expected_offset}, footer starts at {footer_start}"
597 ),
598 }
599 );
600 Ok(())
601}
602
603#[cfg(test)]
607pub(crate) fn decode_object(bytes: &[u8]) -> Result<DecodedObject> {
608 ensure!(
609 bytes.len() >= MIN_OBJECT_LEN,
610 CorruptedWalObjectSnafu {
611 reason: truncated("object", bytes.len(), MIN_OBJECT_LEN),
612 }
613 );
614
615 let header = decode_header(bytes)?;
616 let trailer_start = bytes.len() - TRAILER_LEN;
617 let trailer = decode_trailer(&bytes[trailer_start..])?;
618 let footer_range = footer_range(trailer, bytes.len())?;
619 let footer_start = footer_range.start;
620
621 let footer = decode_footer(&bytes[footer_range], trailer)?;
622 verify_segment_ranges(&footer, footer_start)?;
623
624 let mut records = Vec::new();
625 for entry in &footer {
626 let start = entry.segment_offset as usize;
627 let end = start + entry.segment_len as usize;
628 records.extend(decode_segment(&bytes[start..end], entry)?);
629 }
630 let checksum = object_crc32(&bytes[..trailer_start], trailer);
631 ensure!(
632 checksum == trailer.object_crc32,
633 CorruptedWalObjectSnafu {
634 reason: checksum_mismatch("object", trailer.object_crc32, checksum),
635 }
636 );
637
638 Ok(DecodedObject {
639 header,
640 records,
641 footer,
642 })
643}
644
645fn encode_header(header: &Header, output: &mut BytesMut) {
646 let start = output.len();
647 output.put_slice(HEADER_MAGIC);
648 output.put_u16(FORMAT_VERSION);
649 output.put_u64(header.object_seq);
650 output.put_u64(header.epoch);
651 let prev = header.prev.unwrap_or(ChainLink {
652 object_seq: NO_PREDECESSOR,
653 epoch: 0,
654 });
655 output.put_u64(prev.object_seq);
656 output.put_u64(prev.epoch);
657 output.put_u64(header.reclaim_boundary);
658 output.put_u32(crc32fast::hash(&output[start..]));
659}
660
661fn encode_footer(directory: &[FooterEntry], output: &mut BytesMut) -> Result<()> {
662 output.put_u32(to_u32(directory.len(), "footer entry count")?);
663 for entry in directory {
664 output.put_u64(entry.region_id.as_u64());
665 output.put_u64(entry.min_entry_id);
666 output.put_u64(entry.max_entry_id);
667 output.put_u32(entry.entry_count);
668 output.put_u64(entry.segment_offset);
669 output.put_u64(entry.segment_len);
670 output.put_u32(entry.segment_crc32);
671 }
672 Ok(())
673}
674
675fn encode_trailer(trailer: FixedTrailer, output: &mut BytesMut) {
676 output.put_u64(trailer.footer_offset);
677 output.put_u64(trailer.footer_len);
678 output.put_u32(trailer.footer_crc32);
679 output.put_u32(trailer.object_crc32);
680 output.put_slice(TRAILER_MAGIC);
681}
682
683fn object_crc32(bytes_before_trailer: &[u8], trailer: FixedTrailer) -> u32 {
686 let mut checksum = crc32fast::Hasher::new();
687 checksum.update(bytes_before_trailer);
688 checksum.update(&trailer.footer_offset.to_be_bytes());
689 checksum.update(&trailer.footer_len.to_be_bytes());
690 checksum.update(&trailer.footer_crc32.to_be_bytes());
691 checksum.update(TRAILER_MAGIC);
692 checksum.finalize()
693}
694
695fn truncated(part: &str, actual: usize, expected: usize) -> String {
696 format!("truncated {part}, expected at least {expected} bytes, actual {actual}")
697}
698
699fn trailing_bytes(part: &str, actual: usize, expected: usize) -> String {
700 format!("trailing bytes in {part}, expected {expected} bytes, actual {actual}")
701}
702
703fn invalid_magic(part: &str, expected: &[u8], actual: &[u8]) -> String {
704 format!(
705 "invalid {part} magic, expected {:?}, actual {:?}",
706 String::from_utf8_lossy(expected),
707 String::from_utf8_lossy(actual)
708 )
709}
710
711fn checksum_mismatch(part: &str, expected: u32, actual: u32) -> String {
712 format!("{part} checksum mismatch, expected {expected:#010x}, actual {actual:#010x}")
713}
714
715fn to_u32(value: usize, name: &str) -> Result<u32> {
716 u32::try_from(value)
717 .ok()
718 .with_context(|| CorruptedWalObjectSnafu {
719 reason: format!("{name} {value} does not fit into u32"),
720 })
721}
722
723fn to_u64(value: usize, name: &str) -> Result<u64> {
724 u64::try_from(value)
725 .ok()
726 .with_context(|| CorruptedWalObjectSnafu {
727 reason: format!("{name} {value} does not fit into u64"),
728 })
729}
730
731fn to_usize(value: u64, name: &str) -> Result<usize> {
732 usize::try_from(value)
733 .ok()
734 .with_context(|| CorruptedWalObjectSnafu {
735 reason: format!("{name} {value} does not fit into usize"),
736 })
737}
738
739struct Reader<'a> {
740 bytes: &'a [u8],
741 offset: usize,
742}
743
744impl<'a> Reader<'a> {
745 fn new(bytes: &'a [u8]) -> Self {
746 Self { bytes, offset: 0 }
747 }
748
749 fn take(&mut self, len: usize, part: &'static str) -> Result<&'a [u8]> {
750 let end = self
751 .offset
752 .checked_add(len)
753 .with_context(|| CorruptedWalObjectSnafu {
754 reason: truncated(part, self.bytes.len(), len),
755 })?;
756 let value = self
757 .bytes
758 .get(self.offset..end)
759 .with_context(|| CorruptedWalObjectSnafu {
760 reason: truncated(part, self.bytes.len(), end),
761 })?;
762 self.offset = end;
763 Ok(value)
764 }
765
766 fn u16(&mut self, part: &'static str) -> Result<u16> {
767 Ok(u16::from_be_bytes(
768 self.take(2, part)?.try_into().expect("u16 has two bytes"),
769 ))
770 }
771
772 fn u32(&mut self, part: &'static str) -> Result<u32> {
773 Ok(u32::from_be_bytes(
774 self.take(4, part)?.try_into().expect("u32 has four bytes"),
775 ))
776 }
777
778 fn u64(&mut self, part: &'static str) -> Result<u64> {
779 Ok(u64::from_be_bytes(
780 self.take(8, part)?.try_into().expect("u64 has eight bytes"),
781 ))
782 }
783
784 fn is_empty(&self) -> bool {
785 self.offset == self.bytes.len()
786 }
787
788 fn remaining_len(&self) -> usize {
789 self.bytes.len() - self.offset
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use crate::error::Error;
797
798 fn header() -> Header {
799 Header {
800 object_seq: 42,
801 epoch: 5,
802 prev: Some(ChainLink {
803 object_seq: 40,
804 epoch: 4,
805 }),
806 reclaim_boundary: 37,
807 }
808 }
809
810 fn records() -> Vec<Record> {
811 vec![
812 Record {
813 region_id: RegionId::new(2, 1),
814 entry_id: 7,
815 payload: Bytes::from_static(b"region-2"),
816 },
817 Record {
818 region_id: RegionId::new(1, 1),
819 entry_id: 2,
820 payload: Bytes::from_static(b"second"),
821 },
822 Record {
823 region_id: RegionId::new(1, 1),
824 entry_id: 1,
825 payload: Bytes::from_static(b"first"),
826 },
827 ]
828 }
829
830 #[test]
831 fn test_format_round_trip_with_multiple_regions_and_entries() {
832 let encoded = encode_object(header(), &records()).unwrap();
833 let decoded = decode_object(&encoded.bytes).unwrap();
834
835 assert_eq!(header(), decoded.header);
836 assert_eq!(2, decoded.footer.len());
837 assert_eq!(RegionId::new(1, 1), decoded.footer[0].region_id);
838 assert_eq!((1, 2, 2), footer_ids(&decoded.footer[0]));
839 assert_eq!(RegionId::new(2, 1), decoded.footer[1].region_id);
840 assert_eq!((7, 7, 1), footer_ids(&decoded.footer[1]));
841 assert_eq!(
842 vec![
843 (RegionId::new(1, 1), 1, Bytes::from_static(b"first")),
844 (RegionId::new(1, 1), 2, Bytes::from_static(b"second")),
845 (RegionId::new(2, 1), 7, Bytes::from_static(b"region-2")),
846 ],
847 decoded
848 .records
849 .into_iter()
850 .map(|record| (record.region_id, record.entry_id, record.payload))
851 .collect::<Vec<_>>()
852 );
853 }
854
855 #[test]
856 fn test_format_decodes_footer_trailer_and_segment_ranges() {
857 let encoded = encode_object(header(), &records()).unwrap();
858 let trailer_start = encoded.bytes.len() - TRAILER_LEN;
859 let trailer = decode_trailer(&encoded.bytes[trailer_start..]).unwrap();
860 assert_eq!(encoded.trailer, trailer);
861
862 let footer_start = trailer.footer_offset as usize;
863 let footer_end = footer_start + trailer.footer_len as usize;
864 let footer = decode_footer(&encoded.bytes[footer_start..footer_end], trailer).unwrap();
865 assert_eq!(encoded.footer, footer);
866
867 let entry = &footer[0];
868 let start = entry.segment_offset as usize;
869 let end = start + entry.segment_len as usize;
870 let segment = decode_segment(&encoded.bytes[start..end], entry).unwrap();
871 assert_eq!(
872 vec![1, 2],
873 segment
874 .iter()
875 .map(|entry| entry.entry_id)
876 .collect::<Vec<_>>()
877 );
878 }
879
880 #[test]
881 fn test_format_rejects_truncation() {
882 let encoded = encode_object(header(), &records()).unwrap();
883 assert_corrupted(
884 decode_object(&encoded.bytes[..HEADER_LEN - 1]),
885 "truncated object",
886 );
887 assert_corrupted(
888 decode_trailer(&encoded.bytes[encoded.bytes.len() - TRAILER_LEN + 1..]),
889 "truncated trailer",
890 );
891
892 let entry = &encoded.footer[0];
893 let start = entry.segment_offset as usize;
894 let end = start + entry.segment_len as usize - 1;
895 assert_corrupted(
896 decode_segment(&encoded.bytes[start..end], entry),
897 "truncated segment",
898 );
899 }
900
901 #[test]
902 fn test_format_rejects_header_magic_and_version() {
903 let encoded = encode_object(header(), &records()).unwrap();
904
905 let mut bad_magic = encoded.bytes.to_vec();
906 bad_magic[0] ^= 1;
907 assert_corrupted(decode_object(&bad_magic), "invalid header magic");
908
909 let mut bad_version = encoded.bytes.to_vec();
910 bad_version[8..10].copy_from_slice(&(FORMAT_VERSION + 1).to_be_bytes());
911 assert_corrupted(decode_object(&bad_version), "unsupported format version 2");
912
913 for offset in [10, 18, 26, 34, 42, HEADER_LEN - 1] {
916 let mut bad_field = encoded.bytes.to_vec();
917 bad_field[offset] ^= 1;
918 assert_corrupted(decode_header(&bad_field), "header checksum mismatch");
919 }
920 }
921
922 #[test]
923 fn test_format_round_trips_the_first_object_of_a_chain() {
924 let first = Header {
925 prev: None,
926 ..header()
927 };
928 let encoded = encode_object(first.clone(), &records()).unwrap();
929 assert_eq!(first, decode_object(&encoded.bytes).unwrap().header);
930 }
931
932 #[test]
933 fn test_format_rejects_a_reclaim_boundary_above_the_object() {
934 let above = Header {
935 reclaim_boundary: header().object_seq + 1,
936 ..header()
937 };
938 let encoded = encode_object(above, &records()).unwrap();
939 assert_corrupted(
940 decode_header(&encoded.bytes),
941 "reclaim boundary 43 is above the object sequence 42",
942 );
943 }
944
945 #[test]
946 fn test_format_rejects_trailer_magic_and_invalid_footer_range() {
947 let encoded = encode_object(header(), &records()).unwrap();
948
949 let mut bad_magic = encoded.bytes.to_vec();
950 let last = bad_magic.len() - 1;
951 bad_magic[last] ^= 1;
952 assert_corrupted(decode_object(&bad_magic), "invalid trailer magic");
953
954 let mut bad_range = encoded.bytes.to_vec();
955 let trailer_start = bad_range.len() - TRAILER_LEN;
956 bad_range[trailer_start..trailer_start + 8].copy_from_slice(&u64::MAX.to_be_bytes());
957 assert_corrupted(decode_object(&bad_range), "footer");
958 }
959
960 #[test]
961 fn test_format_rejects_segment_and_footer_crc_errors() {
962 let encoded = encode_object(header(), &records()).unwrap();
963
964 let mut bad_segment = encoded.bytes.to_vec();
965 let segment = &encoded.footer[0];
966 bad_segment[segment.segment_offset as usize + SEGMENT_HEADER_LEN] ^= 1;
967 assert_corrupted(
968 decode_object(&bad_segment),
969 &format!("segment of region {} checksum mismatch", segment.region_id),
970 );
971
972 let mut bad_footer = encoded.bytes.to_vec();
973 bad_footer[encoded.trailer.footer_offset as usize] ^= 1;
974 assert_corrupted(decode_object(&bad_footer), "footer checksum mismatch");
975 }
976
977 #[test]
978 fn test_format_rejects_corrupted_trailer_checksums() {
979 let encoded = encode_object(header(), &records()).unwrap();
980 let trailer_start = encoded.bytes.len() - TRAILER_LEN;
981
982 let object_crc32_start = trailer_start + 8 + 8 + 4;
985 let mut bad_object_crc32 = encoded.bytes.to_vec();
986 bad_object_crc32[object_crc32_start] ^= 1;
987 assert_corrupted(decode_object(&bad_object_crc32), "object checksum mismatch");
988
989 let footer_crc32_start = trailer_start + 8 + 8;
990 let mut bad_footer_crc32 = encoded.bytes.to_vec();
991 bad_footer_crc32[footer_crc32_start] ^= 1;
992 assert_corrupted(decode_object(&bad_footer_crc32), "footer checksum mismatch");
993 }
994
995 #[test]
996 fn test_format_round_trips_an_object_without_records() {
997 let encoded = encode_object(header(), &[]).unwrap();
998 assert!(encoded.footer.is_empty());
999 assert_eq!(
1000 MIN_OBJECT_LEN,
1001 encoded.bytes.len(),
1002 "an empty object is a header, an empty footer and the trailer"
1003 );
1004 assert_eq!(encoded.bytes.len() as u64, object_len(&encoded.footer));
1005 let decoded = decode_object(&encoded.bytes).unwrap();
1006 assert_eq!(header(), decoded.header);
1007 assert!(decoded.records.is_empty());
1008 assert!(decoded.footer.is_empty());
1009 }
1010
1011 #[test]
1012 fn test_format_rejects_duplicate_entry_ids() {
1013 let region_id = RegionId::new(1, 1);
1014 let records = vec![
1015 Record {
1016 region_id,
1017 entry_id: 1,
1018 payload: Bytes::from_static(b"first"),
1019 },
1020 Record {
1021 region_id,
1022 entry_id: 1,
1023 payload: Bytes::from_static(b"duplicate"),
1024 },
1025 ];
1026 assert_corrupted(
1027 encode_object(header(), &records),
1028 &format!("non-monotonic entry id for region {region_id}"),
1029 );
1030 }
1031
1032 #[test]
1033 fn test_format_rejects_segment_count_larger_than_available_bytes() {
1034 let region_id = RegionId::new(1, 1);
1035 let mut segment = BytesMut::new();
1036 segment.put_u64(region_id.as_u64());
1037 segment.put_u32(u32::MAX);
1038 let entry = FooterEntry {
1039 region_id,
1040 min_entry_id: 1,
1041 max_entry_id: 1,
1042 entry_count: u32::MAX,
1043 segment_offset: HEADER_LEN as u64,
1044 segment_len: segment.len() as u64,
1045 segment_crc32: crc32fast::hash(&segment),
1046 };
1047
1048 assert_corrupted(
1049 decode_segment(&segment, &entry),
1050 "segment declares 4294967295 entries but only holds bytes for 0",
1051 );
1052 }
1053
1054 const FIXTURE_V1_HEX: &str = concat!(
1060 "475457414c4f424a",
1063 "0001",
1064 "0000000000000007",
1065 "0000000000000003",
1066 "0000000000000006",
1067 "0000000000000002",
1068 "0000000000000005",
1069 "118e773f",
1070 "0000000100000001",
1073 "00000003",
1074 "0000000000000001",
1075 "00000001",
1076 "61",
1077 "0000000000000002",
1078 "00000002",
1079 "6262",
1080 "0000000000000003",
1081 "00000003",
1082 "636363",
1083 "0000000200000001",
1085 "00000002",
1086 "000000000000000a",
1087 "00000001",
1088 "78",
1089 "000000000000000b",
1090 "00000002",
1091 "7979",
1092 "00000002",
1095 "0000000100000001",
1096 "0000000000000001",
1097 "0000000000000003",
1098 "00000003",
1099 "0000000000000036",
1100 "0000000000000036",
1101 "25ac0486",
1102 "0000000200000001",
1103 "000000000000000a",
1104 "000000000000000b",
1105 "00000002",
1106 "000000000000006c",
1107 "0000000000000027",
1108 "65dc08ec",
1109 "0000000000000093",
1111 "0000000000000064",
1112 "d80c9f07",
1113 "65c91839",
1114 "475457414c54524c",
1115 );
1116
1117 fn fixture_bytes() -> Vec<u8> {
1118 let hex = FIXTURE_V1_HEX.as_bytes();
1119 hex.chunks(2)
1120 .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())
1121 .collect()
1122 }
1123
1124 fn fixture_records() -> Vec<Record> {
1125 [
1126 (RegionId::new(1, 1), 1, &b"a"[..]),
1127 (RegionId::new(1, 1), 2, b"bb"),
1128 (RegionId::new(1, 1), 3, b"ccc"),
1129 (RegionId::new(2, 1), 10, b"x"),
1130 (RegionId::new(2, 1), 11, b"yy"),
1131 ]
1132 .into_iter()
1133 .map(|(region_id, entry_id, payload)| Record {
1134 region_id,
1135 entry_id,
1136 payload: Bytes::copy_from_slice(payload),
1137 })
1138 .collect()
1139 }
1140
1141 #[test]
1142 fn test_format_matches_version_1_fixture() {
1143 let fixture = fixture_bytes();
1144 let header = Header {
1145 object_seq: 7,
1146 epoch: 3,
1147 prev: Some(ChainLink {
1148 object_seq: 6,
1149 epoch: 2,
1150 }),
1151 reclaim_boundary: 5,
1152 };
1153 let footer = vec![
1154 FooterEntry {
1155 region_id: RegionId::new(1, 1),
1156 min_entry_id: 1,
1157 max_entry_id: 3,
1158 entry_count: 3,
1159 segment_offset: 54,
1160 segment_len: 54,
1161 segment_crc32: 0x25ac0486,
1162 },
1163 FooterEntry {
1164 region_id: RegionId::new(2, 1),
1165 min_entry_id: 10,
1166 max_entry_id: 11,
1167 entry_count: 2,
1168 segment_offset: 108,
1169 segment_len: 39,
1170 segment_crc32: 0x65dc08ec,
1171 },
1172 ];
1173 assert_eq!(279, fixture.len());
1174
1175 let decoded = decode_object(&fixture).unwrap();
1176 assert_eq!(header, decoded.header);
1177 assert_eq!(footer, decoded.footer);
1178 assert_eq!(fixture_records(), decoded.records);
1179 assert_eq!(
1180 FixedTrailer {
1181 footer_offset: 147,
1182 footer_len: 100,
1183 footer_crc32: 0xd80c9f07,
1184 object_crc32: 0x65c91839,
1185 },
1186 decode_trailer(&fixture[fixture.len() - TRAILER_LEN..]).unwrap()
1187 );
1188
1189 let mut reversed = fixture_records();
1191 reversed.reverse();
1192 for records in [fixture_records(), reversed] {
1193 let encoded = encode_object(header.clone(), &records).unwrap();
1194 assert_eq!(fixture, encoded.bytes.as_ref());
1195 assert_eq!(footer, encoded.footer);
1196 assert_eq!(fixture.len() as u64, object_len(&encoded.footer));
1197 }
1198 }
1199
1200 const FIXTURE_RELEASE: &[u8] =
1203 include_bytes!("../../../../tests/compatibility/wal/object_store/release_record.wal");
1204
1205 fn release_fixture_records() -> Vec<Record> {
1206 let releases = BTreeMap::from([
1207 (RegionId::new(2, 1), (2 << 20) | 1),
1208 (RegionId::new(3, 0), u64::MAX),
1209 ]);
1210 [
1211 (RegionId::new(1, 1), Bytes::from_static(b"a1")),
1212 (RELEASE_REGION_ID, encode_releases(&releases)),
1213 ]
1214 .into_iter()
1215 .map(|(region_id, payload)| Record {
1216 region_id,
1217 entry_id: (3 << 20) | 1,
1218 payload,
1219 })
1220 .collect()
1221 }
1222
1223 #[test]
1224 fn test_format_matches_release_record_fixture() {
1225 let header = Header {
1226 object_seq: 3,
1227 epoch: 2,
1228 prev: Some(ChainLink {
1229 object_seq: 2,
1230 epoch: 2,
1231 }),
1232 reclaim_boundary: 1,
1233 };
1234 let encoded = encode_object(header.clone(), &release_fixture_records()).unwrap();
1235 assert_eq!(FIXTURE_RELEASE, encoded.bytes.as_ref());
1236
1237 assert_eq!([0, 1], FIXTURE_RELEASE[8..10]);
1240 let decoded = decode_object(FIXTURE_RELEASE).unwrap();
1241 assert_eq!(header, decoded.header);
1242 assert_eq!(release_fixture_records(), decoded.records);
1243 assert_eq!(
1244 vec![
1245 Release {
1246 region_id: RegionId::new(2, 1),
1247 entry_id: (2 << 20) | 1,
1248 },
1249 Release {
1250 region_id: RegionId::new(3, 0),
1251 entry_id: u64::MAX,
1252 },
1253 ],
1254 decode_releases(&decoded.records[1].payload).unwrap()
1255 );
1256 }
1257
1258 #[test]
1259 fn test_format_rejects_invalid_release_records() {
1260 let one = BTreeMap::from([(RegionId::new(1, 1), 1)]);
1261 let encoded = encode_releases(&one);
1262 for (payload, reason) in [
1263 (&[0, 0, 0, 0][..], "declare 0 records in 0 bytes"),
1264 (
1265 &encoded[..encoded.len() - 1],
1266 "declare 1 records in 15 bytes",
1267 ),
1268 ] {
1269 assert_corrupted(decode_releases(payload), reason);
1270 }
1271 let mut unordered = encode_releases(&BTreeMap::from([
1272 (RegionId::new(1, 1), 1),
1273 (RegionId::new(1, 2), 1),
1274 ]))
1275 .to_vec();
1276 unordered[4..12].copy_from_slice(&RegionId::new(1, 2).as_u64().to_be_bytes());
1277 assert_corrupted(decode_releases(&unordered), "not strictly ordered");
1278 }
1279
1280 fn footer_ids(entry: &FooterEntry) -> (u64, u64, u32) {
1281 (entry.min_entry_id, entry.max_entry_id, entry.entry_count)
1282 }
1283
1284 fn assert_corrupted<T: std::fmt::Debug>(result: Result<T>, expected_reason: &str) {
1285 match result {
1286 Err(Error::CorruptedWalObject { reason, .. }) => assert!(
1287 reason.contains(expected_reason),
1288 "expected reason to contain {expected_reason:?}, actual {reason:?}"
1289 ),
1290 other => panic!("expected a corrupted object error, actual {other:?}"),
1291 }
1292 }
1293}