lodestone-proto 0.0.16

Versioned protocol envelopes and transport-neutral requests for Lodestone
Documentation
//! Method-level envelope for the two read-only `WriterRealtimeRead` payloads.
//!
//! ADR-0035 deliberately admits both gateway point GETs and searcher realtime
//! slices through one outer RPC method. This envelope is the sole method-level
//! discriminant; the selected packet continues to own and validate its strict
//! inner application codec.

use std::error::Error;
use std::fmt::{self, Display, Formatter};

use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;

use crate::{RPC_MAX_CONTROL_PAYLOAD_BYTES, RPC_SEARCH_RESPONSE_FRAME_BYTES};

pub const REALTIME_READ_ENVELOPE_FORMAT_VERSION: u16 = 1;

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RealtimeReadKind {
    PointGet,
    SearchSlice,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct RealtimeReadEnvelope<'a> {
    format_version: u16,
    kind: RealtimeReadKind,
    #[serde(borrow)]
    payload: &'a RawValue,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RealtimeReadEnvelopeError {
    MessageTooLarge {
        actual: usize,
        maximum: usize,
    },
    Malformed,
    NonCanonical,
    UnsupportedVersion(u16),
    UnexpectedKind {
        expected: RealtimeReadKind,
        actual: RealtimeReadKind,
    },
}

impl Display for RealtimeReadEnvelopeError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::MessageTooLarge { actual, maximum } => {
                write!(
                    formatter,
                    "realtime-read envelope is {actual} bytes; maximum is {maximum}"
                )
            }
            Self::Malformed => formatter.write_str("realtime-read envelope is malformed"),
            Self::NonCanonical => formatter.write_str("realtime-read envelope is not canonical"),
            Self::UnsupportedVersion(version) => {
                write!(formatter, "realtime-read envelope version {version} is unsupported")
            }
            Self::UnexpectedKind { expected, actual } => {
                write!(
                    formatter,
                    "realtime-read envelope kind {actual:?} does not match {expected:?}"
                )
            }
        }
    }
}

impl Error for RealtimeReadEnvelopeError {}

pub fn encode_realtime_read_request(
    kind: RealtimeReadKind,
    payload: &[u8],
) -> Result<Vec<u8>, RealtimeReadEnvelopeError> {
    encode_envelope(kind, payload, RPC_MAX_CONTROL_PAYLOAD_BYTES)
}

pub fn decode_realtime_read_request(
    expected: RealtimeReadKind,
    bytes: &[u8],
) -> Result<Vec<u8>, RealtimeReadEnvelopeError> {
    decode_envelope(expected, bytes, RPC_MAX_CONTROL_PAYLOAD_BYTES)
}

pub fn encode_realtime_read_response(
    kind: RealtimeReadKind,
    payload: &[u8],
) -> Result<Vec<u8>, RealtimeReadEnvelopeError> {
    encode_envelope(kind, payload, RPC_SEARCH_RESPONSE_FRAME_BYTES)
}

pub fn decode_realtime_read_response(
    expected: RealtimeReadKind,
    bytes: &[u8],
) -> Result<Vec<u8>, RealtimeReadEnvelopeError> {
    decode_envelope(expected, bytes, RPC_SEARCH_RESPONSE_FRAME_BYTES)
}

fn encode_envelope(
    kind: RealtimeReadKind,
    payload: &[u8],
    maximum: usize,
) -> Result<Vec<u8>, RealtimeReadEnvelopeError> {
    let payload = RawValue::from_string(
        std::str::from_utf8(payload)
            .map_err(|_| RealtimeReadEnvelopeError::Malformed)?
            .to_owned(),
    )
    .map_err(|_| RealtimeReadEnvelopeError::Malformed)?;
    let envelope = RealtimeReadEnvelope {
        format_version: REALTIME_READ_ENVELOPE_FORMAT_VERSION,
        kind,
        payload: &payload,
    };
    let encoded = serde_json::to_vec(&envelope).map_err(|_| RealtimeReadEnvelopeError::Malformed)?;
    check_size(encoded.len(), maximum)?;
    Ok(encoded)
}

fn decode_envelope(
    expected: RealtimeReadKind,
    bytes: &[u8],
    maximum: usize,
) -> Result<Vec<u8>, RealtimeReadEnvelopeError> {
    check_size(bytes.len(), maximum)?;
    let envelope =
        serde_json::from_slice::<RealtimeReadEnvelope<'_>>(bytes).map_err(|_| RealtimeReadEnvelopeError::Malformed)?;
    if envelope.format_version != REALTIME_READ_ENVELOPE_FORMAT_VERSION {
        return Err(RealtimeReadEnvelopeError::UnsupportedVersion(envelope.format_version));
    }
    if envelope.kind != expected {
        return Err(RealtimeReadEnvelopeError::UnexpectedKind {
            expected,
            actual: envelope.kind,
        });
    }
    let canonical = serde_json::to_vec(&envelope).map_err(|_| RealtimeReadEnvelopeError::Malformed)?;
    if canonical != bytes {
        return Err(RealtimeReadEnvelopeError::NonCanonical);
    }
    Ok(envelope.payload.get().as_bytes().to_vec())
}

fn check_size(actual: usize, maximum: usize) -> Result<(), RealtimeReadEnvelopeError> {
    if actual > maximum {
        Err(RealtimeReadEnvelopeError::MessageTooLarge { actual, maximum })
    } else {
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn round_trip_is_canonical_and_kind_fenced() -> Result<(), Box<dyn Error>> {
        let encoded = encode_realtime_read_request(RealtimeReadKind::SearchSlice, br#"{"value":1}"#)?;
        assert_eq!(
            decode_realtime_read_request(RealtimeReadKind::SearchSlice, &encoded)?,
            br#"{"value":1}"#
        );
        assert!(matches!(
            decode_realtime_read_request(RealtimeReadKind::PointGet, &encoded),
            Err(RealtimeReadEnvelopeError::UnexpectedKind { .. })
        ));

        let noncanonical = std::str::from_utf8(&encoded)?.replace("{\"format_version\"", "{ \"format_version\"");
        assert!(matches!(
            decode_realtime_read_request(RealtimeReadKind::SearchSlice, noncanonical.as_bytes()),
            Err(RealtimeReadEnvelopeError::NonCanonical)
        ));

        let mut future: serde_json::Value = serde_json::from_slice(&encoded)?;
        future["format_version"] = serde_json::json!(2);
        assert!(matches!(
            decode_realtime_read_request(RealtimeReadKind::SearchSlice, &serde_json::to_vec(&future)?),
            Err(RealtimeReadEnvelopeError::UnsupportedVersion(2))
        ));
        let mut unknown: serde_json::Value = serde_json::from_slice(&encoded)?;
        unknown["unexpected"] = serde_json::json!(true);
        assert!(matches!(
            decode_realtime_read_request(RealtimeReadKind::SearchSlice, &serde_json::to_vec(&unknown)?),
            Err(RealtimeReadEnvelopeError::Malformed)
        ));
        assert!(matches!(
            decode_realtime_read_request(
                RealtimeReadKind::SearchSlice,
                &vec![b'x'; RPC_MAX_CONTROL_PAYLOAD_BYTES + 1],
            ),
            Err(RealtimeReadEnvelopeError::MessageTooLarge { .. })
        ));
        Ok(())
    }
}