Skip to main content

actordb_protocol/
client_decode.rs

1//! Decoded-object admission, separate from encoded framing.
2//!
3//! The budget, header/payload and pending-shape walk is allocation-free and
4//! precedes all message/collection materialization. The existing typed receipt
5//! validator can allocate bounded error metadata on malformed receipts; that
6//! rejection does not materialize their decoded messages or payloads.
7
8use std::mem::size_of;
9
10use crate::{client, meta, write_concern, MAX_BATCH_ITEMS, MAX_FRAME_BYTES};
11
12/// Default accounted response allowance, including raw framing, decoded
13/// collections/payloads and portable conversion work. Not an allocator/RSS cap.
14pub const DEFAULT_MAX_DECODED_RESPONSE_BYTES: usize = 64 * 1024 * 1024;
15/// Conservative portable QueryValue header allowance. SDK tests check that
16/// their actual layout fits this charge without coupling the wire crate to SQL.
17pub const PORTABLE_CELL_HEADER_CHARGE: usize = 64;
18const MAP_SLOT_CHARGE: usize = 128;
19const VALIDATION_SLOT_CHARGE: usize = 64;
20const MAX_WRITER_BYTES: usize = 255;
21
22/// Work admitted before Prost or SDK receipt/result conversion allocates.
23/// Header charges include Vec geometric growth/minimum capacity and conservative
24/// map/portable-conversion storage. Payloads are charged twice for decoded and
25/// temporary validation copies. Caller-retained returned results, allocator
26/// slack, transport buffers and outgoing request storage are outside this scope.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[non_exhaustive]
29pub struct ClientDecodeReport {
30    pub accounted_bytes: usize,
31    pub raw_bytes: usize,
32    pub header_bytes: usize,
33    pub payload_bytes: usize,
34    pub query_cells: usize,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38enum Operation {
39    Read(usize),
40    Mutation(usize),
41    Query,
42    Inspect,
43    Scan,
44    Unsupported,
45}
46
47/// Expectations derived from the actual pending application request, never
48/// from data echoed by the server. Use optional query shape from a trusted
49/// locally parsed plan; the protocol preflight does not parse SQL.
50#[derive(Debug, Clone, Copy)]
51pub struct ClientResponseShape {
52    id: u64,
53    operation: Operation,
54    columns: Option<usize>,
55    rows: usize,
56    aggregate: bool,
57}
58impl ClientResponseShape {
59    pub fn from_request(request: &client::Request) -> Self {
60        use client::request::Operation as Request;
61        let operation = match &request.operation {
62            Some(Request::Get(_)) => Operation::Read(1),
63            Some(Request::BatchGet(batch)) => Operation::Read(batch.keys.len()),
64            Some(Request::Put(_) | Request::Delete(_)) => Operation::Mutation(1),
65            Some(Request::BatchPut(batch)) => Operation::Mutation(batch.puts.len()),
66            Some(Request::Query(_)) => Operation::Query,
67            Some(Request::Inspect(_)) => Operation::Inspect,
68            Some(Request::Scan(_)) => Operation::Scan,
69            None => Operation::Unsupported,
70        };
71        Self {
72            id: request.id,
73            operation,
74            columns: None,
75            rows: 100_000,
76            aggregate: false,
77        }
78    }
79
80    /// Refine a query using its validated pending plan. Invalid refinements
81    /// fail instead of becoming a less restrictive expectation.
82    pub fn with_query_shape(
83        mut self,
84        columns: usize,
85        limit: Option<u64>,
86        aggregate: bool,
87    ) -> Result<Self, &'static str> {
88        if self.operation != Operation::Query
89            || !(1..=32).contains(&columns)
90            || limit.is_some_and(|limit| limit > 100_000)
91        {
92            return Err("invalid expected query shape");
93        }
94        self.columns = Some(columns);
95        self.rows = if aggregate {
96            1
97        } else {
98            limit.unwrap_or(100_000) as usize
99        };
100        self.aggregate = aggregate;
101        Ok(self)
102    }
103}
104
105/// Validate before message/collection materialization using the default budget.
106/// The accounting/shape pass allocates no owned data. Malformed typed receipt
107/// validation can allocate a bounded error string before returning rejection.
108pub fn preflight_client_frame(bytes: &[u8]) -> Result<(), &'static str> {
109    preflight_client_frame_with_budget(bytes, DEFAULT_MAX_DECODED_RESPONSE_BYTES).map(|_| ())
110}
111
112/// As [`preflight_client_frame`], with an explicit accounted allowance and report.
113/// Successful preflight does not materialize messages, collections or payloads.
114pub fn preflight_client_frame_with_budget(
115    bytes: &[u8],
116    max_accounted_bytes: usize,
117) -> Result<ClientDecodeReport, &'static str> {
118    preflight(bytes, max_accounted_bytes, None, None)
119}
120
121/// Exact message expected by the current application handshake/exchange phase.
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123#[non_exhaustive]
124pub enum ClientFrameKind {
125    Hello,
126    Auth,
127    Request,
128    Response,
129}
130
131pub fn preflight_client_frame_kind(
132    bytes: &[u8],
133    max_accounted_bytes: usize,
134    kind: ClientFrameKind,
135) -> Result<ClientDecodeReport, &'static str> {
136    preflight(bytes, max_accounted_bytes, None, Some(kind))
137}
138
139/// Additionally validate the actual pending request before materialization.
140/// The same bounded malformed-receipt error-metadata caveat applies.
141pub fn preflight_client_response(
142    bytes: &[u8],
143    max_accounted_bytes: usize,
144    shape: &ClientResponseShape,
145) -> Result<ClientDecodeReport, &'static str> {
146    preflight(
147        bytes,
148        max_accounted_bytes,
149        Some(shape),
150        Some(ClientFrameKind::Response),
151    )
152}
153
154fn preflight(
155    bytes: &[u8],
156    maximum: usize,
157    shape: Option<&ClientResponseShape>,
158    kind: Option<ClientFrameKind>,
159) -> Result<ClientDecodeReport, &'static str> {
160    if bytes.is_empty() || bytes.len() > MAX_FRAME_BYTES {
161        return Err("invalid application frame size");
162    }
163    if bytes.len() > maximum {
164        return Err("decoded application response exceeds admission allowance");
165    }
166    if let Some(expected) = kind {
167        if envelope_kind(bytes)? != expected {
168            return Err("unexpected application message kind");
169        }
170    }
171    let mut budget = Budget {
172        maximum,
173        report: ClientDecodeReport {
174            accounted_bytes: bytes.len(),
175            raw_bytes: bytes.len(),
176            header_bytes: 0,
177            payload_bytes: 0,
178            query_cells: 0,
179        },
180        columns: 0,
181        rows: 0,
182        wrong_row_width: false,
183    };
184    budget.charge(0, false)?;
185    walk(bytes, Context::Frame, &mut budget, shape)?;
186    // Preserve every original envelope/oneof/receipt structural constraint.
187    // Admission runs first so dense rejected frames need not traverse every
188    // repeated nested message before the aggregate budget can stop the work.
189    crate::validate_client_frame_structure(bytes)?;
190    Ok(budget.report)
191}
192
193struct Budget {
194    maximum: usize,
195    report: ClientDecodeReport,
196    columns: usize,
197    rows: usize,
198    wrong_row_width: bool,
199}
200impl Budget {
201    fn charge(&mut self, bytes: usize, payload: bool) -> Result<(), &'static str> {
202        self.report.accounted_bytes = self
203            .report
204            .accounted_bytes
205            .checked_add(bytes)
206            .ok_or("decoded application accounting overflow")?;
207        if self.report.accounted_bytes > self.maximum {
208            return Err("decoded application response exceeds admission allowance");
209        }
210        let counter = if payload {
211            &mut self.report.payload_bytes
212        } else {
213            &mut self.report.header_bytes
214        };
215        *counter = counter
216            .checked_add(bytes)
217            .ok_or("decoded application accounting overflow")?;
218        Ok(())
219    }
220    fn payload(&mut self, bytes: usize) -> Result<(), &'static str> {
221        self.charge(
222            bytes
223                .max(if bytes == 0 { 0 } else { 8 })
224                .checked_mul(2)
225                .ok_or("decoded payload accounting overflow")?,
226            true,
227        )
228    }
229    fn repeated(&mut self, count: usize, element: usize) -> Result<(), &'static str> {
230        // Current Vec push growth: a non-byte collection starts at four slots,
231        // then doubles. Charge its capacity, not only its populated elements.
232        let capacity = count
233            .checked_next_power_of_two()
234            .ok_or("decoded collection overflow")?
235            .max(4);
236        let previous = if count == 1 {
237            0
238        } else {
239            (count - 1)
240                .checked_next_power_of_two()
241                .ok_or("decoded collection overflow")?
242                .max(4)
243        };
244        self.charge(
245            (capacity - previous)
246                .checked_mul(element)
247                .ok_or("decoded collection overflow")?,
248            false,
249        )
250    }
251}
252
253#[derive(Clone, Copy, PartialEq, Eq)]
254enum Context {
255    Frame,
256    Hello,
257    Auth,
258    Request,
259    Get,
260    Put,
261    Delete,
262    BatchGet,
263    BatchPut,
264    Query,
265    Inspect,
266    Scan,
267    Response,
268    Error,
269    Value,
270    Read,
271    Mutation,
272    QueryResult,
273    Row,
274    Cell,
275    Inspection,
276    Health,
277    Placement,
278    MapEntry,
279    ScanMatch,
280    Receipt,
281    Evidence,
282    OperationEvidence,
283    Acknowledgement,
284}
285impl Context {
286    fn size(self) -> usize {
287        use Context::*;
288        match self {
289            Frame => size_of::<client::Frame>(),
290            Hello => size_of::<client::Hello>(),
291            Auth => size_of::<client::Auth>(),
292            Request => size_of::<client::Request>(),
293            Get => size_of::<client::Get>(),
294            Put => size_of::<client::Put>(),
295            Delete => size_of::<client::Delete>(),
296            BatchGet => size_of::<client::BatchGet>(),
297            BatchPut => size_of::<client::BatchPut>(),
298            Query => size_of::<client::Query>(),
299            Inspect => size_of::<client::Inspect>(),
300            Scan => size_of::<client::Scan>(),
301            Response => size_of::<client::Response>(),
302            Error => size_of::<client::Error>(),
303            Value => size_of::<client::Value>(),
304            Read => size_of::<client::ReadResult>(),
305            Mutation => size_of::<client::MutationResult>(),
306            QueryResult => size_of::<client::QueryResult>(),
307            Row => size_of::<meta::QueryPageRow>(),
308            Cell => size_of::<meta::QueryCell>(),
309            Inspection => size_of::<client::Inspection>(),
310            Health => size_of::<client::FragmentHealth>(),
311            Placement => size_of::<client::Placement>(),
312            MapEntry => size_of::<(String, u64)>(),
313            ScanMatch => size_of::<meta::ScanMatch>(),
314            Receipt => size_of::<meta::WriteConcernResult>(),
315            Evidence => size_of::<meta::WriteConcernEvidence>(),
316            OperationEvidence => size_of::<meta::WriteOperationEvidence>(),
317            Acknowledgement => size_of::<meta::WriteAcknowledgement>(),
318        }
319    }
320}
321
322#[derive(Clone, Copy)]
323enum Field {
324    Scalar,
325    Text,
326    Bytes,
327    Message(Context),
328    Texts {
329        limit: usize,
330        extra: usize,
331    },
332    Messages {
333        context: Context,
334        limit: usize,
335        extra: usize,
336    },
337    Map {
338        limit: usize,
339    },
340    ReceiptBytes,
341}
342fn schema(context: Context, field: u64) -> Option<Field> {
343    use Context::*;
344    use Field::{Bytes, Map, Message, Messages, ReceiptBytes, Scalar, Text, Texts};
345    let strings = |limit| Texts { limit, extra: 0 };
346    let messages = |context, limit| Messages {
347        context,
348        limit,
349        extra: 0,
350    };
351    Some(match (context, field) {
352        (Frame, 1) => Message(Hello),
353        (Frame, 2) => Message(Auth),
354        (Frame, 3) => Message(Request),
355        (Frame, 4) => Message(Response),
356        (Hello, 1) => Text,
357        (Hello, 2) | (Auth, 1) => Bytes,
358        (Hello, 3) => Scalar,
359        (Request, 1) => Scalar,
360        (Request, 10) => Text,
361        (Request, 2) => Message(Get),
362        (Request, 3) => Message(Put),
363        (Request, 4) => Message(Delete),
364        (Request, 5) => Message(BatchGet),
365        (Request, 6) => Message(BatchPut),
366        (Request, 7) => Message(Query),
367        (Request, 8) => Message(Inspect),
368        (Request, 9) => Message(Scan),
369        (Get, 1) | (Put, 1) | (Delete, 1) => Text,
370        (Get, 2) | (Put, 3 | 4) | (Delete, 2) => Scalar,
371        (Put, 2 | 5) | (Delete, 3) => Bytes,
372        (BatchGet, 1) => strings(MAX_BATCH_ITEMS),
373        (BatchGet | BatchPut, 2) => Scalar,
374        (BatchPut, 1) => messages(Put, MAX_BATCH_ITEMS),
375        (Query, 1) => Text,
376        (Query, 2) => Messages {
377            context: Cell,
378            limit: MAX_BATCH_ITEMS,
379            extra: PORTABLE_CELL_HEADER_CHARGE,
380        },
381        (Scan, 1 | 3 | 4 | 5 | 6) => Scalar,
382        (Scan, 2) => Bytes,
383        (Response, 1) => Scalar,
384        (Response, 2) => Message(Error),
385        (Response, 3) => messages(Read, MAX_BATCH_ITEMS),
386        (Response, 4) => messages(Mutation, MAX_BATCH_ITEMS),
387        (Response, 5) => Message(QueryResult),
388        (Response, 6) => Message(Inspection),
389        (Response, 7) => messages(ScanMatch, 100_000),
390        (Error, 1 | 3 | 5 | 6) => Scalar,
391        (Error, 2 | 4) => Text,
392        (Value, 1) => Bytes,
393        (Value, 2 | 4 | 5) => Scalar,
394        (Value, 3) => Text,
395        (Read, 1) | (Mutation, 1) => Text,
396        (Read, 2) => Message(Value),
397        (Read, 3) | (Mutation, 2) => Message(Error),
398        (Mutation, 3) => ReceiptBytes,
399        (QueryResult, 1) => strings(32),
400        (QueryResult, 2) => Messages {
401            context: Row,
402            limit: 100_000,
403            extra: size_of::<Vec<()>>(),
404        },
405        (Row, 1) => Text,
406        (Row, 2) => Messages {
407            context: Cell,
408            limit: 32,
409            extra: PORTABLE_CELL_HEADER_CHARGE,
410        },
411        (Cell, 1) => Scalar,
412        (Cell, 2 | 4) => Text,
413        (Cell, 3) => Bytes,
414        (Inspection, 1 | 2) => Text,
415        (Inspection, 3 | 6) => Scalar,
416        (Inspection, 4) => messages(Health, 100_000),
417        (Inspection, 5) => messages(Placement, 100_000),
418        (Health, 1 | 2 | 6 | 7 | 8) => Scalar,
419        (Health, 3) => strings(MAX_BATCH_ITEMS),
420        (Health, 4) => Map {
421            limit: MAX_BATCH_ITEMS,
422        },
423        (Placement, 1) => Scalar,
424        (Placement, 2) => strings(MAX_BATCH_ITEMS),
425        (Placement, 3) => Text,
426        (MapEntry, 1) => Text,
427        (MapEntry, 2) => Scalar,
428        (ScanMatch, 1) => Text,
429        (ScanMatch, 2) => Bytes,
430        (Receipt, 1 | 3 | 4 | 5 | 11) => Scalar,
431        (Receipt, 2 | 7 | 8) => Text,
432        (Receipt, 6 | 9) => Texts {
433            limit: write_concern::MAX_WRITE_CONCERN_REPLICAS,
434            extra: VALIDATION_SLOT_CHARGE,
435        },
436        (Receipt, 10) => Message(Evidence),
437        (Evidence, 1) => Message(OperationEvidence),
438        (Evidence, 3) => Bytes,
439        (Evidence, 2) => Texts {
440            limit: write_concern::MAX_WRITE_CONCERN_REPLICAS,
441            extra: VALIDATION_SLOT_CHARGE,
442        },
443        (Evidence, 4) => Messages {
444            context: Acknowledgement,
445            limit: write_concern::MAX_WRITE_CONCERN_REPLICAS,
446            extra: size_of::<write_concern::WriteAcknowledgement>() + MAX_WRITER_BYTES,
447        },
448        (OperationEvidence, 1 | 4 | 5 | 7 | 8) | (Acknowledgement, 1 | 2) => Scalar,
449        (OperationEvidence, 2 | 3 | 9) => Bytes,
450        (OperationEvidence, 6) => Text,
451        _ => return None,
452    })
453}
454
455fn varint(bytes: &[u8], position: &mut usize) -> Result<u64, &'static str> {
456    let mut result = 0;
457    for shift in (0..70).step_by(7) {
458        let byte = *bytes.get(*position).ok_or("truncated varint")?;
459        *position += 1;
460        if shift == 63 && byte > 1 {
461            return Err("overflowing varint");
462        }
463        result |= u64::from(byte & 127) << shift;
464        if byte & 128 == 0 {
465            return Ok(result);
466        }
467    }
468    Err("overflowing varint")
469}
470
471fn skip(bytes: &[u8], position: &mut usize, wire: u64) -> Result<(), &'static str> {
472    let length = match wire {
473        0 => {
474            varint(bytes, position)?;
475            return Ok(());
476        }
477        1 => 8,
478        5 => 4,
479        2 => usize::try_from(varint(bytes, position)?).map_err(|_| "overflow")?,
480        _ => return Err("invalid wire type"),
481    };
482    *position = position.checked_add(length).ok_or("overflow")?;
483    if *position > bytes.len() {
484        return Err("truncated field");
485    }
486    Ok(())
487}
488fn envelope_kind(bytes: &[u8]) -> Result<ClientFrameKind, &'static str> {
489    let mut position = 0;
490    let mut kind = None;
491    while position < bytes.len() {
492        let tag = varint(bytes, &mut position)?;
493        let next = match tag >> 3 {
494            1 => Some(ClientFrameKind::Hello),
495            2 => Some(ClientFrameKind::Auth),
496            3 => Some(ClientFrameKind::Request),
497            4 => Some(ClientFrameKind::Response),
498            0 => return Err("invalid field"),
499            _ => None,
500        };
501        if let Some(next) = next {
502            if kind.replace(next).is_some() || tag & 7 != 2 {
503                return Err("duplicate or invalid message");
504            }
505        }
506        skip(bytes, &mut position, tag & 7)?;
507    }
508    kind.ok_or("missing message")
509}
510fn response_id(bytes: &[u8]) -> Result<u64, &'static str> {
511    let mut position = 0;
512    let mut id = 0;
513    while position < bytes.len() {
514        let tag = varint(bytes, &mut position)?;
515        if tag >> 3 == 1 {
516            if tag & 7 != 0 {
517                return Err("invalid response id wire type");
518            }
519            id = varint(bytes, &mut position)?;
520        } else {
521            skip(bytes, &mut position, tag & 7)?;
522        }
523    }
524    Ok(id)
525}
526
527fn walk(
528    bytes: &[u8],
529    context: Context,
530    budget: &mut Budget,
531    shape: Option<&ClientResponseShape>,
532) -> Result<(), &'static str> {
533    if context == Context::Response {
534        if let Some(shape) = shape {
535            if response_id(bytes)? != shape.id {
536                return Err("response id does not match pending request");
537            }
538        }
539    }
540    budget.charge(context.size(), false)?;
541    let mut position = 0;
542    let mut counts = [0usize; 12];
543    while position < bytes.len() {
544        let tag = varint(bytes, &mut position)?;
545        let field = tag >> 3;
546        if field == 0 {
547            return Err("invalid field");
548        }
549        let kind = schema(context, field);
550        if let Some(kind) = kind {
551            let expected_wire = if matches!(kind, Field::Scalar) { 0 } else { 2 };
552            if tag & 7 != expected_wire {
553                return Err("invalid known application field wire type");
554            }
555            let count = counts
556                .get_mut(field as usize)
557                .ok_or("invalid known field")?;
558            *count = count.checked_add(1).ok_or("decoded collection overflow")?;
559            let repeated = match kind {
560                Field::Texts { limit, extra } => Some((limit, size_of::<String>() + extra)),
561                Field::Messages {
562                    context,
563                    limit,
564                    extra,
565                } => Some((limit, context.size() + extra)),
566                Field::Map { limit } => Some((limit, MAP_SLOT_CHARGE)),
567                _ => None,
568            };
569            if let Some((limit, element)) = repeated {
570                if *count > limit {
571                    return Err("invalid or excessive repeated field");
572                }
573                budget.repeated(*count, element)?;
574            }
575            if context == Context::Row && field == 2 {
576                budget.report.query_cells = budget
577                    .report
578                    .query_cells
579                    .checked_add(1)
580                    .ok_or("decoded cell overflow")?;
581            }
582        }
583        match tag & 7 {
584            0 => {
585                varint(bytes, &mut position)?;
586            }
587            1 => {
588                position = position.checked_add(8).ok_or("overflow")?;
589            }
590            5 => {
591                position = position.checked_add(4).ok_or("overflow")?;
592            }
593            2 => {
594                let length =
595                    usize::try_from(varint(bytes, &mut position)?).map_err(|_| "overflow")?;
596                let end = position.checked_add(length).ok_or("overflow")?;
597                let inner = bytes.get(position..end).ok_or("truncated field")?;
598                match kind {
599                    Some(Field::Text | Field::Texts { .. }) => {
600                        std::str::from_utf8(inner).map_err(|_| "invalid application UTF-8")?;
601                        budget.payload(inner.len())?;
602                    }
603                    Some(Field::Bytes) => budget.payload(inner.len())?,
604                    Some(
605                        Field::Message(nested)
606                        | Field::Messages {
607                            context: nested, ..
608                        },
609                    ) => {
610                        walk(inner, nested, budget, shape)?;
611                    }
612                    Some(Field::Map { .. }) => walk(inner, Context::MapEntry, budget, shape)?,
613                    Some(Field::ReceiptBytes) => {
614                        budget.payload(inner.len())?;
615                        // Opaque application receipt bytes are subsequently decoded
616                        // by the SDK. Admit that nested materialization as well.
617                        walk(inner, Context::Receipt, budget, shape)?;
618                    }
619                    _ => {}
620                }
621                position = end;
622            }
623            _ => return Err("invalid wire type"),
624        }
625        if position > bytes.len() {
626            return Err("truncated field");
627        }
628    }
629    if context == Context::Row {
630        budget.wrong_row_width |= shape
631            .and_then(|shape| shape.columns)
632            .is_some_and(|columns| counts[2] != columns);
633    }
634    if context == Context::QueryResult {
635        budget.columns = counts[1];
636        budget.rows = counts[2];
637    }
638    if context == Context::Frame && shape.is_some() && counts[4] != 1 {
639        return Err("expected pending application response");
640    }
641    if context == Context::Response && counts[2] == 0 {
642        if let Some(shape) = shape {
643            let (reads, mutations, query, inspection, scans) = match shape.operation {
644                Operation::Read(reads) => (reads, 0, 0, 0, false),
645                Operation::Mutation(mutations) => (0, mutations, 0, 0, false),
646                Operation::Query => (0, 0, 1, 0, false),
647                Operation::Inspect => (0, 0, 0, 1, false),
648                Operation::Scan => (0, 0, 0, 0, true),
649                Operation::Unsupported => (0, 0, 0, 0, false),
650            };
651            if counts[3] != reads
652                || counts[4] != mutations
653                || counts[5] != query
654                || counts[6] != inspection
655                || (!scans && counts[7] != 0)
656                || (query == 1
657                    && (shape
658                        .columns
659                        .is_some_and(|columns| columns != budget.columns)
660                        || budget.rows > shape.rows
661                        || (shape.aggregate && budget.rows != 1)
662                        || budget.wrong_row_width))
663            {
664                return Err("decoded response does not match pending request shape");
665            }
666        }
667    }
668    Ok(())
669}