Skip to main content

reddb_wire/
legacy.rs

1//! Legacy RedDB binary protocol vocabulary.
2//!
3//! This is the pre-RedWire frame shape still used by the direct TCP
4//! handlers and fast paths:
5//!
6//! ```text
7//! [total_len: u32 LE][msg_type: u8][payload...]
8//! ```
9//!
10//! The crate owns the byte-level contract. The conversions between the
11//! engine `Value` (now in the keystone crate `reddb-io-types`, below wire)
12//! and `WireValue` live here: the orphan rule pins `From`/`TryFrom` impls
13//! that mention `WireValue` to this, its home crate (ADR 0052, #1061).
14
15use reddb_types::Value;
16
17// Message type constants.
18pub const MSG_QUERY: u8 = 0x01;
19pub const MSG_RESULT: u8 = 0x02;
20pub const MSG_ERROR: u8 = 0x03;
21pub const MSG_BULK_INSERT: u8 = 0x04;
22pub const MSG_BULK_OK: u8 = 0x05;
23pub const MSG_BULK_INSERT_BINARY: u8 = 0x06;
24pub const MSG_QUERY_BINARY: u8 = 0x07;
25pub const MSG_BULK_INSERT_PREVALIDATED: u8 = 0x08;
26pub const MSG_BULK_STREAM_START: u8 = 0x09;
27pub const MSG_BULK_STREAM_ROWS: u8 = 0x0A;
28pub const MSG_BULK_STREAM_COMMIT: u8 = 0x0B;
29pub const MSG_BULK_STREAM_ACK: u8 = 0x0C;
30pub const MSG_PREPARE: u8 = 0x0D;
31pub const MSG_PREPARED_OK: u8 = 0x0E;
32pub const MSG_EXECUTE_PREPARED: u8 = 0x0F;
33pub const MSG_DEALLOCATE: u8 = 0x10;
34pub const MSG_DECLARE_CURSOR: u8 = 0x11;
35pub const MSG_CURSOR_OK: u8 = 0x12;
36pub const MSG_FETCH: u8 = 0x13;
37pub const MSG_CURSOR_BATCH: u8 = 0x14;
38pub const MSG_CLOSE_CURSOR: u8 = 0x15;
39
40// Value type tags.
41pub const VAL_NULL: u8 = 0;
42pub const VAL_I64: u8 = 1;
43pub const VAL_F64: u8 = 2;
44pub const VAL_TEXT: u8 = 3;
45pub const VAL_BOOL: u8 = 4;
46pub const VAL_U64: u8 = 5;
47
48#[derive(Debug, Clone, PartialEq)]
49pub enum WireValue {
50    Null,
51    I64(i64),
52    U64(u64),
53    F64(f64),
54    Text(String),
55    Bool(bool),
56    Bytes(Vec<u8>),
57    Timestamp(u64),
58}
59
60// Conversions between the engine `Value` (keystone crate `reddb-io-types`)
61// and the legacy `WireValue`. Re-homed from `reddb-server`'s
62// `wire::protocol` (ADR 0052, #1061): once `Value` moved below wire, the
63// orphan rule required these impls to live in `WireValue`'s home crate.
64// Bodies are byte-faithful relocations; the byte contract is unchanged.
65
66impl From<&Value> for WireValue {
67    fn from(value: &Value) -> Self {
68        match value {
69            Value::Null => WireValue::Null,
70            Value::Integer(n) => WireValue::I64(*n),
71            Value::UnsignedInteger(n) => WireValue::U64(*n),
72            Value::Float(f) => WireValue::F64(*f),
73            Value::Text(s) => WireValue::Text(s.to_string()),
74            Value::Blob(bytes) => WireValue::Bytes(bytes.clone()),
75            Value::Boolean(b) => WireValue::Bool(*b),
76            Value::Timestamp(t) => WireValue::Timestamp(*t as u64),
77            _ => WireValue::Null,
78        }
79    }
80}
81
82impl From<Value> for WireValue {
83    fn from(value: Value) -> Self {
84        match value {
85            Value::Null => WireValue::Null,
86            Value::Integer(n) => WireValue::I64(n),
87            Value::UnsignedInteger(n) => WireValue::U64(n),
88            Value::Float(f) => WireValue::F64(f),
89            Value::Text(s) => WireValue::Text(s.to_string()),
90            Value::Blob(bytes) => WireValue::Bytes(bytes),
91            Value::Boolean(b) => WireValue::Bool(b),
92            Value::Timestamp(t) => WireValue::Timestamp(t as u64),
93            _ => WireValue::Null,
94        }
95    }
96}
97
98impl TryFrom<WireValue> for Value {
99    type Error = &'static str;
100
101    fn try_from(value: WireValue) -> Result<Self, Self::Error> {
102        match value {
103            WireValue::Null => Ok(Value::Null),
104            WireValue::I64(n) => Ok(Value::Integer(n)),
105            WireValue::U64(n) => Ok(Value::UnsignedInteger(n)),
106            WireValue::F64(f) => Ok(Value::Float(f)),
107            WireValue::Text(s) => Ok(Value::text(s)),
108            WireValue::Bool(b) => Ok(Value::Boolean(b)),
109            WireValue::Bytes(bytes) => Ok(Value::Blob(bytes)),
110            WireValue::Timestamp(t) => {
111                let timestamp = i64::try_from(t).map_err(|_| "timestamp exceeds i64 range")?;
112                Ok(Value::Timestamp(timestamp))
113            }
114        }
115    }
116}
117
118/// Write a legacy frame header: [total_len: u32 LE][msg_type: u8].
119#[inline]
120pub fn write_frame_header(buf: &mut Vec<u8>, msg_type: u8, payload_len: u32) {
121    let total = payload_len + 1; // +1 for msg_type
122    buf.extend_from_slice(&total.to_le_bytes());
123    buf.push(msg_type);
124}
125
126pub fn build_legacy_frame(msg_type: u8, payload: &[u8]) -> Vec<u8> {
127    let mut out = Vec::with_capacity(5 + payload.len());
128    write_frame_header(&mut out, msg_type, payload.len() as u32);
129    out.extend_from_slice(payload);
130    out
131}
132
133pub fn build_legacy_result_frame(payload: &[u8]) -> Vec<u8> {
134    build_legacy_frame(MSG_RESULT, payload)
135}
136
137/// Encode a result payload directly into its final frame allocation. The
138/// callback must append after the reserved header without modifying it.
139/// Capacity is a hint, not an admission limit.
140pub fn build_legacy_result_frame_with(
141    payload_capacity: usize,
142    encode: impl FnOnce(&mut Vec<u8>),
143) -> Vec<u8> {
144    let mut out = Vec::with_capacity(payload_capacity.saturating_add(5));
145    write_frame_header(&mut out, MSG_RESULT, 0);
146    encode(&mut out);
147    assert!(
148        out.len() >= 5,
149        "encoder must preserve the legacy frame header"
150    );
151    let total = u32::try_from(out.len() - 4).expect("legacy frame length fits u32");
152    out[..4].copy_from_slice(&total.to_le_bytes());
153    out
154}
155
156/// Finish a result in caller-owned, already admitted capacity. The first five
157/// bytes are reserved for the legacy envelope; no payload copy or growth occurs.
158pub fn build_legacy_result_frame_in_place(out: &mut [u8]) -> Result<(), &'static str> {
159    if out.len() < 5 {
160        return Err("truncated legacy result buffer");
161    }
162    let total = u32::try_from(out.len() - 4).map_err(|_| "legacy result exceeds frame length")?;
163    out[..4].copy_from_slice(&total.to_le_bytes());
164    out[4] = MSG_RESULT;
165    Ok(())
166}
167
168pub fn build_legacy_error_frame(message: &[u8]) -> Vec<u8> {
169    build_legacy_frame(MSG_ERROR, message)
170}
171
172pub fn build_legacy_bulk_ok_frame(payload: &[u8]) -> Vec<u8> {
173    build_legacy_frame(MSG_BULK_OK, payload)
174}
175
176pub fn build_legacy_bulk_stream_ack_frame() -> Vec<u8> {
177    build_legacy_frame(MSG_BULK_STREAM_ACK, &[])
178}
179
180pub fn build_legacy_prepared_ok_frame(payload: &[u8]) -> Vec<u8> {
181    build_legacy_frame(MSG_PREPARED_OK, payload)
182}
183
184pub fn build_legacy_cursor_ok_frame(payload: &[u8]) -> Vec<u8> {
185    build_legacy_frame(MSG_CURSOR_OK, payload)
186}
187
188pub fn build_legacy_cursor_batch_frame(payload: &[u8]) -> Vec<u8> {
189    build_legacy_frame(MSG_CURSOR_BATCH, payload)
190}
191
192#[inline]
193pub fn encode_value(buf: &mut Vec<u8>, value: &WireValue) {
194    match value {
195        WireValue::Null | WireValue::Bytes(_) => buf.push(VAL_NULL),
196        WireValue::I64(n) => {
197            buf.push(VAL_I64);
198            buf.extend_from_slice(&n.to_le_bytes());
199        }
200        WireValue::U64(n) | WireValue::Timestamp(n) => {
201            buf.push(VAL_U64);
202            buf.extend_from_slice(&n.to_le_bytes());
203        }
204        WireValue::F64(f) => {
205            buf.push(VAL_F64);
206            buf.extend_from_slice(&f.to_le_bytes());
207        }
208        WireValue::Text(s) => {
209            buf.push(VAL_TEXT);
210            let bytes = s.as_bytes();
211            buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
212            buf.extend_from_slice(bytes);
213        }
214        WireValue::Bool(b) => {
215            buf.push(VAL_BOOL);
216            buf.push(*b as u8);
217        }
218    }
219}
220
221/// Encode borrowed engine values without constructing owned WireValue text/blob
222/// copies. Unsupported variants (including Blob) retain the legacy Null tag.
223#[inline]
224pub fn encode_native_value(buf: &mut Vec<u8>, value: &Value) {
225    match value {
226        Value::Text(text) => {
227            buf.push(VAL_TEXT);
228            buf.extend_from_slice(&(text.len() as u32).to_le_bytes());
229            buf.extend_from_slice(text.as_bytes());
230        }
231        Value::Integer(value) => encode_value(buf, &WireValue::I64(*value)),
232        Value::UnsignedInteger(value) => encode_value(buf, &WireValue::U64(*value)),
233        Value::Float(value) => encode_value(buf, &WireValue::F64(*value)),
234        Value::Boolean(value) => encode_value(buf, &WireValue::Bool(*value)),
235        Value::Timestamp(value) => encode_value(buf, &WireValue::Timestamp(*value as u64)),
236        _ => buf.push(VAL_NULL),
237    }
238}
239
240/// Exact encoded length without allocating or traversing text contents.
241#[inline]
242pub fn native_value_encoded_len(value: &Value) -> usize {
243    match value {
244        Value::Text(text) => 5 + text.len(),
245        Value::Integer(_) | Value::UnsignedInteger(_) | Value::Float(_) | Value::Timestamp(_) => 9,
246        Value::Boolean(_) => 2,
247        _ => 1,
248    }
249}
250
251#[inline]
252pub fn decode_value(data: &[u8], pos: &mut usize) -> WireValue {
253    try_decode_value(data, pos).unwrap_or(WireValue::Null)
254}
255
256#[inline]
257pub fn try_decode_value(data: &[u8], pos: &mut usize) -> Result<WireValue, &'static str> {
258    if *pos >= data.len() {
259        return Err("missing value tag");
260    }
261
262    let tag = data[*pos];
263    *pos += 1;
264
265    match tag {
266        VAL_NULL => Ok(WireValue::Null),
267        VAL_I64 => Ok(WireValue::I64(i64::from_le_bytes(read_array::<8>(
268            data,
269            pos,
270            "truncated i64 value",
271        )?))),
272        VAL_U64 => Ok(WireValue::U64(u64::from_le_bytes(read_array::<8>(
273            data,
274            pos,
275            "truncated u64 value",
276        )?))),
277        VAL_F64 => Ok(WireValue::F64(f64::from_le_bytes(read_array::<8>(
278            data,
279            pos,
280            "truncated f64 value",
281        )?))),
282        VAL_TEXT => {
283            let len =
284                u32::from_le_bytes(read_array::<4>(data, pos, "truncated text length")?) as usize;
285            let bytes = read_bytes(data, pos, len, "truncated text value")?;
286            let cow = std::string::String::from_utf8_lossy(bytes);
287            Ok(WireValue::Text(cow.into_owned()))
288        }
289        VAL_BOOL => {
290            let bytes = read_bytes(data, pos, 1, "truncated bool value")?;
291            Ok(WireValue::Bool(bytes[0] != 0))
292        }
293        _ => Err("unknown value tag"),
294    }
295}
296
297#[inline]
298fn read_bytes<'a>(
299    data: &'a [u8],
300    pos: &mut usize,
301    len: usize,
302    err: &'static str,
303) -> Result<&'a [u8], &'static str> {
304    let end = pos.saturating_add(len);
305    if end > data.len() {
306        return Err(err);
307    }
308    let bytes = &data[*pos..end];
309    *pos = end;
310    Ok(bytes)
311}
312
313#[inline]
314fn read_array<const N: usize>(
315    data: &[u8],
316    pos: &mut usize,
317    err: &'static str,
318) -> Result<[u8; N], &'static str> {
319    let bytes = read_bytes(data, pos, N, err)?;
320    let mut array = [0u8; N];
321    array.copy_from_slice(bytes);
322    Ok(array)
323}
324
325#[inline]
326pub fn encode_column_name(buf: &mut Vec<u8>, name: &str) {
327    let bytes = name.as_bytes();
328    buf.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
329    buf.extend_from_slice(bytes);
330}
331
332pub fn encode_result_payload_header<'a, I>(buf: &mut Vec<u8>, columns: I, row_count: u32) -> usize
333where
334    I: IntoIterator<Item = &'a str>,
335    I::IntoIter: ExactSizeIterator,
336{
337    let columns = columns.into_iter();
338    buf.extend_from_slice(&(columns.len() as u16).to_le_bytes());
339    for column in columns {
340        encode_column_name(buf, column);
341    }
342    let row_count_offset = buf.len();
343    buf.extend_from_slice(&row_count.to_le_bytes());
344    row_count_offset
345}
346
347pub fn set_result_payload_row_count(
348    buf: &mut [u8],
349    row_count_offset: usize,
350    row_count: u32,
351) -> Result<(), &'static str> {
352    let end = row_count_offset.saturating_add(4);
353    if end > buf.len() {
354        return Err("result payload row-count offset out of bounds");
355    }
356    buf[row_count_offset..end].copy_from_slice(&row_count.to_le_bytes());
357    Ok(())
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn admitted_in_place_result_matches_canonical_envelope() {
366        for payload in [b"".as_slice(), b"hello", &[0, 255, 1, 2]] {
367            let expected = build_legacy_result_frame(payload);
368            let mut actual = vec![0; 5 + payload.len()];
369            actual[5..].copy_from_slice(payload);
370            build_legacy_result_frame_in_place(&mut actual).unwrap();
371            assert_eq!(actual, expected);
372        }
373        for length in 0..5 {
374            let mut truncated = vec![0; length];
375            assert!(build_legacy_result_frame_in_place(&mut truncated).is_err());
376        }
377    }
378
379    #[test]
380    fn borrowed_native_encoding_matches_owned_conversion_and_exact_length() {
381        let values = [
382            Value::Null,
383            Value::Integer(i64::MIN),
384            Value::UnsignedInteger(u64::MAX),
385            Value::Float(-1.25),
386            Value::Boolean(true),
387            Value::Boolean(false),
388            Value::Timestamp(-1),
389            Value::text(""),
390            Value::text("quotes\"雪\n"),
391            Value::text("x".repeat(192 * 1024)),
392            Value::Blob(vec![1; 192 * 1024]),
393        ];
394        for value in values {
395            let mut expected = Vec::new();
396            encode_value(&mut expected, &WireValue::from(&value));
397            let mut actual = Vec::with_capacity(native_value_encoded_len(&value));
398            encode_native_value(&mut actual, &value);
399            assert_eq!(actual, expected);
400            assert_eq!(actual.len(), native_value_encoded_len(&value));
401            assert_eq!(actual.len(), actual.capacity());
402        }
403    }
404
405    // Pins the WireValue<->Value field mapping re-homed here from
406    // reddb-server's wire::protocol (ADR 0052, #1061). The conversion is a
407    // byte-faithful relocation, so this round-trips every losslessly-mapped
408    // variant and locks the supported tag correspondence.
409    #[test]
410    fn value_wirevalue_field_mapping_round_trips() {
411        let cases = [
412            (Value::Null, WireValue::Null),
413            (Value::Integer(-7), WireValue::I64(-7)),
414            (Value::UnsignedInteger(9), WireValue::U64(9)),
415            (Value::Float(1.5), WireValue::F64(1.5)),
416            (Value::text("hi"), WireValue::Text("hi".to_string())),
417            (Value::Blob(vec![1, 2, 3]), WireValue::Bytes(vec![1, 2, 3])),
418            (Value::Boolean(true), WireValue::Bool(true)),
419            (Value::Timestamp(42), WireValue::Timestamp(42)),
420        ];
421        for (value, wire) in cases {
422            // Value -> WireValue (owned and borrowed paths agree).
423            assert_eq!(WireValue::from(value.clone()), wire);
424            assert_eq!(WireValue::from(&value), wire);
425            // WireValue -> Value round-trips back to the original.
426            assert_eq!(Value::try_from(wire.clone()), Ok(value));
427        }
428    }
429
430    #[test]
431    fn wirevalue_timestamp_rejects_i64_overflow() {
432        let overflow = WireValue::Timestamp(u64::MAX);
433        assert_eq!(
434            Value::try_from(overflow),
435            Err("timestamp exceeds i64 range")
436        );
437    }
438
439    #[test]
440    fn frame_header_keeps_legacy_length_shape() {
441        let mut out = Vec::new();
442        write_frame_header(&mut out, MSG_RESULT, 10);
443        assert_eq!(out, [11, 0, 0, 0, MSG_RESULT]);
444    }
445
446    #[test]
447    fn legacy_frame_builders_wrap_payloads() {
448        assert_eq!(
449            build_legacy_result_frame(b"ok"),
450            [3, 0, 0, 0, MSG_RESULT, b'o', b'k']
451        );
452        assert_eq!(
453            build_legacy_error_frame(b"no"),
454            [3, 0, 0, 0, MSG_ERROR, b'n', b'o']
455        );
456        assert_eq!(
457            build_legacy_bulk_ok_frame(b"\x02\0\0\0\0\0\0\0"),
458            [9, 0, 0, 0, MSG_BULK_OK, 2, 0, 0, 0, 0, 0, 0, 0]
459        );
460        assert_eq!(
461            build_legacy_bulk_stream_ack_frame(),
462            [1, 0, 0, 0, MSG_BULK_STREAM_ACK]
463        );
464        assert_eq!(
465            build_legacy_prepared_ok_frame(b"p"),
466            [2, 0, 0, 0, MSG_PREPARED_OK, b'p']
467        );
468        assert_eq!(
469            build_legacy_cursor_ok_frame(b"c"),
470            [2, 0, 0, 0, MSG_CURSOR_OK, b'c']
471        );
472        assert_eq!(
473            build_legacy_cursor_batch_frame(b"b"),
474            [2, 0, 0, 0, MSG_CURSOR_BATCH, b'b']
475        );
476    }
477
478    #[test]
479    fn direct_result_frame_encoder_matches_legacy_bytes_across_growth() {
480        for size in [0, 1, 255, 1024, 192 * 1024] {
481            let payload = vec![0x71; size];
482            let frame = build_legacy_result_frame_with(8, |out| {
483                out.extend_from_slice(&payload[..size / 2]);
484                out.extend_from_slice(&payload[size / 2..]);
485            });
486            assert_eq!(frame, build_legacy_result_frame(&payload));
487        }
488    }
489
490    #[test]
491    fn wire_values_round_trip_legacy_tags() {
492        let values = [
493            WireValue::Null,
494            WireValue::I64(-7),
495            WireValue::U64(42),
496            WireValue::F64(3.5),
497            WireValue::Text("hello".to_string()),
498            WireValue::Bool(true),
499            WireValue::Timestamp(1234),
500        ];
501
502        let mut bytes = Vec::new();
503        for value in &values {
504            encode_value(&mut bytes, value);
505        }
506
507        let mut pos = 0;
508        assert_eq!(try_decode_value(&bytes, &mut pos), Ok(WireValue::Null));
509        assert_eq!(try_decode_value(&bytes, &mut pos), Ok(WireValue::I64(-7)));
510        assert_eq!(try_decode_value(&bytes, &mut pos), Ok(WireValue::U64(42)));
511        assert_eq!(try_decode_value(&bytes, &mut pos), Ok(WireValue::F64(3.5)));
512        assert_eq!(
513            try_decode_value(&bytes, &mut pos),
514            Ok(WireValue::Text("hello".to_string()))
515        );
516        assert_eq!(
517            try_decode_value(&bytes, &mut pos),
518            Ok(WireValue::Bool(true))
519        );
520        assert_eq!(try_decode_value(&bytes, &mut pos), Ok(WireValue::U64(1234)));
521        assert_eq!(pos, bytes.len());
522    }
523
524    #[test]
525    fn result_payload_header_encodes_columns_and_row_count() {
526        let mut bytes = Vec::new();
527        let row_count_offset = encode_result_payload_header(&mut bytes, ["id", "name"], 3);
528
529        assert_eq!(
530            bytes,
531            [
532                2, 0, // ncols
533                2, 0, b'i', b'd', // "id"
534                4, 0, b'n', b'a', b'm', b'e', // "name"
535                3, 0, 0, 0, // nrows
536            ]
537        );
538        assert_eq!(row_count_offset, bytes.len() - 4);
539        set_result_payload_row_count(&mut bytes, row_count_offset, 5).unwrap();
540        assert_eq!(
541            &bytes[row_count_offset..row_count_offset + 4],
542            &[5, 0, 0, 0]
543        );
544    }
545}