use toasty_core::stmt::Value;
use topcoat::Result;
use crate::error::TabloError;
const VERSION: u8 = 1;
const TAG_NULL: u8 = b'n';
const TAG_BOOL: u8 = b'b';
const TAG_I8: u8 = b'1';
const TAG_I16: u8 = b'2';
const TAG_I32: u8 = b'4';
const TAG_I64: u8 = b'8';
const TAG_U8: u8 = b'A';
const TAG_U16: u8 = b'B';
const TAG_U32: u8 = b'C';
const TAG_U64: u8 = b'D';
const TAG_F32: u8 = b'f';
const TAG_F64: u8 = b'g';
const TAG_STRING: u8 = b's';
const TAG_UUID: u8 = b'u';
const TAG_TIMESTAMP: u8 = b't';
const TAG_DATE: u8 = b'd';
const TAG_DATETIME: u8 = b'm';
const TAG_TIME: u8 = b'i';
const TAG_BYTES: u8 = b'x';
const TAG_ZONED: u8 = b'z';
const TAG_RECORD: u8 = b'r';
pub(crate) fn encode(value: &Value) -> Result<String> {
let mut payload = vec![VERSION];
write_value(value, &mut payload)?;
Ok(hex_encode(&payload))
}
fn malformed(message: impl Into<String>) -> topcoat::Error {
TabloError::Cursor(message.into()).into()
}
fn unencodable(message: impl Into<String>) -> topcoat::Error {
crate::error::declaration(message)
}
pub(crate) fn rejected(error: &topcoat::Error) -> topcoat::Error {
TabloError::CursorRejected(error.to_string()).into()
}
pub(crate) fn decode(token: &str) -> Result<Value> {
let payload = hex_decode(token)?;
let mut buf = &payload[..];
let version = take::<1>(&mut buf)?[0];
if version != VERSION {
return Err(malformed(format!("cursor: unsupported version {version}")));
}
let (value, rest) = read_value_with_depth(buf, 0)?;
if !rest.is_empty() {
return Err(malformed("cursor: trailing bytes after value"));
}
Ok(value)
}
const MAX_CURSOR_DEPTH: usize = 16;
macro_rules! cursor_tags {
(
bytes: $( $btag:ident => $bv:ident($bty:ty) ),* $(,)? ;
text: $( $ttag:ident => $tv:ident($tty:ty) as $tlabel:literal ),* $(,)? ;
) => {
fn write_tagged(value: &Value, out: &mut Vec<u8>) -> Result<bool> {
match value {
$(
Value::$bv(v) => {
out.push($btag);
out.extend_from_slice(&v.to_le_bytes());
}
)*
$(
Value::$tv(v) => {
out.push($ttag);
write_len_prefixed(v.to_string().as_bytes(), out)?;
}
)*
_ => return Ok(false),
}
Ok(true)
}
fn read_tagged(tag: u8, buf: &mut &[u8]) -> Result<Option<Value>> {
Ok(Some(match tag {
$(
$btag => Value::$bv(<$bty>::from_le_bytes(
take::<{ std::mem::size_of::<$bty>() }>(buf)?,
)),
)*
$(
$ttag => {
let s = read_len_prefixed(buf)?;
let text = std::str::from_utf8(&s).map_err(|e| {
malformed(format!("cursor: invalid {}: {e}", $tlabel))
})?;
Value::$tv(text.parse::<$tty>().map_err(|e| {
malformed(format!("cursor: invalid {}: {e}", $tlabel))
})?)
}
)*
_ => return Ok(None),
}))
}
};
}
cursor_tags! {
bytes: TAG_I8 => I8(i8), TAG_I16 => I16(i16), TAG_I32 => I32(i32), TAG_I64 => I64(i64),
TAG_U8 => U8(u8), TAG_U16 => U16(u16), TAG_U32 => U32(u32), TAG_U64 => U64(u64),
TAG_F32 => F32(f32), TAG_F64 => F64(f64);
text: TAG_TIMESTAMP => Timestamp(jiff::Timestamp) as "timestamp",
TAG_DATE => Date(jiff::civil::Date) as "date",
TAG_DATETIME => DateTime(jiff::civil::DateTime) as "datetime",
TAG_TIME => Time(jiff::civil::Time) as "time",
TAG_ZONED => Zoned(jiff::Zoned) as "zoned";
}
fn write_value(value: &Value, out: &mut Vec<u8>) -> Result<()> {
if write_tagged(value, out)? {
return Ok(());
}
match value {
Value::Null => out.push(TAG_NULL),
Value::Bool(b) => {
out.push(TAG_BOOL);
out.push(u8::from(*b));
}
Value::String(v) => {
out.push(TAG_STRING);
write_len_prefixed(v.as_bytes(), out)?;
}
Value::Uuid(v) => {
out.push(TAG_UUID);
out.extend_from_slice(v.as_bytes());
}
Value::Bytes(v) => {
out.push(TAG_BYTES);
write_len_prefixed(v, out)?;
}
Value::Record(record) => {
out.push(TAG_RECORD);
out.extend_from_slice(
&u32::try_from(record.fields.len())
.map_err(|e| unencodable(format!("cursor: record too long: {e}")))?
.to_le_bytes(),
);
for field in &record.fields {
write_value(field, out)?;
}
}
other => {
return Err(unencodable(format!("cursor: unsupported value {other:?}")));
}
}
Ok(())
}
fn read_value_with_depth(buf: &[u8], depth: usize) -> Result<(Value, &[u8])> {
if depth > MAX_CURSOR_DEPTH {
return Err(malformed("cursor: record nesting too deep"));
}
let mut buf = buf;
let tag = take::<1>(&mut buf)?[0];
if let Some(value) = read_tagged(tag, &mut buf)? {
return Ok((value, buf));
}
match tag {
TAG_NULL => Ok((Value::Null, buf)),
TAG_BOOL => {
let b = take::<1>(&mut buf)?[0];
match b {
0 => Ok((Value::Bool(false), buf)),
1 => Ok((Value::Bool(true), buf)),
_ => Err(malformed("cursor: invalid bool byte")),
}
}
TAG_STRING => {
let s = read_len_prefixed(&mut buf)?;
Ok((
Value::String(
String::from_utf8(s)
.map_err(|e| malformed(format!("cursor: invalid utf-8 string: {e}")))?,
),
buf,
))
}
TAG_UUID => {
let bytes = take::<16>(&mut buf)?;
Ok((
Value::Uuid(
uuid::Uuid::from_slice(&bytes)
.map_err(|e| malformed(format!("cursor: invalid uuid: {e}")))?,
),
buf,
))
}
TAG_BYTES => {
let s = read_len_prefixed(&mut buf)?;
Ok((Value::Bytes(s), buf))
}
TAG_RECORD => {
let count = u32::from_le_bytes(take::<4>(&mut buf)?) as usize;
let mut fields = Vec::with_capacity(count.min(64));
for _ in 0..count {
let (field, rest) = read_value_with_depth(buf, depth + 1)?;
buf = rest;
fields.push(field);
}
Ok((
Value::Record(toasty_core::stmt::ValueRecord::from_vec(fields)),
buf,
))
}
_ => Err(malformed(format!("cursor: unknown tag byte {tag:#04x}"))),
}
}
fn write_len_prefixed(bytes: &[u8], out: &mut Vec<u8>) -> Result<()> {
let len = u32::try_from(bytes.len())
.map_err(|e| unencodable(format!("cursor: value too long: {e}")))?;
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(bytes);
Ok(())
}
fn read_len_prefixed(buf: &mut &[u8]) -> Result<Vec<u8>> {
let len = u32::from_le_bytes(take::<4>(buf)?) as usize;
Ok(take_slice(buf, len)?.to_vec())
}
fn take<const N: usize>(buf: &mut &[u8]) -> Result<[u8; N]> {
let Some((head, rest)) = buf.split_first_chunk::<N>() else {
return Err(malformed("cursor: unexpected end of payload"));
};
*buf = rest;
Ok(*head)
}
fn take_slice<'a>(buf: &mut &'a [u8], n: usize) -> Result<&'a [u8]> {
if buf.len() < n {
return Err(malformed("cursor: unexpected end of payload"));
}
let (head, rest) = buf.split_at(n);
*buf = rest;
Ok(head)
}
fn hex_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{b:02x}"));
}
out
}
fn hex_decode(token: &str) -> Result<Vec<u8>> {
let chars: Vec<char> = token.chars().collect();
if !chars.len().is_multiple_of(2) {
return Err(malformed("cursor: odd-length hex token"));
}
let mut out = Vec::with_capacity(chars.len() / 2);
for pair in chars.chunks(2) {
let hi = pair[0]
.to_digit(16)
.ok_or_else(|| malformed("cursor: invalid hex digit"))?;
let lo = pair[1]
.to_digit(16)
.ok_or_else(|| malformed("cursor: invalid hex digit"))?;
out.push(((hi << 4) | lo) as u8);
}
Ok(out)
}
#[cfg(test)]
mod tests;