Skip to main content

reifydb_codec/frame/decode/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4mod any;
5mod fixed;
6mod varlen;
7
8use std::{str, sync::Arc};
9
10use arrow_array::{ArrayRef, LargeStringArray, RecordBatch, RecordBatchOptions, make_array};
11use arrow_buffer::{BooleanBuffer, Buffer, NullBuffer};
12use arrow_schema::{FieldRef, Schema};
13use reifydb_value::{
14	encoding::LeBytes,
15	reifydb_assertions,
16	value::{
17		container::varlen_array::blob_array,
18		diff_type::DiffType,
19		frame::frame::Frame,
20		value_type::{
21			ValueType,
22			field::{FieldType, to_field},
23		},
24	},
25};
26
27use crate::{
28	error::DecodeError,
29	frame::{
30		encoding::dict::{decode_dict_blob, decode_dict_utf8},
31		format::{
32			COL_FLAG_HAS_NONES, COLUMN_DESCRIPTOR_SIZE, Encoding, FRAME_HEADER_SIZE, MESSAGE_HEADER_SIZE,
33			RBCF_MAGIC, RBCF_VERSION, dict_index_width_from_flags,
34		},
35	},
36	tag::{TypeTag, ValueKind},
37};
38
39pub fn decode_frames(data: &[u8]) -> Result<Vec<Frame>, DecodeError> {
40	let mut pos = 0;
41
42	check_len(data, pos, MESSAGE_HEADER_SIZE)?;
43	let magic = read_u32(data, pos);
44	pos += 4;
45	if magic != RBCF_MAGIC {
46		return Err(DecodeError::InvalidMagic(magic));
47	}
48	let version = read_u16(data, pos);
49	pos += 2;
50	if version != RBCF_VERSION {
51		return Err(DecodeError::UnsupportedVersion(version));
52	}
53	let _flags = read_u16(data, pos);
54	pos += 2;
55	let frame_count = read_u32(data, pos) as usize;
56	pos += 4;
57	let _total_size = read_u32(data, pos) as usize;
58	pos += 4;
59
60	let mut frames = Vec::with_capacity(frame_count);
61	for _ in 0..frame_count {
62		let (frame, new_pos) = decode_frame(data, pos)?;
63		frames.push(frame);
64		pos = new_pos;
65	}
66
67	reifydb_assertions! {
68		assert!(
69			pos == _total_size,
70			"the RBCF message header declared a total size that disagrees with the bytes consumed while decoding, so the message is truncated or carries trailing bytes and a peer would mis-frame the following message (declared={} consumed={})",
71			_total_size,
72			pos
73		);
74	}
75
76	Ok(frames)
77}
78
79struct FrameHeader {
80	row_count: usize,
81	column_count: usize,
82	op: Option<DiffType>,
83}
84
85fn decode_frame(data: &[u8], start: usize) -> Result<(Frame, usize), DecodeError> {
86	let (header, pos) = read_frame_header(data, start)?;
87	let (columns, pos) = read_frame_columns(data, pos, header.column_count)?;
88
89	Ok((
90		Frame {
91			batch: frame_batch(columns, header.row_count)?,
92			op: header.op,
93		},
94		pos,
95	))
96}
97
98pub(crate) fn frame_batch(columns: Vec<(FieldRef, ArrayRef)>, row_count: usize) -> Result<RecordBatch, DecodeError> {
99	let (fields, arrays): (Vec<FieldRef>, Vec<ArrayRef>) = columns.into_iter().unzip();
100	RecordBatch::try_new_with_options(
101		Arc::new(Schema::new(fields)),
102		arrays,
103		&RecordBatchOptions::new().with_row_count(Some(row_count)),
104	)
105	.map_err(|error| DecodeError::InvalidData(format!("frame columns do not form a batch: {error}")))
106}
107
108#[inline]
109fn read_frame_header(data: &[u8], start: usize) -> Result<(FrameHeader, usize), DecodeError> {
110	let mut pos = start;
111	check_len(data, pos, FRAME_HEADER_SIZE)?;
112	let row_count = read_u32(data, pos) as usize;
113	pos += 4;
114	let column_count = read_u16(data, pos) as usize;
115	pos += 2;
116	let meta_flags = data[pos];
117	if meta_flags != 0 {
118		return Err(DecodeError::InvalidData(format!("frame meta byte must be 0, found 0x{meta_flags:02X}")));
119	}
120	pos += 1;
121	let op = match data[pos] {
122		0 => None,
123		raw => Some(DiffType::from_u8(raw)
124			.ok_or_else(|| DecodeError::InvalidData(format!("unknown frame op {raw}")))?),
125	};
126	pos += 1;
127	let _frame_size = read_u32(data, pos);
128	pos += 4;
129	Ok((
130		FrameHeader {
131			row_count,
132			column_count,
133			op,
134		},
135		pos,
136	))
137}
138
139#[inline]
140fn read_frame_columns(
141	data: &[u8],
142	mut pos: usize,
143	column_count: usize,
144) -> Result<(Vec<(FieldRef, ArrayRef)>, usize), DecodeError> {
145	let mut columns = Vec::with_capacity(column_count);
146	for _ in 0..column_count {
147		let (col, new_pos) = decode_column(data, pos)?;
148		columns.push(col);
149		pos = new_pos;
150	}
151	Ok((columns, pos))
152}
153
154fn decode_column(data: &[u8], start: usize) -> Result<((FieldRef, ArrayRef), usize), DecodeError> {
155	let mut pos = start;
156	check_len(data, pos, COLUMN_DESCRIPTOR_SIZE)?;
157
158	let type_code = data[pos];
159	pos += 1;
160	let encoding_byte = data[pos];
161	pos += 1;
162	let flags = data[pos];
163	pos += 1;
164	let _reserved = data[pos];
165	pos += 1;
166	let name_len = read_u16(data, pos) as usize;
167	pos += 2;
168	let _reserved2 = read_u16(data, pos);
169	pos += 2;
170	let row_count = read_u32(data, pos) as usize;
171	pos += 4;
172	let nones_len = read_u32(data, pos) as usize;
173	pos += 4;
174	let data_len = read_u32(data, pos) as usize;
175	pos += 4;
176	let offsets_len = read_u32(data, pos) as usize;
177	pos += 4;
178	let extra_len = read_u32(data, pos) as usize;
179	pos += 4;
180
181	let encoding = Encoding::from_u8(encoding_byte).ok_or(DecodeError::UnknownEncoding(encoding_byte))?;
182	let has_nones = flags & COL_FLAG_HAS_NONES != 0;
183	let tag = TypeTag::from_byte(type_code)?;
184	let depth = tag.depth() as usize;
185	let base_code = tag.kind_bits();
186
187	check_len(data, pos, name_len)?;
188	let name = str::from_utf8(&data[pos..pos + name_len])
189		.map_err(|e| DecodeError::InvalidData(format!("invalid column name: {}", e)))?
190		.to_string();
191	pos += name_len;
192	let name_pad = (4 - (name_len % 4)) % 4;
193	pos += name_pad;
194
195	let result = (|| -> Result<((FieldRef, ArrayRef), usize), DecodeError> {
196		let mut pos = pos;
197
198		if has_nones != (depth > 0) {
199			return Err(DecodeError::InvalidData(format!(
200				"column type code 0x{type_code:02X} has option depth {depth} but the has-nones flag is {}",
201				if has_nones {
202					"set"
203				} else {
204					"clear"
205				}
206			)));
207		}
208		if depth > 1 {
209			return Err(DecodeError::InvalidData(format!(
210				"column type code 0x{type_code:02X} has option depth {depth}, but a column holds at most one option layer"
211			)));
212		}
213		let layer_len = row_count.div_ceil(8);
214		if nones_len != depth * layer_len {
215			return Err(DecodeError::InvalidData(format!(
216				"nones length {nones_len} disagrees with option depth {depth} and row count {row_count} (expected {})",
217				depth * layer_len
218			)));
219		}
220		check_len(data, pos, nones_len)?;
221		let layers: Vec<BooleanBuffer> = (0..depth)
222			.map(|layer| {
223				let start = pos + layer * layer_len;
224				decode_bitvec(&data[start..start + layer_len], row_count)
225			})
226			.collect();
227		pos += nones_len;
228
229		check_len(data, pos, data_len)?;
230		let data_bytes = &data[pos..pos + data_len];
231		pos += data_len;
232
233		check_len(data, pos, offsets_len)?;
234		let offsets_bytes = &data[pos..pos + offsets_len];
235		pos += offsets_len;
236
237		check_len(data, pos, extra_len)?;
238		let extra_bytes = &data[pos..pos + extra_len];
239		pos += extra_len;
240
241		let (enum_name, base_code, extra_bytes) = match base_code == ValueKind::Enum.byte() {
242			true => {
243				let (enum_name, rest) = decode_enum_name(extra_bytes)?;
244				(Some(enum_name), ValueKind::Utf8.byte(), rest)
245			}
246			false => (None, base_code, extra_bytes),
247		};
248		let (value_type, array) = decode_column_dispatch(
249			base_code,
250			encoding,
251			flags,
252			row_count,
253			data_bytes,
254			offsets_bytes,
255			extra_bytes,
256			layers.last(),
257		)?;
258
259		let (field, array) = decoded_column(&name, value_type, array, layers.into_iter().next(), enum_name)?;
260		Ok(((field, array), pos))
261	})()
262	.map_err(|e| DecodeError::ColumnDecodeFailed {
263		column_name: name.clone(),
264		row_index: None,
265		source: Box::new(e),
266	})?;
267
268	Ok(result)
269}
270
271pub(crate) fn decoded_column(
272	name: &str,
273	value_type: ValueType,
274	array: ArrayRef,
275	layer: Option<BooleanBuffer>,
276	enum_name: Option<String>,
277) -> Result<(FieldRef, ArrayRef), DecodeError> {
278	let value_type = match enum_name {
279		Some(name) => ValueType::Enum(name),
280		None => value_type,
281	};
282	let declared_type = matches!(value_type, ValueType::List(_) | ValueType::Record(_) | ValueType::Tuple(_))
283		.then(|| value_type.clone());
284	let (value_type, array) = match layer {
285		Some(layer) => (ValueType::Option(Box::new(value_type)), attach_layer(array, layer)?),
286		None if array.logical_null_count() == 0 => (value_type, array),
287		None if matches!(value_type, ValueType::Digest { .. }) => {
288			(ValueType::Option(Box::new(value_type)), array)
289		}
290		None => {
291			return Err(DecodeError::InvalidData(format!(
292				"column of type {value_type:?} holds {} none cells but has no option layer",
293				array.logical_null_count()
294			)));
295		}
296	};
297	let field_type = FieldType {
298		value_type: Some(value_type),
299		declared_type,
300		..FieldType::default()
301	};
302	Ok((Arc::new(to_field(name, &field_type)), array))
303}
304
305fn attach_layer(array: ArrayRef, layer: BooleanBuffer) -> Result<ArrayRef, DecodeError> {
306	let nulls = NullBuffer::union(Some(&NullBuffer::new(layer)), array.logical_nulls().as_ref());
307	let data =
308		array.to_data().into_builder().nulls(nulls).build().map_err(|error| {
309			DecodeError::InvalidData(format!("option layer does not fit the column: {error}"))
310		})?;
311	Ok(make_array(data))
312}
313
314fn decode_enum_name(extra: &[u8]) -> Result<(String, &[u8]), DecodeError> {
315	let short = || DecodeError::InvalidData("enum column name is truncated".to_string());
316	let len = u16::from_le_bytes(extra.get(0..2).ok_or_else(short)?.try_into().map_err(|_| short())?) as usize;
317	let name = str::from_utf8(extra.get(2..2 + len).ok_or_else(short)?)
318		.map_err(|e| DecodeError::InvalidData(format!("enum column name is not utf8: {e}")))?;
319	Ok((name.to_string(), &extra[2 + len..]))
320}
321
322pub(crate) fn column_type_from_code(type_code: u8) -> Result<ValueType, DecodeError> {
323	TypeTag::from_byte(type_code)?.to_type()
324}
325
326#[allow(clippy::too_many_arguments)]
327fn decode_column_dispatch(
328	type_code: u8,
329	encoding: Encoding,
330	flags: u8,
331	row_count: usize,
332	data: &[u8],
333	offsets: &[u8],
334	extra: &[u8],
335	defined: Option<&BooleanBuffer>,
336) -> Result<(ValueType, ArrayRef), DecodeError> {
337	if type_code == ValueKind::Digest.byte() {
338		if encoding != Encoding::Plain {
339			return Err(DecodeError::InvalidData(format!(
340				"digest column must use plain encoding, found {encoding:?}"
341			)));
342		}
343		return varlen::decode_digest_plain(row_count, data, offsets, extra);
344	}
345	if let Some(kind @ ValueKind::Decimal) = ValueKind::from_byte(type_code) {
346		return fixed::decode_unscaled_column(kind, encoding, row_count, data, extra);
347	}
348	let ty = column_type_from_code(type_code)?;
349
350	let array: ArrayRef = match encoding {
351		Encoding::Plain | Encoding::BitPack => {
352			if ty == ValueType::Any {
353				any::decode_any_column(row_count, data, defined)?
354			} else if let Some(result) = fixed::decode_fixed_plain(type_code, row_count, data) {
355				result?
356			} else if let Some(result) = varlen::decode_varlen_plain(type_code, row_count, data, offsets) {
357				result?
358			} else {
359				return Err(DecodeError::UnsupportedType(format!("{:?}", ty)));
360			}
361		}
362		Encoding::Dict => match ty {
363			ValueType::Utf8 => {
364				let index_width = dict_index_width_from_flags(flags);
365				let strings = decode_dict_utf8(data, extra, row_count, index_width)?;
366				Arc::new(LargeStringArray::from(strings))
367			}
368			ValueType::Blob => {
369				let index_width = dict_index_width_from_flags(flags);
370				let blobs = decode_dict_blob(data, extra, row_count, index_width)?;
371				Arc::new(blob_array(&blobs))
372			}
373			_ => {
374				return Err(DecodeError::InvalidData(format!(
375					"Dict encoding not supported for type {:?}",
376					ty
377				)));
378			}
379		},
380		Encoding::Rle => fixed::decode_rle_column(type_code, row_count, data)?,
381		Encoding::Delta => fixed::decode_delta_column(type_code, row_count, data)?,
382		Encoding::DeltaRle => fixed::decode_delta_rle_column(type_code, row_count, data)?,
383	};
384	Ok((ty, array))
385}
386
387fn decode_bitvec(data: &[u8], len: usize) -> BooleanBuffer {
388	BooleanBuffer::new(Buffer::from_vec(data.to_vec()), 0, len)
389}
390
391#[inline]
392fn read_u16(data: &[u8], pos: usize) -> u16 {
393	u16::read_le(&data[pos..])
394}
395
396#[inline]
397fn read_u32(data: &[u8], pos: usize) -> u32 {
398	u32::read_le(&data[pos..])
399}
400
401fn check_len(data: &[u8], pos: usize, needed: usize) -> Result<(), DecodeError> {
402	if pos + needed > data.len() {
403		Err(DecodeError::UnexpectedEof {
404			expected: needed,
405			available: data.len().saturating_sub(pos),
406		})
407	} else {
408		Ok(())
409	}
410}