use serde::{Deserialize, Serialize};
use super::super::{Failure, ForkProvenance, identifier};
use crate::serve::protocol::Version;
const MAX_CURSOR_BYTES: usize = 32_768;
const MAX_KEY_BYTES: usize = 6 * 513;
const DIGITS: &[u8; 16] = b"0123456789abcdef";
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Cursor {
pub(super) branch: String,
pub(super) version: Version,
pub(super) from: ForkProvenance,
pub(super) shard: usize,
pub(super) key: Option<Vec<u8>>,
}
fn invalid(message: &str) -> Failure {
Failure::new("invalid_continuation", message)
}
impl Cursor {
pub(super) fn decode(text: &str, branch: &str) -> Result<Self, Failure> {
let cursor: Self = decode(text)?;
cursor.validate(branch)?;
Ok(cursor)
}
pub(super) fn validate(&self, branch: &str) -> Result<(), Failure> {
identifier(&self.branch)
.map_err(|_| invalid("review continuation has an invalid branch"))?;
identifier(&self.from.branch)
.map_err(|_| invalid("review continuation has an invalid origin"))?;
if self.branch != branch
|| self
.key
.as_ref()
.is_some_and(|key| key.len() > MAX_KEY_BYTES)
{
return Err(invalid(
"review continuation does not belong to this branch or key range",
));
}
Ok(())
}
pub(super) fn encode(&self) -> Result<String, Failure> {
encode(self)
}
}
pub(in crate::serve::storage) fn decode<T: serde::de::DeserializeOwned>(
text: &str,
) -> Result<T, Failure> {
if text.len() > MAX_CURSOR_BYTES || !text.len().is_multiple_of(2) {
return Err(invalid("review continuation has an invalid length"));
}
let bytes = text
.as_bytes()
.as_chunks::<2>()
.0
.iter()
.map(|pair| Ok((digit(pair[0])? << 4) | digit(pair[1])?))
.collect::<Result<Vec<_>, Failure>>()?;
serde_json::from_slice(&bytes)
.map_err(|_| invalid("review continuation is not a captured cursor"))
}
pub(in crate::serve::storage) fn encode<T: Serialize>(cursor: &T) -> Result<String, Failure> {
let bytes = serde_json::to_vec(cursor).map_err(Failure::storage)?;
let mut text = String::new();
for byte in bytes {
text.push(char::from(DIGITS[usize::from(byte >> 4)]));
text.push(char::from(DIGITS[usize::from(byte & 15)]));
}
if text.len() > MAX_CURSOR_BYTES {
return Err(invalid("review continuation exceeds its byte bound"));
}
Ok(text)
}
fn digit(byte: u8) -> Result<u8, Failure> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
_ => Err(invalid("review continuation is not hexadecimal")),
}
}