use crate::{
ClientError, Error, Result,
client::RespLimits,
resp::{
ATTRIBUTE_TAG, MAP_TAG, NULL_TAG, RespTape, RespTapeMut, TAPE_LEN_TAG, is_collection_tag,
parse_int_at, scalar_end,
},
};
use std::fmt;
pub(crate) enum ParsedFrame {
Scalar { at: usize },
Collection(RespTape),
Null,
}
impl fmt::Debug for ParsedFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Scalar { at } => f.debug_struct("Scalar").field("at", at).finish(),
Self::Collection(tape) => f
.debug_struct("Collection")
.field("nodes", &tape.node_count())
.finish(),
Self::Null => f.write_str("Null"),
}
}
}
enum CollectionHeader {
Null { end: usize },
Open { count: usize, end: usize },
}
#[expect(
clippy::arithmetic_side_effects,
reason = "`at` indexes `data` — the read above proves it — so stepping past \
the tag byte stays inside `usize`. The cardinality is doubled in \
`u64`, where a non-negative `i64` times 2 cannot overflow, and capped \
before it is narrowed."
)]
fn parse_collection_header(
data: &[u8],
at: usize,
max_collection_length: usize,
) -> Result<CollectionHeader> {
let tag = *data.get(at).ok_or_else(|| Error::EOF)?;
let (n, end) = parse_int_at(data, at + 1)?;
if n == -1 {
return Ok(CollectionHeader::Null { end });
}
let is_map_shaped = matches!(tag, MAP_TAG | ATTRIBUTE_TAG);
if n < 0 {
return Err(Error::Client(if is_map_shaped {
ClientError::CannotParseMap
} else {
ClientError::CannotParseSequence
}));
}
let multiplier: u64 = if is_map_shaped { 2 } else { 1 };
let wide_count = n.cast_unsigned() * multiplier;
if wide_count > max_collection_length as u64 {
return Err(Error::Client(ClientError::CollectionLengthTooLarge));
}
let Ok(count) = usize::try_from(wide_count) else {
return Err(Error::Client(ClientError::CollectionLengthTooLarge));
};
Ok(CollectionHeader::Open { count, end })
}
#[expect(
clippy::arithmetic_side_effects,
reason = "the guard above returned unless `depth < max_nesting_depth`, so the \
incremented depth is at most that setting."
)]
fn skip_leading_attributes(
data: &[u8],
mut pos: usize,
depth: usize,
limits: &RespLimits,
) -> Result<usize> {
while data.get(pos) == Some(&ATTRIBUTE_TAG) {
if depth >= limits.max_nesting_depth {
return Err(Error::Client(ClientError::MaxNestingDepthExceeded));
}
let CollectionHeader::Open { count, end } =
parse_collection_header(data, pos, limits.max_collection_length)?
else {
return Err(Error::Client(ClientError::CannotParseMap));
};
pos = skip_children(data, end, count, depth + 1, limits)?;
}
Ok(pos)
}
fn skip_children(
data: &[u8],
from: usize,
count: usize,
depth: usize,
limits: &RespLimits,
) -> Result<usize> {
let mut pos = from;
for _ in 0..count {
pos = skip_one_value(data, pos, depth, limits)?;
}
Ok(pos)
}
#[expect(
clippy::arithmetic_side_effects,
reason = "same nesting guard as `skip_leading_attributes`: the increment is \
only reached with `depth < max_nesting_depth`."
)]
fn skip_one_value(data: &[u8], pos: usize, depth: usize, limits: &RespLimits) -> Result<usize> {
let pos = skip_leading_attributes(data, pos, depth, limits)?;
let tag = *data.get(pos).ok_or_else(|| Error::EOF)?;
if is_collection_tag(tag) {
match parse_collection_header(data, pos, limits.max_collection_length)? {
CollectionHeader::Null { end } => Ok(end),
CollectionHeader::Open { count, end } => {
if depth >= limits.max_nesting_depth {
return Err(Error::Client(ClientError::MaxNestingDepthExceeded));
}
skip_children(data, end, count, depth + 1, limits)
}
}
} else {
scalar_end(data, pos, limits.max_bulk_length)
}
}
pub(crate) struct OpenCollection {
tag: u8,
head_index: usize,
remaining: usize,
}
pub(crate) struct RespFrameParser<'a, 'b> {
buf: &'a [u8],
limits: RespLimits,
tape: &'b mut RespTapeMut,
pos: usize,
}
impl<'a, 'b> RespFrameParser<'a, 'b> {
pub(crate) fn new(buf: &'a [u8], tape: &'b mut RespTapeMut) -> Self {
Self::with_limits(buf, tape, RespLimits::DEFAULT)
}
pub(crate) fn with_limits(
buf: &'a [u8],
tape: &'b mut RespTapeMut,
limits: RespLimits,
) -> Self {
Self {
buf,
limits,
tape,
pos: 0,
}
}
pub(crate) fn at(
buf: &'a [u8],
tape: &'b mut RespTapeMut,
pos: usize,
limits: RespLimits,
) -> Self {
Self {
buf,
limits,
tape,
pos,
}
}
#[inline(always)]
pub(crate) fn pos(&self) -> usize {
self.pos
}
pub(crate) fn parse(&mut self) -> Result<(ParsedFrame, usize)> {
let mut stack = Vec::new();
match self.parse_resumable(&mut stack)? {
Some(frame) => Ok((frame, self.pos)),
None => Err(Error::EOF),
}
}
pub(crate) fn parse_resumable(
&mut self,
stack: &mut Vec<OpenCollection>,
) -> Result<Option<ParsedFrame>> {
if !stack.is_empty() {
return self.run_collection_loop(stack);
}
let frame_start = self.pos;
if self.buf.get(self.pos) == Some(&ATTRIBUTE_TAG) {
match skip_leading_attributes(self.buf, self.pos, 0, &self.limits) {
Ok(at) => self.pos = at,
Err(Error::EOF) => {
self.pos = frame_start;
return Ok(None);
}
Err(e) => return Err(e),
}
}
let value_pos = self.pos;
let Some(&tag) = self.buf.get(value_pos) else {
return Ok(None);
};
if is_collection_tag(tag) {
return self.begin_collection(tag, stack);
}
match scalar_end(self.buf, value_pos, self.limits.max_bulk_length) {
Ok(end) => {
self.pos = end;
Ok(Some(ParsedFrame::Scalar { at: value_pos }))
}
Err(Error::EOF) => {
self.pos = value_pos;
Ok(None)
}
Err(e) => Err(e),
}
}
fn begin_collection(
&mut self,
tag: u8,
stack: &mut Vec<OpenCollection>,
) -> Result<Option<ParsedFrame>> {
let at = self.pos;
match parse_collection_header(self.buf, at, self.limits.max_collection_length) {
Ok(CollectionHeader::Null { end }) => {
self.pos = end;
Ok(Some(ParsedFrame::Null))
}
Ok(CollectionHeader::Open { count, end }) => {
debug_assert!(self.tape.is_empty(), "tape must start empty per frame");
let head = self.tape.push(tag, 0);
self.tape.push(TAPE_LEN_TAG, count as u64);
self.pos = end;
stack.push(OpenCollection {
tag,
head_index: head,
remaining: count,
});
self.run_collection_loop(stack)
}
Err(Error::EOF) => {
self.pos = at;
Ok(None)
}
Err(e) => Err(e),
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "a level is only pushed while its parent still had a child to \
fill, and the parent is credited only when that child closes, so \
the parent's `remaining` is non-zero here — the same invariant \
`credit_open_collection` documents."
)]
fn run_collection_loop(
&mut self,
stack: &mut Vec<OpenCollection>,
) -> Result<Option<ParsedFrame>> {
while let Some(remaining) = stack.last().map(|open| open.remaining) {
if remaining == 0 {
let Some(done) = stack.pop() else { break };
let next = self.tape.node_count() as u64;
self.tape.patch(done.head_index, done.tag, next);
if let Some(parent) = stack.last_mut() {
parent.remaining -= 1;
continue;
}
if !is_collection_tag(done.tag) {
return Err(Error::Client(ClientError::Unexpected));
}
return Ok(Some(ParsedFrame::Collection(self.tape.split_freeze())));
}
let child_start = self.pos;
match self.emit_one_child(stack) {
Ok(()) => {}
Err(Error::EOF) => {
self.pos = child_start;
return Ok(None);
}
Err(e) => return Err(e),
}
}
Err(Error::Client(ClientError::Unexpected))
}
#[inline]
fn emit_one_child(&mut self, stack: &mut Vec<OpenCollection>) -> Result<()> {
let mut at = self.pos;
if self.buf.get(at) == Some(&ATTRIBUTE_TAG) {
at = skip_leading_attributes(self.buf, at, stack.len(), &self.limits)?;
}
let tag = *self.buf.get(at).ok_or_else(|| Error::EOF)?;
if is_collection_tag(tag) {
match parse_collection_header(self.buf, at, self.limits.max_collection_length)? {
CollectionHeader::Null { end } => {
self.tape.push(NULL_TAG, at as u64);
self.pos = end;
credit_open_collection(stack);
}
CollectionHeader::Open { count, end } => {
if stack.len() >= self.limits.max_nesting_depth {
return Err(Error::Client(ClientError::MaxNestingDepthExceeded));
}
let head = self.tape.push(tag, 0);
self.tape.push(TAPE_LEN_TAG, count as u64);
self.pos = end;
stack.push(OpenCollection {
tag,
head_index: head,
remaining: count,
});
}
}
} else {
let end = scalar_end(self.buf, at, self.limits.max_bulk_length)?;
self.tape.push(tag, at as u64);
self.pos = end;
credit_open_collection(stack);
}
Ok(())
}
}
#[inline]
#[expect(
clippy::arithmetic_side_effects,
reason = "the innermost level's `remaining` is non-zero here: the collection \
loop is the only caller and it parses a child only when the level \
still has one to fill. Routing the underflow through a `Result` \
instead costs 2-5% on all three collection benches, measured paired \
— this runs once per element."
)]
fn credit_open_collection(stack: &mut [OpenCollection]) {
if let Some(open) = stack.last_mut() {
open.remaining -= 1;
}
}