use crate::{
ClientError, Error, RedisError, Result,
resp::{
ARRAY_TAG, BULK_ERROR_TAG, MAP_TAG, NULL_TAG, PUSH_TAG, ParsedFrame, RespBuf,
RespDeserializer, RespTape, SET_TAG, SIMPLE_ERROR_TAG, SIMPLE_STRING_TAG, ScalarKind,
TapeNode, frame_scalar_value, scalar_span, scalar_value,
},
};
use bytes::Bytes;
use serde::de::DeserializeOwned;
use std::{
fmt::{self, Write as _},
ops::Range,
};
#[derive(Clone, PartialEq)]
pub(crate) enum RespResponse {
Null,
Integer(i64),
#[allow(
dead_code,
reason = "produced by `compact`, which only the client-side cache and the tests build"
)]
Double(f64),
IntegerArray(Vec<i64>),
OwnedArray(Vec<RespResponse>),
Frame {
buf: RespBuf,
tape: RespTape,
root: u32,
},
}
const DEBUG_RENDER_LIMIT: usize = 1000;
impl fmt::Debug for RespResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let truncated = {
let mut writer = TruncatingWriter::new(f, DEBUG_RENDER_LIMIT);
match self.view() {
Ok(view) => write!(writer, "{view:?}")?,
Err(e) => write!(writer, "{:?}", UnreadableElement(e))?,
}
writer.truncated
};
if truncated {
f.write_str("<truncated>")?;
}
Ok(())
}
}
struct TruncatingWriter<'a, 'b> {
inner: &'a mut fmt::Formatter<'b>,
remaining: usize,
truncated: bool,
}
impl<'a, 'b> TruncatingWriter<'a, 'b> {
fn new(inner: &'a mut fmt::Formatter<'b>, limit: usize) -> Self {
Self {
inner,
remaining: limit,
truncated: false,
}
}
}
impl fmt::Write for TruncatingWriter<'_, '_> {
#[expect(
clippy::arithmetic_side_effects,
reason = "the subtraction is inside the `s.len() <= self.remaining` branch, \
and `end` is decremented only while `end > 0`."
)]
fn write_str(&mut self, s: &str) -> fmt::Result {
if s.len() <= self.remaining {
self.remaining -= s.len();
return self.inner.write_str(s);
}
let mut end = self.remaining;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
self.remaining = 0;
self.truncated = true;
self.inner.write_str(&s[..end])
}
}
impl RespResponse {
#[inline(always)]
pub(crate) fn new(buf: RespBuf, parsed: ParsedFrame) -> Self {
match parsed {
ParsedFrame::Scalar { at } => Self::Frame {
buf: if at == 0 {
buf
} else {
RespBuf::from(buf.slice(at..))
},
tape: RespTape::default(),
root: 0,
},
ParsedFrame::Collection(tape) => Self::Frame { buf, tape, root: 0 },
ParsedFrame::Null => Self::Null,
}
}
#[inline(always)]
pub(crate) fn view(&self) -> Result<RespView<'_>> {
match self {
RespResponse::Null => Ok(RespView::Null),
RespResponse::Integer(i) => Ok(RespView::Integer(*i, b"")),
RespResponse::Double(d) => Ok(RespView::Double(*d, b"")),
RespResponse::IntegerArray(a) => Ok(RespView::IntegerArray(a)),
RespResponse::OwnedArray(a) => Ok(RespView::OwnedArray(a)),
RespResponse::Frame { buf, tape, root } => view_at(buf.as_ref(), tape, *root as usize),
}
}
#[inline(always)]
fn frame_tag(&self) -> Option<u8> {
match self {
RespResponse::Frame { buf, tape, root } => {
if tape.is_empty() {
buf.first().copied()
} else {
Some(tape.node(*root as usize).tag())
}
}
_ => None,
}
}
pub(crate) fn retained_bytes(&self) -> usize {
match self {
RespResponse::Frame { buf, .. } => buf.as_ref().len(),
_ => 0,
}
}
#[inline(always)]
pub(crate) fn is_push(&self) -> bool {
self.frame_tag() == Some(PUSH_TAG)
}
#[inline(always)]
pub(crate) fn is_monitor(&self) -> bool {
match self {
RespResponse::Frame { buf, tape, .. } if tape.is_empty() => {
matches!(buf.as_ref(), [SIMPLE_STRING_TAG, second, ..] if second.is_ascii_digit())
}
_ => false,
}
}
#[inline(always)]
pub(crate) fn is_error(&self) -> bool {
matches!(self.frame_tag(), Some(SIMPLE_ERROR_TAG | BULK_ERROR_TAG))
}
#[inline(always)]
pub(crate) fn null() -> RespResponse {
Self::Null
}
#[inline(always)]
pub(crate) fn integer(i: i64) -> RespResponse {
Self::Integer(i)
}
#[inline(always)]
pub(crate) fn integer_array(a: Vec<i64>) -> RespResponse {
Self::IntegerArray(a)
}
#[inline(always)]
pub(crate) fn owned_array(a: Vec<RespResponse>) -> RespResponse {
Self::OwnedArray(a)
}
#[inline(always)]
pub(crate) fn ok() -> RespResponse {
Self::Frame {
buf: RespBuf::from(Bytes::from_static(b"+OK\r\n")),
tape: RespTape::default(),
root: 0,
}
}
#[inline]
pub(crate) fn to<T: DeserializeOwned>(&self) -> Result<T> {
T::deserialize(RespDeserializer::new(self.view()?))
}
#[cfg(any(test, feature = "client-cache"))]
pub(crate) fn compact(&self) -> RespResponse {
match self {
RespResponse::Null => RespResponse::Null,
RespResponse::Integer(i) => RespResponse::Integer(*i),
RespResponse::Double(d) => RespResponse::Double(*d),
RespResponse::IntegerArray(a) => RespResponse::IntegerArray(a.clone()),
RespResponse::OwnedArray(a) => {
RespResponse::OwnedArray(a.iter().map(RespResponse::compact).collect())
}
RespResponse::Frame { buf, tape, .. } if tape.is_empty() => {
let data = buf.as_ref();
match read_frame_view(data) {
Ok(RespView::Integer(i, _)) => RespResponse::Integer(i),
Ok(RespView::Double(d, _)) => RespResponse::Double(d),
Ok(RespView::Null) => RespResponse::Null,
_ => RespResponse::Frame {
buf: RespBuf::from(Bytes::copy_from_slice(data)),
tape: RespTape::default(),
root: 0,
},
}
}
RespResponse::Frame { buf, tape, root } => RespResponse::Frame {
buf: RespBuf::from(Bytes::copy_from_slice(buf.as_ref())),
tape: tape.compact(),
root: *root,
},
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "the guard proves `root` is a collection head, and the parser \
writes a head's element-count node immediately after it, so \
`root + 1` addresses a node that exists."
)]
pub(crate) fn into_collection_iter(self) -> Result<RespResponseIter> {
if self.is_error()
&& let Ok(RespView::Error(message)) = self.view()
{
return Err(Error::Redis(RedisError::try_from(message)?));
}
match self {
RespResponse::Frame { buf, tape, root }
if !tape.is_empty() && tape.node(root as usize).is_collection() =>
{
let root = root as usize;
let len = tape.node(root + 1).payload_index();
Ok(RespResponseIter::new(buf, tape, root, len))
}
_ => Err(Error::Client(ClientError::Unexpected)),
}
}
}
#[inline]
fn view_at<'a>(buf: &'a [u8], tape: &'a RespTape, root: usize) -> Result<RespView<'a>> {
if tape.is_empty() {
return read_frame_view(buf);
}
let node = tape.node(root);
if node.is_collection() {
Ok(collection_view(node.tag(), buf, tape, root))
} else {
read_node_view(node, buf)
}
}
#[inline]
fn read_frame_view(data: &[u8]) -> Result<RespView<'_>> {
let (kind, value) = frame_scalar_value(data)?;
decode_value(kind, data, value)
}
#[inline]
fn read_node_view<'a>(node: TapeNode, data: &'a [u8]) -> Result<RespView<'a>> {
if node.tag() == NULL_TAG {
return Ok(RespView::Null);
}
read_scalar_view(data, node.payload_index())
}
#[inline]
fn read_scalar_view(data: &[u8], off: usize) -> Result<RespView<'_>> {
let (kind, value) = scalar_value(data, off)?;
decode_value(kind, data, value)
}
#[inline]
fn decode_value(kind: ScalarKind, data: &[u8], value: Range<usize>) -> Result<RespView<'_>> {
let value = data
.get(value)
.ok_or_else(|| Error::Client(ClientError::Unexpected))?;
Ok(match kind {
ScalarKind::SimpleString => RespView::SimpleString(value),
ScalarKind::Error => RespView::Error(value),
ScalarKind::Integer => RespView::Integer(
atoi::atoi(value).ok_or_else(|| Error::Client(ClientError::CannotParseInteger))?,
value,
),
ScalarKind::Double => RespView::Double(
fast_float2::parse(value).map_err(|_| Error::Client(ClientError::CannotParseDouble))?,
value,
),
ScalarKind::BulkString => RespView::BulkString(value),
ScalarKind::Boolean => RespView::Boolean(value.first() == Some(&b't')),
ScalarKind::Null => RespView::Null,
})
}
#[inline]
#[expect(
clippy::unreachable,
reason = "invariant: callers gate on `is_collection_tag`, whose `matches!` \
lists exactly these four tags. The arm asserts that pairing; a \
fallback would have to invent a view for a tag that is not a \
collection."
)]
fn collection_view<'a>(tag: u8, buf: &'a [u8], tape: &'a RespTape, root: usize) -> RespView<'a> {
let view = RespCollectionView::new(buf, tape, root);
match tag {
ARRAY_TAG => RespView::Array(view),
MAP_TAG => RespView::Map(view),
SET_TAG => RespView::Set(view),
PUSH_TAG => RespView::Push(view),
_ => unreachable!("collection_view called with a non-collection tag"),
}
}
#[derive(PartialEq)]
pub(crate) enum RespView<'a> {
SimpleString(&'a [u8]),
Integer(i64, &'a [u8]),
Double(f64, &'a [u8]),
BulkString(&'a [u8]),
Boolean(bool),
IntegerArray(&'a [i64]),
OwnedArray(&'a [RespResponse]),
Array(RespCollectionView<'a>),
Map(RespCollectionView<'a>),
Set(RespCollectionView<'a>),
Push(RespCollectionView<'a>),
Error(&'a [u8]),
Null,
}
impl<'a> fmt::Debug for RespView<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SimpleString(arg0) => f
.debug_tuple("SimpleString")
.field(&String::from_utf8_lossy(arg0))
.finish(),
Self::Integer(arg0, _) => f.debug_tuple("Integer").field(arg0).finish(),
Self::Double(arg0, _) => f.debug_tuple("Double").field(arg0).finish(),
Self::BulkString(arg0) => f
.debug_tuple("BulkString")
.field(&String::from_utf8_lossy(arg0))
.finish(),
Self::Boolean(arg0) => f.debug_tuple("Boolean").field(arg0).finish(),
Self::IntegerArray(arg0) => f.debug_tuple("IntegerArray").field(arg0).finish(),
Self::OwnedArray(arg0) => f.debug_tuple("OwnedArray").field(arg0).finish(),
Self::Array(arg0) => f.debug_tuple("Array").field(arg0).finish(),
Self::Map(arg0) => {
f.write_str("Map(")?;
fmt_pairs(f, arg0)?;
f.write_str(")")
}
Self::Set(arg0) => f.debug_tuple("Set").field(arg0).finish(),
Self::Push(arg0) => f.debug_tuple("Push").field(arg0).finish(),
Self::Error(arg0) => f
.debug_tuple("Error")
.field(&String::from_utf8_lossy(arg0))
.finish(),
Self::Null => write!(f, "Null"),
}
}
}
fn fmt_pairs(f: &mut fmt::Formatter<'_>, view: &RespCollectionView<'_>) -> fmt::Result {
let mut map = f.debug_map();
let mut it = view.clone().into_iter();
while let Some(key) = it.next() {
match (key, it.next()) {
(Ok(k), Some(Ok(v))) => map.entry(&k, &v),
(Ok(k), Some(Err(e))) => map.entry(&k, &UnreadableElement(e)),
(Ok(k), None) => map.entry(&k, &format_args!("<missing value>")),
(Err(e), v) => match v {
Some(Ok(v)) => map.entry(&UnreadableElement(e), &v),
Some(Err(e2)) => map.entry(&UnreadableElement(e), &UnreadableElement(e2)),
None => map.entry(&UnreadableElement(e), &format_args!("<missing value>")),
},
};
}
map.finish()
}
struct UnreadableElement(Error);
impl fmt::Debug for UnreadableElement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<unreadable element: {}>", self.0)
}
}
#[derive(Clone, PartialEq)]
pub(crate) struct RespCollectionView<'a> {
buf: &'a [u8],
tape: &'a RespTape,
root: usize,
len: usize,
}
impl<'a> RespCollectionView<'a> {
#[inline(always)]
#[expect(
clippy::arithmetic_side_effects,
reason = "`root` is a collection head, whose element-count node the parser \
writes immediately after it."
)]
pub(crate) fn new(buf: &'a [u8], tape: &'a RespTape, root: usize) -> Self {
let len = tape.node(root + 1).payload_index();
Self {
buf,
tape,
root,
len,
}
}
#[inline(always)]
pub(crate) fn len(&self) -> usize {
self.len
}
}
impl fmt::Debug for RespCollectionView<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
for element in self.clone() {
match element {
Ok(view) => list.entry(&view),
Err(e) => list.entry(&UnreadableElement(e)),
};
}
list.finish()
}
}
impl<'a> IntoIterator for RespCollectionView<'a> {
type Item = Result<RespView<'a>>;
type IntoIter = RespCollectionIter<'a>;
fn into_iter(self) -> Self::IntoIter {
RespCollectionIter::new(self.buf, self.tape, self.root, self.len)
}
}
pub(crate) struct RespCollectionIter<'a> {
buf: &'a [u8],
tape: &'a RespTape,
cursor: usize,
remaining: usize,
}
impl<'a> RespCollectionIter<'a> {
#[inline(always)]
#[expect(
clippy::arithmetic_side_effects,
reason = "the first child of a collection sits two nodes past its head — \
the head, then its element count — both written by the parser."
)]
pub(crate) fn new(buf: &'a [u8], tape: &'a RespTape, root: usize, len: usize) -> Self {
Self {
buf,
tape,
cursor: root + 2,
remaining: len,
}
}
#[inline(always)]
pub(crate) fn len(&self) -> usize {
self.remaining
}
#[inline(always)]
pub(crate) fn has_next(&self) -> bool {
self.remaining > 0
}
}
impl<'a> Iterator for RespCollectionIter<'a> {
type Item = Result<RespView<'a>>;
#[expect(
clippy::arithmetic_side_effects,
reason = "the zero check above is what makes the decrement safe, and \
`cursor` steps to the next node of a tape it was built over."
)]
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
let node = self.tape.node(self.cursor);
let tag = node.tag();
if node.is_collection() {
let root = self.cursor;
self.cursor = node.payload_index();
self.remaining -= 1;
Some(Ok(collection_view(tag, self.buf, self.tape, root)))
} else {
match read_node_view(node, self.buf) {
Ok(view) => {
self.cursor += 1;
self.remaining -= 1;
Some(Ok(view))
}
Err(e) => {
self.remaining = 0;
Some(Err(e))
}
}
}
}
}
pub(crate) struct RespResponseIter {
buf: RespBuf,
tape: RespTape,
cursor: usize,
remaining: usize,
}
impl RespResponseIter {
#[expect(
clippy::arithmetic_side_effects,
reason = "same tape layout as `RespCollectionIter::new`: head, element \
count, then the first child."
)]
pub(crate) fn new(buf: RespBuf, tape: RespTape, root: usize, len: usize) -> Self {
Self {
buf,
tape,
cursor: root + 2,
remaining: len,
}
}
}
impl Iterator for RespResponseIter {
type Item = Result<RespResponse>;
#[expect(
clippy::arithmetic_side_effects,
reason = "same as `RespCollectionIter::next`: the zero check above guards \
the decrement, and `cursor` walks a tape this iterator owns."
)]
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
let node = self.tape.node(self.cursor);
if node.is_collection() {
let root = self.cursor;
self.cursor = node.payload_index();
self.remaining -= 1;
let Ok(root) = u32::try_from(root) else {
self.remaining = 0;
return Some(Err(Error::Client(ClientError::Unexpected)));
};
return Some(Ok(RespResponse::Frame {
buf: self.buf.clone(),
tape: self.tape.clone(),
root,
}));
}
self.cursor += 1;
self.remaining -= 1;
if node.tag() == NULL_TAG {
return Some(Ok(RespResponse::Null));
}
let at = node.payload_index();
let data = self.buf.as_ref();
match scalar_span(data, at) {
Ok(span) => Some(Ok(RespResponse::Frame {
buf: RespBuf::from(self.buf.slice(span)),
tape: RespTape::default(),
root: 0,
})),
Err(e) => {
self.remaining = 0;
Some(Err(e))
}
}
}
}