haematite-cli 0.21.1

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
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")),
    }
}