krabka_remote_storage/diskless/
object.rs1use {
4 bytes::{BufMut, Bytes, BytesMut},
5 uuid::Uuid,
6};
7
8const MAGIC: [u8; 4] = *b"CKWL";
9const OBJECT_VERSION: u16 = 1;
10const HEADER_LEN: usize = 6;
11const TRAILER_LEN: usize = 8;
12const OBJECT_ENTRY_LEN: usize = 48;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct WalObjectEntry {
17 pub topic_id: Uuid,
18 pub partition: i32,
19 pub first_offset: i64,
20 pub last_offset: i64,
21 pub byte_start: u64,
22 pub byte_len: u32,
23}
24
25#[derive(Debug, thiserror::Error)]
27pub enum WalObjectError {
28 #[error("wal object too short")]
30 TooShort,
31 #[error("bad wal object magic")]
33 BadMagic,
34 #[error("unsupported wal object version {0}")]
36 BadVersion(u16),
37 #[error("corrupt wal object manifest")]
39 BadManifest,
40 #[error("wal object exceeds the CKWL 4 GiB length limit")]
42 TooLarge,
43}
44
45#[derive(Default)]
47pub struct WalObjectBuilder {
48 body: BytesMut,
49 entries: Vec<WalObjectEntry>,
50}
51
52impl WalObjectBuilder {
53 #[must_use]
54 pub fn new() -> Self { Self::default() }
55
56 #[must_use]
57 pub fn body_len(&self) -> usize { self.body.len() }
58
59 #[must_use]
60 pub fn is_empty(&self) -> bool { self.entries.is_empty() }
61
62 pub fn append_run(
68 &mut self,
69 topic_id: Uuid,
70 partition: i32,
71 first_offset: i64,
72 last_offset: i64,
73 run: &[u8],
74 ) -> Result<(), WalObjectError> {
75 let byte_start =
76 u64::try_from(HEADER_LEN + self.body.len()).map_err(|_| WalObjectError::TooLarge)?;
77 let byte_len = u32::try_from(run.len()).map_err(|_| WalObjectError::TooLarge)?;
78 self.body.extend_from_slice(run);
79 self.entries.push(WalObjectEntry {
80 topic_id,
81 partition,
82 first_offset,
83 last_offset,
84 byte_start,
85 byte_len,
86 });
87 Ok(())
88 }
89
90 pub fn finish(self) -> Result<Bytes, WalObjectError> {
96 let footer_len = self
97 .entries
98 .len()
99 .checked_mul(OBJECT_ENTRY_LEN)
100 .and_then(|len| u32::try_from(len).ok())
101 .ok_or(WalObjectError::TooLarge)?;
102 let mut out = BytesMut::with_capacity(
103 HEADER_LEN + self.body.len() + self.entries.len() * OBJECT_ENTRY_LEN + TRAILER_LEN,
104 );
105 out.extend_from_slice(&MAGIC);
106 out.put_u16_le(OBJECT_VERSION);
107 out.extend_from_slice(&self.body);
108 for entry in self.entries {
109 out.extend_from_slice(entry.topic_id.as_bytes());
110 out.put_i32_le(entry.partition);
111 out.put_i64_le(entry.first_offset);
112 out.put_i64_le(entry.last_offset);
113 out.put_u64_le(entry.byte_start);
114 out.put_u32_le(entry.byte_len);
115 }
116 out.put_u32_le(footer_len);
117 out.extend_from_slice(&MAGIC);
118 Ok(out.freeze())
119 }
120
121 pub fn finish_with_run(
127 mut self,
128 topic_id: Uuid,
129 partition: i32,
130 first_offset: i64,
131 last_offset: i64,
132 run: &[u8],
133 ) -> Result<Bytes, WalObjectError> {
134 self.append_run(topic_id, partition, first_offset, last_offset, run)?;
135 self.finish()
136 }
137}
138
139pub fn parse_wal_object(object: &Bytes) -> Result<Vec<WalObjectEntry>, WalObjectError> {
144 if object.len() < HEADER_LEN + TRAILER_LEN {
145 return Err(WalObjectError::TooShort);
146 }
147 let (Some(&[m0, m1, m2, m3, v0, v1]), Some(&[l0, l1, l2, l3, t0, t1, t2, t3])) = (
148 object.first_chunk::<HEADER_LEN>(),
149 object.last_chunk::<TRAILER_LEN>(),
150 ) else {
151 return Err(WalObjectError::TooShort);
152 };
153 if [m0, m1, m2, m3] != MAGIC || [t0, t1, t2, t3] != MAGIC {
154 return Err(WalObjectError::BadMagic);
155 }
156 let version = u16::from_le_bytes([v0, v1]);
157 if version != OBJECT_VERSION {
158 return Err(WalObjectError::BadVersion(version));
159 }
160 let trailer = object.len() - TRAILER_LEN;
161 let footer_len = usize::try_from(u32::from_le_bytes([l0, l1, l2, l3]))
162 .map_err(|_| WalObjectError::BadManifest)?;
163 if footer_len % OBJECT_ENTRY_LEN != 0 {
164 return Err(WalObjectError::BadManifest);
165 }
166 let footer = trailer
167 .checked_sub(footer_len)
168 .filter(|start| *start >= HEADER_LEN)
169 .ok_or(WalObjectError::BadManifest)?;
170 let mut entries = Vec::with_capacity(footer_len / OBJECT_ENTRY_LEN);
171 let footer_bytes = object
172 .get(footer..trailer)
173 .ok_or(WalObjectError::BadManifest)?;
174 for raw in footer_bytes.as_chunks::<OBJECT_ENTRY_LEN>().0 {
175 let entry = WalObjectEntry {
176 topic_id: Uuid::from_slice(&raw[..16]).map_err(|_| WalObjectError::BadManifest)?,
177 partition: i32::from_le_bytes(
178 raw[16..20]
179 .try_into()
180 .map_err(|_| WalObjectError::BadManifest)?,
181 ),
182 first_offset: i64::from_le_bytes(
183 raw[20..28]
184 .try_into()
185 .map_err(|_| WalObjectError::BadManifest)?,
186 ),
187 last_offset: i64::from_le_bytes(
188 raw[28..36]
189 .try_into()
190 .map_err(|_| WalObjectError::BadManifest)?,
191 ),
192 byte_start: u64::from_le_bytes(
193 raw[36..44]
194 .try_into()
195 .map_err(|_| WalObjectError::BadManifest)?,
196 ),
197 byte_len: u32::from_le_bytes(
198 raw[44..48]
199 .try_into()
200 .map_err(|_| WalObjectError::BadManifest)?,
201 ),
202 };
203 let start = usize::try_from(entry.byte_start).map_err(|_| WalObjectError::BadManifest)?;
204 let end = start
205 .checked_add(usize::try_from(entry.byte_len).map_err(|_| WalObjectError::BadManifest)?)
206 .filter(|end| *end <= footer)
207 .ok_or(WalObjectError::BadManifest)?;
208 let _ = end;
209 if start < HEADER_LEN {
210 return Err(WalObjectError::BadManifest);
211 }
212 entries.push(entry);
213 }
214 Ok(entries)
215}