use std::{
io::Cursor,
mem::take,
sync::{PoisonError, atomic::Ordering},
};
use tokio::sync::mpsc::Sender;
use tracing::{error, warn};
use crate::{
EncryptedFrame, render_server_update,
udp::reader::{ClientRenderCtx, apply_full_state_rendering, decode_all_capped},
};
const STATESYNC_MISMATCH_LIMIT: u32 = 3;
#[derive(Debug, Default)]
pub(crate) struct StateSyncClient {
ack_state: Vec<u8>,
ack_state_seq: u64,
statesync_mismatch_count: u32,
initial_state_received: bool,
pending_chunk_total: u16,
pending_chunk_seq: u16,
pending_chunk_data: Vec<u8>,
}
impl StateSyncClient {
pub(crate) fn apply_full_state(&mut self, payload: &[u8], ctx: &ClientRenderCtx) -> Vec<u8> {
let repaint = apply_full_state_rendering(
payload,
ctx.emulator(),
ctx.prediction(),
ctx.renderer(),
ctx.in_alt_screen(),
);
let (mut ack, is_alt) = {
let emu = ctx
.emulator()
.lock()
.unwrap_or_else(PoisonError::into_inner);
let screen = emu.screen();
(screen.contents_formatted(), screen.alternate_screen())
};
if is_alt {
let mut prefixed = b"\x1b[?1049h".to_vec();
prefixed.extend_from_slice(&ack);
ack = prefixed;
}
self.ack_state = ack;
self.ack_state_seq = 0;
self.statesync_mismatch_count = 0;
self.initial_state_received = true;
repaint
}
pub(crate) async fn apply_diff(
&mut self,
base_id: u64,
diff_id: u64,
compressed: &[u8],
ctx: &ClientRenderCtx,
nak_out_tx: Option<&Sender<EncryptedFrame>>,
) {
if !self.initial_state_received {
if let Some(tx) = nak_out_tx {
drop(tx.try_send(EncryptedFrame::RepaintRequest));
}
return;
}
if base_id != self.ack_state_seq {
self.statesync_mismatch_count += 1;
if self.statesync_mismatch_count >= STATESYNC_MISMATCH_LIMIT {
self.statesync_mismatch_count = 0;
if let Some(tx) = nak_out_tx
&& let Err(e) = tx.try_send(EncryptedFrame::RepaintRequest)
{
warn!("Failed to send StateSync desync RepaintRequest: {e}");
}
}
return;
}
self.statesync_mismatch_count = 0;
let diff_bytes = match decode_all_capped(compressed) {
Ok(bytes) => bytes,
Err(e) => {
error!("Failed to decompress StateSyncDiff: {e}");
return;
}
};
let (rows, cols) = {
let emu = ctx
.emulator()
.lock()
.unwrap_or_else(PoisonError::into_inner);
emu.screen().size()
};
let mut tmp = vt100::Parser::new(rows, cols, 0);
if !self.ack_state.is_empty() {
tmp.process(&self.ack_state);
}
tmp.process(&diff_bytes);
let is_alt = tmp.screen().alternate_screen();
ctx.in_alt_screen().store(is_alt, Ordering::Relaxed);
let mut new_ack = tmp.screen().contents_formatted();
if is_alt {
let mut prefixed = b"\x1b[?1049h".to_vec();
prefixed.extend_from_slice(&new_ack);
new_ack = prefixed;
}
self.ack_state = new_ack;
self.ack_state_seq = diff_id;
{
let mut emu = ctx
.emulator()
.lock()
.unwrap_or_else(PoisonError::into_inner);
emu.replace_parser(tmp);
}
let repaint =
render_server_update(ctx.emulator(), ctx.prediction(), ctx.renderer(), !is_alt);
if !repaint.is_empty()
&& let Err(e) = ctx.stdout_tx().send(repaint).await
{
error!("Error sending StateSyncDiff to stdout channel: {e}");
}
if let Some(tx) = nak_out_tx
&& let Err(e) = tx.try_send(EncryptedFrame::ClientAck(diff_id))
{
warn!("Failed to send ClientAck: {e}");
}
}
pub(crate) async fn apply_chunk(
&mut self,
seq: u16,
total: u16,
data: Vec<u8>,
ctx: &ClientRenderCtx,
nak_out_tx: Option<&Sender<EncryptedFrame>>,
) {
if seq == 0 {
self.pending_chunk_total = total;
self.pending_chunk_seq = 0;
self.pending_chunk_data = data;
} else if seq == self.pending_chunk_seq && total == self.pending_chunk_total {
self.pending_chunk_data.extend_from_slice(&data);
} else {
self.pending_chunk_total = 0;
self.pending_chunk_seq = 0;
self.pending_chunk_data.clear();
if let Some(tx) = nak_out_tx {
drop(tx.try_send(EncryptedFrame::RepaintRequest));
}
return;
}
self.pending_chunk_seq += 1;
if self.pending_chunk_seq != self.pending_chunk_total {
return;
}
let payload_compressed = take(&mut self.pending_chunk_data);
self.pending_chunk_seq = 0;
self.pending_chunk_total = 0;
match decode_all_capped(payload_compressed.as_slice()) {
Ok(payload) => {
let repaint = self.apply_full_state(&payload, ctx);
if !repaint.is_empty()
&& let Err(e) = ctx.stdout_tx().send(repaint).await
{
error!("Error sending StateChunk repaint to stdout channel: {e}");
}
}
Err(e) => {
error!("Failed to decompress StateChunk assembly: {e}");
}
}
}
}
#[doc(hidden)]
pub fn fuzz_statesync_drive(data: &[u8]) {
use std::sync::{Arc, Mutex, atomic::AtomicBool};
use tokio::{runtime::Builder, sync::mpsc::channel};
use crate::{DisplayPreference, Emulator, PredictionEngine, Renderer};
const ROWS: u16 = 24;
const COLS: u16 = 80;
let Ok(rt) = Builder::new_current_thread().build() else {
return;
};
let (stdout_tx, _stdout_rx) = channel::<Vec<u8>>(64);
let (nak_tx, _nak_rx) = channel::<EncryptedFrame>(64);
let ctx = ClientRenderCtx::new(
stdout_tx,
Arc::new(Mutex::new(Emulator::new(ROWS, COLS))),
Arc::new(Mutex::new(PredictionEngine::new(
DisplayPreference::Adaptive,
))),
Arc::new(Mutex::new(Renderer::new(ROWS, COLS))),
Arc::new(AtomicBool::new(false)),
);
let mut client = StateSyncClient::default();
let mut cur = Cursor::new(data);
rt.block_on(async {
while let Some(op) = next_op(&mut cur) {
match op {
FuzzOp::FullState(payload) => {
let _repaint = client.apply_full_state(&payload, &ctx);
}
FuzzOp::Diff {
base_id,
diff_id,
compressed,
} => {
client
.apply_diff(base_id, diff_id, &compressed, &ctx, Some(&nak_tx))
.await;
}
FuzzOp::Chunk { seq, total, data } => {
client
.apply_chunk(seq, total, data, &ctx, Some(&nak_tx))
.await;
}
}
}
});
}
enum FuzzOp {
FullState(Vec<u8>),
Diff {
base_id: u64,
diff_id: u64,
compressed: Vec<u8>,
},
Chunk {
seq: u16,
total: u16,
data: Vec<u8>,
},
}
fn next_op(cur: &mut Cursor<&[u8]>) -> Option<FuzzOp> {
let selector = read_u8(cur)?;
match selector {
0x00 => Some(FuzzOp::FullState(read_blob(cur)?)),
0x01 => {
let base_id = read_u64(cur)?;
let diff_id = read_u64(cur)?;
let compressed = read_blob(cur)?;
Some(FuzzOp::Diff {
base_id,
diff_id,
compressed,
})
}
0x02 => {
let seq = read_u16(cur)?;
let total = read_u16(cur)?;
let data = read_blob(cur)?;
Some(FuzzOp::Chunk { seq, total, data })
}
_ => None,
}
}
fn read_u8(cur: &mut Cursor<&[u8]>) -> Option<u8> {
let mut b = [0u8; 1];
take_exact(cur, &mut b)?;
Some(b[0])
}
fn read_u16(cur: &mut Cursor<&[u8]>) -> Option<u16> {
let mut b = [0u8; 2];
take_exact(cur, &mut b)?;
Some(u16::from_be_bytes(b))
}
fn read_u64(cur: &mut Cursor<&[u8]>) -> Option<u64> {
let mut b = [0u8; 8];
take_exact(cur, &mut b)?;
Some(u64::from_be_bytes(b))
}
fn read_blob(cur: &mut Cursor<&[u8]>) -> Option<Vec<u8>> {
let len = usize::from(read_u16(cur)?);
let mut buf = vec![0u8; len];
take_exact(cur, &mut buf)?;
Some(buf)
}
fn take_exact(cur: &mut Cursor<&[u8]>, dst: &mut [u8]) -> Option<()> {
use std::io::Read as _;
cur.read_exact(dst).ok()
}