use anyhow::{Context, Result, bail};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use yaiba_core::{Entry, VersionVector};
const MAX_FRAME: usize = 32 * 1024 * 1024;
#[derive(Debug, Serialize, Deserialize)]
pub struct Hello {
pub room: String,
pub vv: VersionVector,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Offer {
pub vv: VersionVector,
pub entries: Vec<Entry>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Push {
pub entries: Vec<Entry>,
}
pub async fn write_frame<T, W>(writer: &mut W, value: &T) -> Result<()>
where
T: Serialize,
W: tokio::io::AsyncWrite + Unpin,
{
use tokio::io::AsyncWriteExt;
let bytes = serde_json::to_vec(value).context("encode frame")?;
if bytes.len() > MAX_FRAME {
bail!("frame too large to send: {} bytes", bytes.len());
}
writer
.write_all(&(bytes.len() as u32).to_be_bytes())
.await?;
writer.write_all(&bytes).await?;
writer.flush().await?;
Ok(())
}
pub async fn read_frame<T, R>(reader: &mut R) -> Result<T>
where
T: DeserializeOwned,
R: tokio::io::AsyncRead + Unpin,
{
use tokio::io::AsyncReadExt;
let mut len = [0u8; 4];
reader.read_exact(&mut len).await.context("read length")?;
let len = u32::from_be_bytes(len) as usize;
if len > MAX_FRAME {
bail!("peer announced an oversized frame: {len} bytes");
}
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf).await.context("read body")?;
serde_json::from_slice(&buf).context("decode frame")
}
pub fn room_matches(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
a.bytes()
.zip(b.bytes())
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
== 0
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn frames_round_trip() {
let hello = Hello {
room: "abc".into(),
vv: VersionVector::new(),
};
let mut buf = Vec::new();
write_frame(&mut buf, &hello).await.unwrap();
let mut cursor = std::io::Cursor::new(buf);
let back: Hello = read_frame(&mut cursor).await.unwrap();
assert_eq!(back.room, "abc");
}
#[tokio::test]
async fn an_oversized_length_prefix_is_refused_before_allocating() {
let mut framed = Vec::new();
framed.extend_from_slice(&u32::MAX.to_be_bytes());
let mut cursor = std::io::Cursor::new(framed);
let err = read_frame::<Hello, _>(&mut cursor).await.unwrap_err();
assert!(err.to_string().contains("oversized"));
}
#[test]
fn room_comparison_rejects_mismatches_and_length_changes() {
assert!(room_matches("deadbeef", "deadbeef"));
assert!(!room_matches("deadbeef", "deadbeee"));
assert!(!room_matches("deadbeef", "deadbee"));
}
}