Skip to main content

log_store/object_store_wal/
format.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Encoding and decoding of a single WAL object. The byte layout is described
16//! in the [module documentation](super).
17
18use 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;
30/// Predecessor sequence recorded for an object that starts a chain. Object
31/// sequences stay below `OBJECT_SEQ_LIMIT`, so it never names an object.
32const NO_PREDECESSOR: u64 = u64::MAX;
33
34/// Length of the object header: magic, version, object sequence, epoch,
35/// predecessor sequence, predecessor epoch, reclaim boundary and the CRC32 of
36/// the header.
37pub(crate) const HEADER_LEN: usize = 8 + 2 + 8 + 8 + 8 + 8 + 8 + 4;
38/// Length of the fixed trailer: footer offset, footer length, footer CRC32,
39/// object CRC32 and magic.
40pub(crate) const TRAILER_LEN: usize = 8 + 8 + 4 + 4 + 8;
41const SEGMENT_HEADER_LEN: usize = 8 + 4;
42/// Length of one footer entry: region id, entry id range, entry count, segment
43/// offset, segment length and segment CRC32.
44pub(crate) const FOOTER_ENTRY_LEN: usize = 8 + 8 + 8 + 4 + 8 + 8 + 4;
45/// Length of the entry count the footer starts with.
46const FOOTER_COUNT_LEN: usize = 4;
47
48/// Header of a WAL object.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub(crate) struct Header {
51    pub(crate) object_seq: u64,
52    /// Epoch of the store instance that wrote the object: one above the
53    /// sequence of the start object the instance created, so the epoch also
54    /// identifies the instance.
55    pub(crate) epoch: u64,
56    /// The object this object extends, `None` for the first object of a chain.
57    pub(crate) prev: Option<ChainLink>,
58    /// Every object below this sequence was fully flushed when this object
59    /// was sealed, so the chain recovery replays need not reach below it. It
60    /// is at or below the sequence of the object.
61    pub(crate) reclaim_boundary: u64,
62}
63
64/// Names the object another object extends: its sequence and the epoch
65/// recorded in its header.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub(crate) struct ChainLink {
68    pub(crate) object_seq: u64,
69    pub(crate) epoch: u64,
70}
71
72/// A single WAL entry inside an object.
73#[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
80/// The region whose segment holds the release records of an object, as a
81/// single entry whose payload [`encode_releases`] writes. It is an ordinary
82/// segment, so a reader that predates release records reads the object as
83/// any other; no region is ever assigned this id.
84pub(crate) const RELEASE_REGION_ID: RegionId = RegionId::from_u64(u64::MAX);
85
86/// A release record: no entry of the region with an id at or below
87/// `entry_id` is needed any more, `u64::MAX` releasing the region in full.
88#[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/// Footer entry describing the segment of one region.
95#[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/// Fixed-size trailer locating the footer of an object.
107#[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/// An encoded object together with the metadata a writer indexes it by.
116#[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/// A decoded object with its records ordered by region id and entry id.
124#[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
132/// Encodes `records` into one object. Records are grouped into a segment per
133/// region; entry ids within a region must be unique. An object without records
134/// only extends the chain.
135pub(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
204/// Returns the length of the object whose footer is `footer`, derived from the
205/// layout: the segments tile the body after the header, and the footer, which
206/// holds an entry count and one entry per segment, and the trailer follow them.
207pub(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
213/// Decodes and verifies the header from the first [`HEADER_LEN`] bytes of an
214/// object.
215pub(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
273/// Decodes the trailer from the last [`TRAILER_LEN`] bytes of an object.
274pub(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
305/// Decodes the footer that `trailer` points at.
306pub(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
395/// Decodes the segment that `entry` describes.
396pub(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    // Every record needs at least an entry id and a payload length. Bound the
435    // allocation by bytes that are actually present before trusting `count`.
436    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
487/// Encodes release records, ordered by region and at most one per region, as
488/// the payload of the entry of [`RELEASE_REGION_ID`]: their count, then the
489/// region id and the entry id of each.
490pub(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
500/// Decodes the payload [`encode_releases`] writes.
501pub(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
532/// Lower bound on the length of an object: header, a footer holding only its
533/// entry count and the trailer.
534pub(crate) const MIN_OBJECT_LEN: usize = HEADER_LEN + 4 + TRAILER_LEN;
535
536/// Locates the footer inside an object of `object_len` bytes from its trailer.
537/// The footer must follow the header and end where the trailer starts.
538pub(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
565/// Checks that the segments `footer` describes tile the object body: the first
566/// starts right after the header, each follows the previous one without a gap
567/// or overlap, and the last ends where the footer at `footer_start` begins.
568pub(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/// Decodes a whole object, verifying every checksum and byte range. Recovery
604/// reads only the header, trailer and footer, so this is the reference
605/// decoder that tests check the store against.
606#[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
683/// Checksum of everything but the object checksum itself, so the trailer can
684/// carry it.
685fn 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        // Every field after the version is covered by the header checksum,
914        // which recovery verifies without reading the whole object.
915        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        // The object checksum the trailer stores, at footer offset, footer length
983        // and footer checksum.
984        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    /// A version 1 object holding entries 1, 2 and 3 of region 1 and entries
1055    /// 10 and 11 of region 2. The bytes were derived from the layout described in
1056    /// the module documentation, independently of [`encode_object`], so a change
1057    /// to field order, endianness or checksum coverage fails this test even when
1058    /// the encoder and the decoder change together.
1059    const FIXTURE_V1_HEX: &str = concat!(
1060        // Header: magic, version 1, object sequence 7, epoch 3, predecessor
1061        // sequence 6, predecessor epoch 2, reclaim boundary 5, header CRC32.
1062        "475457414c4f424a",
1063        "0001",
1064        "0000000000000007",
1065        "0000000000000003",
1066        "0000000000000006",
1067        "0000000000000002",
1068        "0000000000000005",
1069        "118e773f",
1070        // Segment of region 1 at offset 54: region id, entry count, then
1071        // (entry id, payload length, payload) per entry.
1072        "0000000100000001",
1073        "00000003",
1074        "0000000000000001",
1075        "00000001",
1076        "61",
1077        "0000000000000002",
1078        "00000002",
1079        "6262",
1080        "0000000000000003",
1081        "00000003",
1082        "636363",
1083        // Segment of region 2 at offset 108.
1084        "0000000200000001",
1085        "00000002",
1086        "000000000000000a",
1087        "00000001",
1088        "78",
1089        "000000000000000b",
1090        "00000002",
1091        "7979",
1092        // Footer at offset 147: entry count, then per segment region id, min and
1093        // max entry id, entry count, segment offset, length and CRC32.
1094        "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        // Trailer: footer offset, footer length, footer CRC32, object CRC32, magic.
1110        "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        // The encoding does not depend on the order records are passed in.
1190        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    /// An object with an entry of region (1, 1) and the release records of
1201    /// regions (2, 1) and (3, 0), frozen under `tests/compatibility`.
1202    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        // The object is a version 1 object whose last segment holds the
1238        // release records, so a reader that predates them decodes it.
1239        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}