Skip to main content

zeppelin_embed/wal/
header.rs

1//! Shared-file-header encoding for append-only WAL files.
2
3use crate::format::frame::{
4    FILE_HEADER_LEN, FILE_MAGIC, FileHeader, FormatCheck, FormatError,
5    decode_header as decode_frame_header, encode_header as encode_frame_header, read_u16, read_u64,
6};
7use crate::format::{FormatFamily, FormatRegistry, RegistryError};
8
9use super::LogSeq;
10
11/// Complete v1 WAL header: shared header plus the family-owned sequence field.
12pub const WAL_HEADER_LEN: usize = FILE_HEADER_LEN + 8;
13
14/// Validated WAL-specific interpretation of the shared file header.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct WalHeader {
17    /// Sequence required from the first record in this WAL file.
18    pub first_seq: LogSeq,
19}
20
21/// Specific failure found before WAL record replay begins.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum WalHeaderError {
24    /// No file header bytes were present.
25    Missing,
26    /// Some, but not all, complete WAL header bytes were present.
27    Truncated {
28        /// Required complete WAL header width.
29        needed: usize,
30        /// Bytes available in the file.
31        available: usize,
32    },
33    /// The permanent eight-byte magic did not match.
34    WrongMagic {
35        /// Required repository file magic.
36        expected: [u8; 8],
37        /// Eight bytes found at the magic position.
38        actual: [u8; 8],
39    },
40    /// The file declares a persisted family other than WAL.
41    WrongFamily {
42        /// Permanent WAL family identifier.
43        expected: u16,
44        /// Family identifier found in the file.
45        actual: u16,
46    },
47    /// The WAL version is outside the registry's accepted range.
48    UnsupportedVersion {
49        /// WAL family identifier.
50        family: u16,
51        /// Version found in the file.
52        version: u16,
53        /// Lowest accepted WAL version.
54        minimum: u16,
55        /// Highest accepted WAL version.
56        maximum: u16,
57    },
58    /// The shared header width is not exactly the v1 WAL header width.
59    InvalidHeaderLength {
60        /// Required v1 WAL header width.
61        expected: u64,
62        /// Header width found in the file.
63        actual: u64,
64    },
65    /// The append-only WAL declared a non-zero immutable file length.
66    NonZeroFileLength {
67        /// Reserved value found in the shared file-length slot.
68        actual: u64,
69    },
70    /// The shared decoder rejected a check that WAL-specific mapping did not expect.
71    InvalidSharedHeader {
72        /// Exact shared-format check that failed.
73        check: FormatCheck,
74    },
75}
76
77impl std::fmt::Display for WalHeaderError {
78    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            Self::Missing => formatter.write_str("WAL file header is missing"),
81            Self::Truncated { needed, available } => {
82                write!(
83                    formatter,
84                    "WAL file header needs {needed} bytes, got {available}"
85                )
86            }
87            Self::WrongMagic { expected, actual } => {
88                write!(formatter, "WAL magic expected {expected:?}, got {actual:?}")
89            }
90            Self::WrongFamily { expected, actual } => {
91                write!(formatter, "WAL family expected {expected}, got {actual}")
92            }
93            Self::UnsupportedVersion {
94                family,
95                version,
96                minimum,
97                maximum,
98            } => write!(
99                formatter,
100                "WAL family {family} version {version} is outside accepted range {minimum}..={maximum}"
101            ),
102            Self::InvalidHeaderLength { expected, actual } => {
103                write!(
104                    formatter,
105                    "WAL header length expected {expected}, got {actual}"
106                )
107            }
108            Self::NonZeroFileLength { actual } => {
109                write!(formatter, "WAL file length must be zero, got {actual}")
110            }
111            Self::InvalidSharedHeader { check } => {
112                write!(formatter, "WAL shared header failed {check:?}")
113            }
114        }
115    }
116}
117
118impl std::error::Error for WalHeaderError {}
119
120/// Encodes the complete WAL file header for a log beginning at `first_seq`.
121pub fn encode_header(first_seq: LogSeq) -> Result<Vec<u8>, RegistryError> {
122    let family = FormatFamily::Wal;
123    let spec = FormatRegistry::families()
124        .iter()
125        .find(|candidate| candidate.family == family)
126        .ok_or(RegistryError::UnknownFamily(family.id()))?;
127    let mut encoded = encode_frame_header(FileHeader {
128        magic: FILE_MAGIC,
129        family: family.id(),
130        version: spec.current_version,
131        flags: 0,
132        header_length: WAL_HEADER_LEN as u64,
133        file_length: 0,
134    });
135    encoded.extend_from_slice(&first_seq.get().to_le_bytes());
136    Ok(encoded)
137}
138
139/// Decodes and validates a WAL file header before any record is consumed.
140pub fn decode_header(bytes: &[u8]) -> Result<WalHeader, WalHeaderError> {
141    if bytes.is_empty() {
142        return Err(WalHeaderError::Missing);
143    }
144    if bytes.len() < WAL_HEADER_LEN {
145        return Err(WalHeaderError::Truncated {
146            needed: WAL_HEADER_LEN,
147            available: bytes.len(),
148        });
149    }
150    let actual_magic = bytes
151        .get(..8)
152        .and_then(|value| <[u8; 8]>::try_from(value).ok())
153        .ok_or(WalHeaderError::Truncated {
154            needed: WAL_HEADER_LEN,
155            available: bytes.len(),
156        })?;
157    let family = read_u16("WAL", bytes, 8).map_err(invalid_shared_header)?;
158    let version = read_u16("WAL", bytes, 10).map_err(invalid_shared_header)?;
159    let header_length = read_u64("WAL", bytes, 16).map_err(invalid_shared_header)?;
160    let common =
161        decode_frame_header("WAL", FormatFamily::Wal, bytes).map_err(|error| {
162            match error.check() {
163                FormatCheck::Magic => WalHeaderError::WrongMagic {
164                    expected: FILE_MAGIC,
165                    actual: actual_magic,
166                },
167                FormatCheck::Family => WalHeaderError::WrongFamily {
168                    expected: FormatFamily::Wal.id(),
169                    actual: family,
170                },
171                FormatCheck::Version => match FormatRegistry::require(family, version) {
172                    Err(RegistryError::UnsupportedVersion {
173                        family,
174                        version,
175                        minimum,
176                        maximum,
177                    }) => WalHeaderError::UnsupportedVersion {
178                        family,
179                        version,
180                        minimum,
181                        maximum,
182                    },
183                    Ok(_) | Err(_) => WalHeaderError::InvalidSharedHeader {
184                        check: FormatCheck::Version,
185                    },
186                },
187                FormatCheck::HeaderLength => WalHeaderError::InvalidHeaderLength {
188                    expected: WAL_HEADER_LEN as u64,
189                    actual: header_length,
190                },
191                check => WalHeaderError::InvalidSharedHeader { check },
192            }
193        })?;
194    if common.header_length != WAL_HEADER_LEN as u64 {
195        return Err(WalHeaderError::InvalidHeaderLength {
196            expected: WAL_HEADER_LEN as u64,
197            actual: common.header_length,
198        });
199    }
200    if common.file_length != 0 {
201        return Err(WalHeaderError::NonZeroFileLength {
202            actual: common.file_length,
203        });
204    }
205    let first_seq = read_u64("WAL", bytes, FILE_HEADER_LEN).map_err(invalid_shared_header)?;
206    Ok(WalHeader {
207        first_seq: LogSeq::new(first_seq),
208    })
209}
210
211fn invalid_shared_header(error: FormatError) -> WalHeaderError {
212    WalHeaderError::InvalidSharedHeader {
213        check: error.check(),
214    }
215}