Skip to main content

reddb_wire/redwire/
mod.rs

1//! RedWire — RedDB's binary TCP/TLS wire protocol.
2//!
3//! ADR 0001 (`.red/adr/0001-redwire-tcp-protocol.md`) is the
4//! normative spec. This module owns the frame layout, message-kind
5//! discriminator, flags, encode/decode codec, and generic async
6//! frame I/O over byte streams. Server-side dispatch, auth policy,
7//! session loop, and listener accept stay in `reddb` and depend on
8//! these types.
9
10pub mod builder;
11pub mod bulk_binary;
12pub mod bulk_json;
13pub mod bulk_stream;
14pub mod codec;
15pub mod cursor;
16pub mod frame;
17pub mod handshake;
18pub mod io;
19pub mod operations;
20pub mod prepared;
21pub mod queue;
22pub mod scram;
23pub mod stream;
24pub mod typed_insert;
25pub mod ws_gate;
26
27pub use builder::{
28    build_bulk_insert_binary_frame, build_bulk_insert_frame, build_bye_frame, build_delete_frame,
29    build_dispatch_reply_frame, build_error_frame, build_error_frame_lossy, build_get_frame,
30    build_ping_frame, build_query_frame, build_query_with_params_frame, build_reply_frame,
31    build_request_frame, rewrap_length_prefixed_handler_response, BuildError, FrameBuilder,
32};
33pub use bulk_binary::{
34    decode_bulk_binary_payload, encode_bulk_binary_payload, BulkBinaryError, BulkBinaryFlavor,
35    BulkBinaryPayload,
36};
37pub use bulk_json::{
38    decode_bulk_json_payload, encode_bulk_json_payload, BulkJsonError, BulkJsonPayload,
39};
40pub use bulk_stream::{
41    decode_bulk_stream_rows_payload, decode_bulk_stream_start_payload,
42    encode_bulk_stream_rows_payload, encode_bulk_stream_start_payload, BulkStreamError,
43    BulkStreamRowsPayload, BulkStreamStartPayload,
44};
45pub use codec::{
46    decode_frame, decode_frame_parts, encode_frame, frame_len_from_header, FrameError,
47};
48pub use cursor::{
49    decode_close_cursor_payload, decode_declare_cursor_payload, decode_fetch_payload,
50    encode_close_cursor_payload, encode_cursor_batch_payload, encode_cursor_ok_payload,
51    encode_declare_cursor_payload, encode_fetch_payload, CloseCursorPayload, CursorPayloadError,
52    DeclareCursorPayload, FetchPayload,
53};
54pub use frame::{
55    Flags, Frame, MessageClass, MessageDirection, MessageKind, FRAME_HEADER_SIZE, MAX_FRAME_SIZE,
56};
57pub use handshake::{
58    build_auth_fail_frame, build_auth_fail_payload, build_auth_ok_frame_from_payload,
59    build_auth_ok_payload, build_auth_response_anonymous_payload,
60    build_auth_response_bearer_payload, build_auth_response_frame,
61    build_auth_response_oauth_jwt_payload, build_client_hello_frame, build_client_hello_payload,
62    build_hello_ack, build_hello_ack_frame, build_hello_payload, choose_hello_minor_version,
63    expect_auth_response_payload, AuthFail, AuthOk, AuthResponseKindError, Hello, HelloAck,
64    SUPPORTED_METHODS,
65};
66pub use io::{
67    drain_next_frame, frame_to_bytes, read_frame_async, write_frame_async, RedWireIoError,
68};
69pub use operations::{
70    decode_bulk_ok_count_payload, decode_bulk_ok_payload, decode_delete_ok_affected,
71    decode_delete_payload, decode_error_payload, decode_get_payload, decode_get_result_payload,
72    decode_insert_dispatch_payload, decode_query_result_payload, decode_text_payload,
73    encode_bulk_insert_payload, encode_bulk_ok_count_payload, encode_bulk_ok_payload,
74    encode_bulk_ok_payload_from_json_id_literals, encode_bulk_ok_payload_from_json_ids_bytes,
75    encode_delete_ok_payload, encode_get_result_payload, encode_insert_payload, encode_key_payload,
76    encode_query_result_summary_payload, expect_bulk_ok_or_error, expect_delete_ok_or_error,
77    expect_pong_reply, expect_result_or_error, BulkOkPayload, InsertDispatchPayload, KeyPayload,
78    OperationPayloadError, OperationReplyError,
79};
80pub use prepared::{
81    decode_deallocate_payload, decode_execute_prepared_payload, decode_prepare_payload,
82    encode_deallocate_payload, encode_execute_prepared_payload, encode_prepare_payload,
83    encode_prepared_ok_payload, DeallocatePayload, ExecutePreparedPayload, PreparePayload,
84    PreparedOkPayload, PreparedPayloadError,
85};
86pub use queue::{
87    build_event_push_payload, build_event_push_payload_from_json_bytes,
88    build_queue_event_push_frame_from_json_bytes, build_queue_wait_error_frame,
89    build_queue_wait_error_payload, build_queue_wait_open_frame, build_queue_wait_open_payload,
90    build_queue_wait_timeout_frame, build_queue_wait_timeout_payload, parse_queue_wait_open,
91    QueueWaitOpenRequest, QueueWaitParseError, WAIT_CANCELLED_CODE, WAIT_EXCEEDS_CAP_CODE,
92    WAIT_FAILED_CODE,
93};
94pub use stream::{
95    build_input_stream_end_frame, build_input_stream_end_payload, build_input_stream_error_frame,
96    build_input_stream_error_payload, build_open_ack_frame, build_open_ack_payload,
97    build_open_stream_frame, build_open_stream_payload, build_stream_chunk_frame_from_json_bytes,
98    build_stream_chunk_payload, build_stream_chunk_payload_from_json_bytes, build_stream_end_frame,
99    build_stream_end_payload, build_stream_error_frame, build_stream_error_payload,
100    open_stream_is_input, parse_input_chunk, parse_input_chunk_json, parse_open_input,
101    parse_open_stream, parse_stream_cancel, ChunkParseError, InputChunk, InputChunkJson,
102    OpenInputParseError, OpenInputRequest, OpenStreamParseError, OpenStreamRequest,
103    StreamCancelRequest,
104};
105pub use typed_insert::{
106    decode_typed_insert, encode_typed_insert, TypedInsertError, TypedInsertField,
107    TypedInsertRequest, FEATURE_TYPED_MUTATIONS, TYPED_INSERT_VERSION,
108};
109pub use ws_gate::{evaluate_ws_upgrade, WsUpgradeRefusal};
110
111/// Discriminator byte every RedWire client sends as the very first
112/// byte off the wire. The service-router detector keys off this
113/// (and so does the standalone listener path).
114pub const REDWIRE_MAGIC: u8 = 0xFE;
115
116/// Highest minor version the server supports. Wire-bumped as we
117/// add features that change the handshake; data-plane additions
118/// flow through `Hello.features` instead.
119pub const MAX_KNOWN_MINOR_VERSION: u8 = 0x01;
120
121/// Default port for the RedWire listener.
122pub const DEFAULT_REDWIRE_PORT: u16 = 5050;
123
124/// WebSocket subprotocol token advertised by the RedWire-over-WSS edge
125/// (issue #935, ADR 0036). Versioned so a future framing revision can
126/// coexist with v1 clients. reddb-wire is the single source of truth —
127/// the server's axum upgrade handler consumes this constant rather than
128/// defining its own.
129pub const REDWIRE_WS_SUBPROTOCOL: &str = "reddb.redwire.v1";
130
131/// HTTP path the browser client (`wss://host:port` / `red+wss://host:port`) upgrades on to
132/// reach the RedWire-over-WebSocket edge (ADR 0036).
133pub const REDWIRE_WS_PATH: &str = "/redwire";
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum StartupError {
137    BadMagic { got: u8 },
138    UnsupportedMinor { got: u8, max: u8 },
139}
140
141impl std::fmt::Display for StartupError {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            Self::BadMagic { got } => {
145                write!(
146                    f,
147                    "redwire: client did not present magic byte (got 0x{got:02x})"
148                )
149            }
150            Self::UnsupportedMinor { got, max } => {
151                write!(
152                    f,
153                    "redwire: unsupported minor version {got}; max supported is {max}"
154                )
155            }
156        }
157    }
158}
159
160impl std::error::Error for StartupError {}
161
162pub fn client_preface(minor: u8) -> [u8; 2] {
163    [REDWIRE_MAGIC, minor]
164}
165
166pub fn supported_client_preface() -> [u8; 2] {
167    client_preface(MAX_KNOWN_MINOR_VERSION)
168}
169
170pub fn validate_startup_magic(got: u8) -> Result<(), StartupError> {
171    if got == REDWIRE_MAGIC {
172        Ok(())
173    } else {
174        Err(StartupError::BadMagic { got })
175    }
176}
177
178pub fn validate_minor_version(got: u8) -> Result<(), StartupError> {
179    if got <= MAX_KNOWN_MINOR_VERSION {
180        Ok(())
181    } else {
182        Err(StartupError::UnsupportedMinor {
183            got,
184            max: MAX_KNOWN_MINOR_VERSION,
185        })
186    }
187}
188
189/// Outcome of matching an inbound peek buffer against the RedWire magic
190/// discriminator. Mirrors the service-router's three-way detect result
191/// but stays free of router types so reddb-wire owns the classifier.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum RedWireMagicMatch {
194    /// Buffer is empty — need at least one byte before we can classify.
195    Pending,
196    /// First byte is the RedWire magic ([`REDWIRE_MAGIC`]).
197    Match,
198    /// First byte is present and is not the RedWire magic.
199    NoMatch,
200}
201
202/// Classify an inbound peek buffer against the RedWire magic byte
203/// ([`REDWIRE_MAGIC`]). Pure and allocation-free so the service-router's
204/// hot-path classifier can delegate the discriminator decision here:
205/// empty → `Pending`, leading `0xFE` → `Match`, anything else → `NoMatch`.
206pub fn redwire_magic_match(peek: &[u8]) -> RedWireMagicMatch {
207    match peek.first() {
208        None => RedWireMagicMatch::Pending,
209        Some(&first) if first == REDWIRE_MAGIC => RedWireMagicMatch::Match,
210        Some(_) => RedWireMagicMatch::NoMatch,
211    }
212}
213
214#[cfg(test)]
215mod startup_tests {
216    use super::*;
217
218    #[test]
219    fn preface_uses_magic_and_supported_minor() {
220        assert_eq!(supported_client_preface(), [0xfe, MAX_KNOWN_MINOR_VERSION]);
221    }
222
223    #[test]
224    fn startup_validation_rejects_bad_magic_and_future_minor() {
225        assert_eq!(validate_startup_magic(REDWIRE_MAGIC), Ok(()));
226        assert!(matches!(
227            validate_startup_magic(0),
228            Err(StartupError::BadMagic { got: 0 })
229        ));
230        assert_eq!(validate_minor_version(MAX_KNOWN_MINOR_VERSION), Ok(()));
231        assert!(matches!(
232            validate_minor_version(MAX_KNOWN_MINOR_VERSION.saturating_add(1)),
233            Err(StartupError::UnsupportedMinor { .. })
234        ));
235    }
236
237    #[test]
238    fn magic_match_classifies_peek_buffer() {
239        // Empty → Pending: not enough bytes to decide yet.
240        assert_eq!(redwire_magic_match(&[]), RedWireMagicMatch::Pending);
241        // Leading magic → Match, regardless of trailing bytes.
242        assert_eq!(
243            redwire_magic_match(&[REDWIRE_MAGIC]),
244            RedWireMagicMatch::Match
245        );
246        assert_eq!(
247            redwire_magic_match(&[0xFE, 0x01, 0x10]),
248            RedWireMagicMatch::Match
249        );
250        // Any other leading byte (HTTP 'G', H2 'P', binary 0x10) → NoMatch.
251        assert_eq!(redwire_magic_match(b"GET "), RedWireMagicMatch::NoMatch);
252        assert_eq!(redwire_magic_match(&[0x10]), RedWireMagicMatch::NoMatch);
253    }
254}