Skip to main content

reddb_file/
append_only_segment.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{RdbFileError, RdbFileResult};
4
5pub const APPEND_ONLY_SEGMENT_VERSION: u32 = 1;
6pub const APPEND_ONLY_SEGMENT_CHUNK_BYTES: u32 = 512 * 1024;
7
8const MAGIC: &[u8; 8] = b"RDBAOS01";
9const FIXED_HEADER_LEN: usize = 8 + 4 + 8 + 4 + 4;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum AppendOnlySegmentCodec {
14    #[default]
15    Zstd,
16    None,
17}
18
19impl AppendOnlySegmentCodec {
20    pub fn as_str(self) -> &'static str {
21        match self {
22            Self::Zstd => "zstd",
23            Self::None => "none",
24        }
25    }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct AppendOnlySegmentRow {
30    pub primary_key: Vec<u8>,
31    pub payload: Vec<u8>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct AppendOnlySegmentChunkChecksum {
36    pub offset: u64,
37    pub len: u32,
38    pub checksum: String,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct AppendOnlySegmentBloom {
43    pub num_hashes: u8,
44    pub bit_size: u32,
45    pub inserted: u32,
46    pub bits_hex: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct AppendOnlySegment {
51    pub version: u32,
52    pub codec: AppendOnlySegmentCodec,
53    pub chunk_size: u32,
54    pub rows: Vec<AppendOnlySegmentRow>,
55    pub primary_min: Option<Vec<u8>>,
56    pub primary_max: Option<Vec<u8>>,
57    pub primary_bloom: Option<AppendOnlySegmentBloom>,
58    pub chunk_checksums: Vec<AppendOnlySegmentChunkChecksum>,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
62struct SegmentHeader {
63    version: u32,
64    codec: AppendOnlySegmentCodec,
65    chunk_size: u32,
66    row_count: u64,
67    uncompressed_len: u64,
68    primary_min_hex: Option<String>,
69    primary_max_hex: Option<String>,
70    primary_bloom: Option<AppendOnlySegmentBloom>,
71    chunk_checksums: Vec<AppendOnlySegmentChunkChecksum>,
72}
73
74#[derive(Debug, Serialize, Deserialize)]
75struct RowJson {
76    primary_key_hex: String,
77    payload_hex: String,
78}
79
80pub fn encode_append_only_segment(
81    codec: AppendOnlySegmentCodec,
82    rows: &[AppendOnlySegmentRow],
83) -> RdbFileResult<Vec<u8>> {
84    let row_json = rows
85        .iter()
86        .map(|row| RowJson {
87            primary_key_hex: hex::encode(&row.primary_key),
88            payload_hex: hex::encode(&row.payload),
89        })
90        .collect::<Vec<_>>();
91    let plain = serde_json::to_vec(&row_json).map_err(invalid_operation)?;
92    let body = match codec {
93        AppendOnlySegmentCodec::Zstd => {
94            zstd::bulk::compress(&plain, 3).map_err(invalid_operation)?
95        }
96        AppendOnlySegmentCodec::None => plain.clone(),
97    };
98    let chunk_checksums = chunk_checksums(&body);
99    let header = SegmentHeader {
100        version: APPEND_ONLY_SEGMENT_VERSION,
101        codec,
102        chunk_size: APPEND_ONLY_SEGMENT_CHUNK_BYTES,
103        row_count: rows.len() as u64,
104        uncompressed_len: plain.len() as u64,
105        primary_min_hex: rows
106            .iter()
107            .map(|row| row.primary_key.as_slice())
108            .min()
109            .map(hex::encode),
110        primary_max_hex: rows
111            .iter()
112            .map(|row| row.primary_key.as_slice())
113            .max()
114            .map(hex::encode),
115        primary_bloom: build_primary_bloom(rows),
116        chunk_checksums,
117    };
118    let header_bytes = serde_json::to_vec(&header).map_err(invalid_operation)?;
119    let header_len = u32::try_from(header_bytes.len()).map_err(|_| {
120        RdbFileError::InvalidOperation("append-only segment header too large".into())
121    })?;
122    let body_len = u64::try_from(body.len())
123        .map_err(|_| RdbFileError::InvalidOperation("append-only segment body too large".into()))?;
124
125    let mut out = Vec::with_capacity(FIXED_HEADER_LEN + header_bytes.len() + body.len());
126    out.extend_from_slice(MAGIC);
127    out.extend_from_slice(&header_len.to_le_bytes());
128    out.extend_from_slice(&body_len.to_le_bytes());
129    out.extend_from_slice(&crc32(&header_bytes).to_le_bytes());
130    out.extend_from_slice(&crc32(&body).to_le_bytes());
131    out.extend_from_slice(&header_bytes);
132    out.extend_from_slice(&body);
133    Ok(out)
134}
135
136pub fn decode_append_only_segment(bytes: &[u8]) -> RdbFileResult<AppendOnlySegment> {
137    if bytes.len() < FIXED_HEADER_LEN {
138        return Err(RdbFileError::InvalidOperation(
139            "append-only segment frame too short".into(),
140        ));
141    }
142    if &bytes[..8] != MAGIC {
143        return Err(RdbFileError::InvalidOperation(
144            "invalid append-only segment magic".into(),
145        ));
146    }
147    let header_len = read_u32(bytes, 8)? as usize;
148    let body_len = read_u64(bytes, 12)? as usize;
149    let header_crc = read_u32(bytes, 20)?;
150    let body_crc = read_u32(bytes, 24)?;
151    let header_start = FIXED_HEADER_LEN;
152    let body_start = header_start.checked_add(header_len).ok_or_else(|| {
153        RdbFileError::InvalidOperation("append-only segment size overflow".into())
154    })?;
155    let end = body_start.checked_add(body_len).ok_or_else(|| {
156        RdbFileError::InvalidOperation("append-only segment size overflow".into())
157    })?;
158    if bytes.len() != end {
159        return Err(RdbFileError::InvalidOperation(
160            "append-only segment frame length mismatch".into(),
161        ));
162    }
163    let header_bytes = &bytes[header_start..body_start];
164    let body = &bytes[body_start..end];
165    if crc32(header_bytes) != header_crc {
166        return Err(RdbFileError::InvalidOperation(
167            "append-only segment header checksum mismatch".into(),
168        ));
169    }
170    if crc32(body) != body_crc {
171        return Err(RdbFileError::InvalidOperation(
172            "append-only segment body checksum mismatch".into(),
173        ));
174    }
175    let header: SegmentHeader = serde_json::from_slice(header_bytes).map_err(invalid_operation)?;
176    if header.version != APPEND_ONLY_SEGMENT_VERSION {
177        return Err(RdbFileError::InvalidOperation(format!(
178            "unsupported append-only segment version: {}",
179            header.version
180        )));
181    }
182    if header.chunk_size != APPEND_ONLY_SEGMENT_CHUNK_BYTES {
183        return Err(RdbFileError::InvalidOperation(
184            "append-only segment chunk size mismatch".into(),
185        ));
186    }
187    if header.chunk_checksums != chunk_checksums(body) {
188        return Err(RdbFileError::InvalidOperation(
189            "append-only segment chunk checksum mismatch".into(),
190        ));
191    }
192
193    let plain = match header.codec {
194        AppendOnlySegmentCodec::Zstd => {
195            let max_len = usize::try_from(header.uncompressed_len).map_err(|_| {
196                RdbFileError::InvalidOperation("append-only segment plain length too large".into())
197            })?;
198            zstd::bulk::decompress(body, max_len).map_err(invalid_operation)?
199        }
200        AppendOnlySegmentCodec::None => body.to_vec(),
201    };
202    if plain.len() as u64 != header.uncompressed_len {
203        return Err(RdbFileError::InvalidOperation(
204            "append-only segment plain length mismatch".into(),
205        ));
206    }
207    let row_json: Vec<RowJson> = serde_json::from_slice(&plain).map_err(invalid_operation)?;
208    if row_json.len() as u64 != header.row_count {
209        return Err(RdbFileError::InvalidOperation(
210            "append-only segment row count mismatch".into(),
211        ));
212    }
213    let rows = row_json
214        .into_iter()
215        .map(|row| {
216            Ok(AppendOnlySegmentRow {
217                primary_key: hex::decode(row.primary_key_hex).map_err(invalid_operation)?,
218                payload: hex::decode(row.payload_hex).map_err(invalid_operation)?,
219            })
220        })
221        .collect::<RdbFileResult<Vec<_>>>()?;
222    let primary_min = decode_optional_hex(header.primary_min_hex)?;
223    let primary_max = decode_optional_hex(header.primary_max_hex)?;
224    Ok(AppendOnlySegment {
225        version: header.version,
226        codec: header.codec,
227        chunk_size: header.chunk_size,
228        rows,
229        primary_min,
230        primary_max,
231        primary_bloom: header.primary_bloom,
232        chunk_checksums: header.chunk_checksums,
233    })
234}
235
236pub fn append_only_segment_chunk_checksums(bytes: &[u8]) -> Vec<AppendOnlySegmentChunkChecksum> {
237    chunk_checksums(bytes)
238}
239
240pub fn append_only_segment_primary_bloom_might_contain(
241    bloom: &AppendOnlySegmentBloom,
242    key: &[u8],
243) -> bool {
244    if bloom.bit_size == 0 || bloom.num_hashes == 0 {
245        return true;
246    }
247    let Ok(bits) = hex::decode(&bloom.bits_hex) else {
248        return true;
249    };
250    for idx in primary_bloom_indexes(key, bloom.bit_size, bloom.num_hashes) {
251        let byte = (idx / 8) as usize;
252        let mask = 1u8 << (idx % 8);
253        if bits
254            .get(byte)
255            .map(|value| value & mask == 0)
256            .unwrap_or(true)
257        {
258            return false;
259        }
260    }
261    true
262}
263
264fn build_primary_bloom(rows: &[AppendOnlySegmentRow]) -> Option<AppendOnlySegmentBloom> {
265    if rows.is_empty() {
266        return None;
267    }
268    let inserted = u32::try_from(rows.len()).ok()?;
269    let bit_size = inserted.saturating_mul(10).max(64);
270    let num_hashes = 3;
271    let mut bits = vec![0u8; (bit_size as usize).div_ceil(8)];
272    for row in rows {
273        for idx in primary_bloom_indexes(&row.primary_key, bit_size, num_hashes) {
274            bits[(idx / 8) as usize] |= 1u8 << (idx % 8);
275        }
276    }
277    Some(AppendOnlySegmentBloom {
278        num_hashes,
279        bit_size,
280        inserted,
281        bits_hex: hex::encode(bits),
282    })
283}
284
285fn primary_bloom_indexes(key: &[u8], bit_size: u32, num_hashes: u8) -> impl Iterator<Item = u32> {
286    let h1 = fnv1a64(key);
287    let h2 = djb2_64(key).max(1);
288    (0..num_hashes)
289        .map(move |idx| h1.wrapping_add(u64::from(idx).wrapping_mul(h2)) as u32 % bit_size)
290}
291
292fn fnv1a64(bytes: &[u8]) -> u64 {
293    let mut hash = 0xcbf2_9ce4_8422_2325u64;
294    for byte in bytes {
295        hash ^= u64::from(*byte);
296        hash = hash.wrapping_mul(0x100_0000_01b3);
297    }
298    hash
299}
300
301fn djb2_64(bytes: &[u8]) -> u64 {
302    let mut hash = 5381u64;
303    for byte in bytes {
304        hash = hash.wrapping_mul(33).wrapping_add(u64::from(*byte));
305    }
306    hash
307}
308
309fn chunk_checksums(bytes: &[u8]) -> Vec<AppendOnlySegmentChunkChecksum> {
310    bytes
311        .chunks(APPEND_ONLY_SEGMENT_CHUNK_BYTES as usize)
312        .enumerate()
313        .map(|(idx, chunk)| AppendOnlySegmentChunkChecksum {
314            offset: (idx as u64) * u64::from(APPEND_ONLY_SEGMENT_CHUNK_BYTES),
315            len: chunk.len() as u32,
316            checksum: format!("crc32:{:08x}", crc32(chunk)),
317        })
318        .collect()
319}
320
321fn read_u32(bytes: &[u8], offset: usize) -> RdbFileResult<u32> {
322    let slice = bytes
323        .get(offset..offset + 4)
324        .ok_or_else(|| RdbFileError::InvalidOperation("append-only segment u32 missing".into()))?;
325    Ok(u32::from_le_bytes(slice.try_into().expect("slice length")))
326}
327
328fn read_u64(bytes: &[u8], offset: usize) -> RdbFileResult<u64> {
329    let slice = bytes
330        .get(offset..offset + 8)
331        .ok_or_else(|| RdbFileError::InvalidOperation("append-only segment u64 missing".into()))?;
332    Ok(u64::from_le_bytes(slice.try_into().expect("slice length")))
333}
334
335fn decode_optional_hex(value: Option<String>) -> RdbFileResult<Option<Vec<u8>>> {
336    value
337        .map(hex::decode)
338        .transpose()
339        .map_err(invalid_operation)
340}
341
342fn crc32(data: &[u8]) -> u32 {
343    let mut hasher = crc32fast::Hasher::new();
344    hasher.update(data);
345    hasher.finalize()
346}
347
348fn invalid_operation(err: impl ToString) -> RdbFileError {
349    RdbFileError::InvalidOperation(err.to_string())
350}