feat: phase 3 — framebuffer, raw decoder, session pump, snapshot
framebuffer.rs: - Framebuffer struct (8bpp RGB332, row-major) - apply_raw() blit, copy_rect() with overlap-safe logic - to_rgba() via compile-time RGB332 LUT session.rs: - ActiveSession: connect + SetEncodings + initial FBUpdateRequest - Full server message dispatch loop (all 15 message types) - Raw (encoding 0) and CopyRect (encoding 1) decoders - Ping response, bandwidth probe bookends, mode change resize examples/snapshot.rs: - Connects, waits for first FramebufferUpdate, saves PNG - Tested against real OmniView at 10.3.0.130:443 - Successfully captured 640x480 frame Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
80
Cargo.lock
generated
80
Cargo.lock
generated
@@ -2,6 +2,12 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -110,6 +116,12 @@ version = "0.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@@ -128,6 +140,15 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -138,6 +159,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|||||||
name = "ericrfb"
|
name = "ericrfb"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"png",
|
||||||
"proptest",
|
"proptest",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
@@ -170,6 +192,25 @@ version = "2.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fdeflate"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||||
|
dependencies = [
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -440,6 +481,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -510,6 +561,19 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "png"
|
||||||
|
version = "0.17.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"crc32fast",
|
||||||
|
"fdeflate",
|
||||||
|
"flate2",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -546,7 +610,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-set",
|
"bit-set",
|
||||||
"bit-vec",
|
"bit-vec",
|
||||||
"bitflags",
|
"bitflags 2.11.1",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand",
|
"rand",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
@@ -628,7 +692,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -654,7 +718,7 @@ version = "1.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.1",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
@@ -781,6 +845,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -1065,7 +1135,7 @@ version = "0.244.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.1",
|
||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"semver",
|
"semver",
|
||||||
@@ -1150,7 +1220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"bitflags 2.11.1",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ thiserror.workspace = true
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
|
png = "0.17"
|
||||||
|
|||||||
88
crates/ericrfb/examples/snapshot.rs
Normal file
88
crates/ericrfb/examples/snapshot.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufWriter;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use ericrfb::handshake::Config;
|
||||||
|
use ericrfb::session::{ActiveSession, Event};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
let host = args
|
||||||
|
.iter()
|
||||||
|
.position(|a| a == "--host")
|
||||||
|
.and_then(|i| args.get(i + 1))
|
||||||
|
.expect("usage: --host <ip> --applet-id <token> [--port <port>] [--output <file.png>]");
|
||||||
|
|
||||||
|
let applet_id = args
|
||||||
|
.iter()
|
||||||
|
.position(|a| a == "--applet-id")
|
||||||
|
.and_then(|i| args.get(i + 1))
|
||||||
|
.expect("usage: --host <ip> --applet-id <token> [--port <port>] [--output <file.png>]");
|
||||||
|
|
||||||
|
let port: u16 = args
|
||||||
|
.iter()
|
||||||
|
.position(|a| a == "--port")
|
||||||
|
.and_then(|i| args.get(i + 1))
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(443);
|
||||||
|
|
||||||
|
let output = args
|
||||||
|
.iter()
|
||||||
|
.position(|a| a == "--output")
|
||||||
|
.and_then(|i| args.get(i + 1).map(|s| s.as_str()))
|
||||||
|
.unwrap_or("frame.png");
|
||||||
|
|
||||||
|
let cfg = Config::new(host, port, applet_id);
|
||||||
|
println!("Connecting to {}:{}...", cfg.host, cfg.port);
|
||||||
|
|
||||||
|
// Only request Raw encoding (0) for Phase 3
|
||||||
|
let mut session = ActiveSession::connect(&cfg, &[0]).expect("connect failed");
|
||||||
|
println!(
|
||||||
|
"Connected: {}x{}, waiting for first frame...",
|
||||||
|
session.framebuffer.width, session.framebuffer.height
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process messages until we get a FramebufferDirty event
|
||||||
|
loop {
|
||||||
|
match session.process_one() {
|
||||||
|
Ok(Some(Event::FramebufferDirty)) => {
|
||||||
|
println!("Got framebuffer update, saving to {output}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(Some(Event::Resize { width, height })) => {
|
||||||
|
println!("Resized to {width}x{height}");
|
||||||
|
}
|
||||||
|
Ok(Some(Event::Debug(s))) => {
|
||||||
|
eprintln!("[debug] {s}");
|
||||||
|
}
|
||||||
|
Ok(Some(Event::RfbCommand(k, v))) => {
|
||||||
|
eprintln!("[rfb-cmd] {k}={v}");
|
||||||
|
}
|
||||||
|
Ok(Some(_)) => {}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write PNG
|
||||||
|
let rgba = session.framebuffer.to_rgba();
|
||||||
|
let w = session.framebuffer.width as u32;
|
||||||
|
let h = session.framebuffer.height as u32;
|
||||||
|
|
||||||
|
let path = Path::new(output);
|
||||||
|
let file = File::create(path).expect("cannot create output file");
|
||||||
|
let bw = BufWriter::new(file);
|
||||||
|
|
||||||
|
let mut encoder = png::Encoder::new(bw, w, h);
|
||||||
|
encoder.set_color(png::ColorType::Rgba);
|
||||||
|
encoder.set_depth(png::BitDepth::Eight);
|
||||||
|
let mut writer = encoder.write_header().expect("png header failed");
|
||||||
|
writer.write_image_data(&rgba).expect("png write failed");
|
||||||
|
|
||||||
|
println!("Saved {w}x{h} frame to {}", path.display());
|
||||||
|
}
|
||||||
111
crates/ericrfb/src/framebuffer.rs
Normal file
111
crates/ericrfb/src/framebuffer.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use crate::proto::RGB332_LUT;
|
||||||
|
|
||||||
|
/// 8bpp framebuffer storing raw RGB332 pixels. Converts to RGBA on demand.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Framebuffer {
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
/// Raw 8bpp pixel data, row-major, `width * height` bytes.
|
||||||
|
pub pixels: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Framebuffer {
|
||||||
|
pub fn new(width: u16, height: u16) -> Self {
|
||||||
|
let size = width as usize * height as usize;
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pixels: vec![0; size],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize the framebuffer, discarding old contents.
|
||||||
|
pub fn resize(&mut self, width: u16, height: u16) {
|
||||||
|
self.width = width;
|
||||||
|
self.height = height;
|
||||||
|
let size = width as usize * height as usize;
|
||||||
|
self.pixels.resize(size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blit raw 8bpp data into the framebuffer at (x, y) with dimensions (w, h).
|
||||||
|
/// `data` must contain exactly `w * h` bytes.
|
||||||
|
pub fn apply_raw(&mut self, x: u16, y: u16, w: u16, h: u16, data: &[u8]) {
|
||||||
|
debug_assert_eq!(data.len(), w as usize * h as usize);
|
||||||
|
let stride = self.width as usize;
|
||||||
|
for row in 0..h as usize {
|
||||||
|
let dst_offset = (y as usize + row) * stride + x as usize;
|
||||||
|
let src_offset = row * w as usize;
|
||||||
|
let dst = &mut self.pixels[dst_offset..dst_offset + w as usize];
|
||||||
|
dst.copy_from_slice(&data[src_offset..src_offset + w as usize]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy a rectangle within the framebuffer (CopyRect encoding).
|
||||||
|
/// Handles overlapping regions correctly.
|
||||||
|
pub fn copy_rect(&mut self, src_x: u16, src_y: u16, dst_x: u16, dst_y: u16, w: u16, h: u16) {
|
||||||
|
let stride = self.width as usize;
|
||||||
|
let w = w as usize;
|
||||||
|
|
||||||
|
if src_y <= dst_y {
|
||||||
|
// Copy bottom-to-top to handle downward overlap
|
||||||
|
for row in (0..h as usize).rev() {
|
||||||
|
let src_off = (src_y as usize + row) * stride + src_x as usize;
|
||||||
|
let dst_off = (dst_y as usize + row) * stride + dst_x as usize;
|
||||||
|
self.pixels.copy_within(src_off..src_off + w, dst_off);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Copy top-to-bottom to handle upward overlap
|
||||||
|
for row in 0..h as usize {
|
||||||
|
let src_off = (src_y as usize + row) * stride + src_x as usize;
|
||||||
|
let dst_off = (dst_y as usize + row) * stride + dst_x as usize;
|
||||||
|
self.pixels.copy_within(src_off..src_off + w, dst_off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the entire framebuffer to RGBA (4 bytes per pixel).
|
||||||
|
pub fn to_rgba(&self) -> Vec<u8> {
|
||||||
|
let mut rgba = Vec::with_capacity(self.pixels.len() * 4);
|
||||||
|
for &px in &self.pixels {
|
||||||
|
rgba.extend_from_slice(&RGB332_LUT[px as usize]);
|
||||||
|
}
|
||||||
|
rgba
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_raw() {
|
||||||
|
let mut fb = Framebuffer::new(4, 4);
|
||||||
|
// Fill a 2x2 region at (1,1) with value 0xFF
|
||||||
|
fb.apply_raw(1, 1, 2, 2, &[0xFF; 4]);
|
||||||
|
assert_eq!(fb.pixels[0], 0); // (0,0)
|
||||||
|
assert_eq!(fb.pixels[5], 0xFF); // (1,1)
|
||||||
|
assert_eq!(fb.pixels[6], 0xFF); // (2,1)
|
||||||
|
assert_eq!(fb.pixels[9], 0xFF); // (1,2)
|
||||||
|
assert_eq!(fb.pixels[10], 0xFF); // (2,2)
|
||||||
|
assert_eq!(fb.pixels[15], 0); // (3,3)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_rect_no_overlap() {
|
||||||
|
let mut fb = Framebuffer::new(4, 4);
|
||||||
|
fb.apply_raw(0, 0, 2, 2, &[1, 2, 3, 4]);
|
||||||
|
fb.copy_rect(0, 0, 2, 2, 2, 2);
|
||||||
|
// Check destination
|
||||||
|
assert_eq!(fb.pixels[10], 1); // (2,2)
|
||||||
|
assert_eq!(fb.pixels[11], 2); // (3,2)
|
||||||
|
assert_eq!(fb.pixels[14], 3); // (2,3)
|
||||||
|
assert_eq!(fb.pixels[15], 4); // (3,3)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_rgba_size() {
|
||||||
|
let fb = Framebuffer::new(2, 2);
|
||||||
|
let rgba = fb.to_rgba();
|
||||||
|
assert_eq!(rgba.len(), 2 * 2 * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,6 +107,11 @@ pub struct ServerInit {
|
|||||||
pub blue_shift: u8,
|
pub blue_shift: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a ServerInit struct from a stream. Public for reuse in ModeChange (msg 128).
|
||||||
|
pub fn read_server_init_from(r: &mut impl Read) -> Result<ServerInit, HandshakeError> {
|
||||||
|
read_server_init(r)
|
||||||
|
}
|
||||||
|
|
||||||
fn read_server_init(r: &mut impl Read) -> Result<ServerInit, HandshakeError> {
|
fn read_server_init(r: &mut impl Read) -> Result<ServerInit, HandshakeError> {
|
||||||
let supports_resize = read_u8(r)? != 0;
|
let supports_resize = read_u8(r)? != 0;
|
||||||
let width = read_u16_be(r)?;
|
let width = read_u16_be(r)?;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pub mod framebuffer;
|
||||||
pub mod handshake;
|
pub mod handshake;
|
||||||
pub mod msg;
|
pub mod msg;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
|
pub mod session;
|
||||||
|
|||||||
203
crates/ericrfb/src/session.rs
Normal file
203
crates/ericrfb/src/session.rs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
use std::io::{BufReader, BufWriter};
|
||||||
|
use std::net::TcpStream;
|
||||||
|
|
||||||
|
use crate::framebuffer::Framebuffer;
|
||||||
|
use crate::handshake::{self, Config, ServerInit};
|
||||||
|
use crate::msg::{self, ServerMsg};
|
||||||
|
use crate::proto::{self, RectHeader, read_exact, read_u8};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SessionError {
|
||||||
|
#[error("handshake: {0}")]
|
||||||
|
Handshake(#[from] handshake::HandshakeError),
|
||||||
|
#[error("protocol: {0}")]
|
||||||
|
Proto(#[from] proto::ProtoError),
|
||||||
|
#[error("io: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("unsupported encoding: {0}")]
|
||||||
|
UnsupportedEncoding(i32),
|
||||||
|
#[error("unsupported message type: {0}")]
|
||||||
|
UnsupportedMessage(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Events emitted by the session pump to consumers.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
/// A region of the framebuffer was updated.
|
||||||
|
FramebufferDirty,
|
||||||
|
/// The framebuffer was resized.
|
||||||
|
Resize { width: u16, height: u16 },
|
||||||
|
/// Bell from server.
|
||||||
|
Bell,
|
||||||
|
/// Server sent a debug string.
|
||||||
|
Debug(String),
|
||||||
|
/// Server sent an RFB command (key, value).
|
||||||
|
RfbCommand(String, String),
|
||||||
|
/// Server name updated.
|
||||||
|
NameUpdate(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Active protocol session with framebuffer.
|
||||||
|
pub struct ActiveSession {
|
||||||
|
pub framebuffer: Framebuffer,
|
||||||
|
pub server_name: String,
|
||||||
|
pub reader: BufReader<TcpStream>,
|
||||||
|
pub writer: BufWriter<TcpStream>,
|
||||||
|
pub server_init: ServerInit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveSession {
|
||||||
|
/// Connect, handshake, send SetEncodings + initial FBUpdateRequest.
|
||||||
|
pub fn connect(cfg: &Config, encodings: &[i32]) -> Result<Self, SessionError> {
|
||||||
|
let raw = handshake::connect(cfg)?;
|
||||||
|
let mut session = Self {
|
||||||
|
framebuffer: Framebuffer::new(raw.server_init.width, raw.server_init.height),
|
||||||
|
server_name: raw.server_name,
|
||||||
|
reader: raw.reader,
|
||||||
|
writer: raw.writer,
|
||||||
|
server_init: raw.server_init,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send SetEncodings
|
||||||
|
msg::write_set_encodings(&mut session.writer, encodings)?;
|
||||||
|
|
||||||
|
// Request full non-incremental framebuffer update
|
||||||
|
msg::write_fb_update_request(
|
||||||
|
&mut session.writer,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
session.framebuffer.width,
|
||||||
|
session.framebuffer.height,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request an incremental framebuffer update.
|
||||||
|
pub fn request_update(&mut self) -> Result<(), SessionError> {
|
||||||
|
msg::write_fb_update_request(
|
||||||
|
&mut self.writer,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
self.framebuffer.width,
|
||||||
|
self.framebuffer.height,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process one server message. Returns an event if meaningful to the consumer.
|
||||||
|
pub fn process_one(&mut self) -> Result<Option<Event>, SessionError> {
|
||||||
|
let msg_type = read_u8(&mut self.reader)?;
|
||||||
|
let msg = ServerMsg::from(msg_type);
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
ServerMsg::FramebufferUpdate => {
|
||||||
|
self.handle_fb_update()?;
|
||||||
|
Ok(Some(Event::FramebufferDirty))
|
||||||
|
}
|
||||||
|
ServerMsg::Bell => Ok(Some(Event::Bell)),
|
||||||
|
ServerMsg::Ping => {
|
||||||
|
let payload = msg::read_ping(&mut self.reader)?;
|
||||||
|
msg::write_ping_response(&mut self.writer, payload)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
ServerMsg::BandwidthProbe => {
|
||||||
|
msg::write_bandwidth_marker(&mut self.writer, 1)?;
|
||||||
|
msg::read_bandwidth_probe(&mut self.reader)?;
|
||||||
|
msg::write_bandwidth_marker(&mut self.writer, 2)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
ServerMsg::Ack => {
|
||||||
|
msg::read_ack(&mut self.reader)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
ServerMsg::DebugString => {
|
||||||
|
let s = msg::read_debug_string(&mut self.reader)?;
|
||||||
|
Ok(Some(Event::Debug(s)))
|
||||||
|
}
|
||||||
|
ServerMsg::RfbCommand => {
|
||||||
|
let (k, v) = msg::read_rfb_command(&mut self.reader)?;
|
||||||
|
Ok(Some(Event::RfbCommand(k, v)))
|
||||||
|
}
|
||||||
|
ServerMsg::ServerCutText => {
|
||||||
|
let _text = msg::read_server_cut_text(&mut self.reader)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
ServerMsg::ServerNameUpdate => {
|
||||||
|
let name = msg::read_server_name_update(&mut self.reader)?;
|
||||||
|
self.server_name = name.clone();
|
||||||
|
Ok(Some(Event::NameUpdate(name)))
|
||||||
|
}
|
||||||
|
ServerMsg::LayoutLocale => {
|
||||||
|
let _locale = msg::read_layout_locale(&mut self.reader)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
ServerMsg::DesktopResize => {
|
||||||
|
// Reads same struct as handshake pixel-format (aw.i, line 519)
|
||||||
|
let _flag = read_u8(&mut self.reader)?;
|
||||||
|
let _depth = proto::read_u16_be(&mut self.reader)?;
|
||||||
|
let label_len = proto::read_u16_be(&mut self.reader)? as usize;
|
||||||
|
let _label = read_exact(&mut self.reader, label_len)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
ServerMsg::ModeChange => {
|
||||||
|
// Re-read ServerInit (aw.k, line 435) — framebuffer dimensions may change
|
||||||
|
let si = handshake::read_server_init_from(&mut self.reader)?;
|
||||||
|
let old_w = self.framebuffer.width;
|
||||||
|
let old_h = self.framebuffer.height;
|
||||||
|
self.server_init = si;
|
||||||
|
if self.server_init.width != old_w || self.server_init.height != old_h {
|
||||||
|
self.framebuffer
|
||||||
|
.resize(self.server_init.width, self.server_init.height);
|
||||||
|
return Ok(Some(Event::Resize {
|
||||||
|
width: self.server_init.width,
|
||||||
|
height: self.server_init.height,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
ServerMsg::RdpEvent => {
|
||||||
|
let _event_type = msg::read_rdp_event(&mut self.reader)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
ServerMsg::PixelFormatChange => {
|
||||||
|
// aw.e(), line 537: 1 pad + 4×u8 + 8×u16 = 21 bytes
|
||||||
|
let _data = read_exact(&mut self.reader, 21)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
ServerMsg::SetColourMapEntries => Err(SessionError::UnsupportedMessage(msg_type)),
|
||||||
|
ServerMsg::Unknown(t) => Err(SessionError::UnsupportedMessage(t)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_fb_update(&mut self) -> Result<(), SessionError> {
|
||||||
|
let num_rects = msg::read_fb_update_header(&mut self.reader)?;
|
||||||
|
|
||||||
|
for _ in 0..num_rects {
|
||||||
|
let hdr = RectHeader::read_from(&mut self.reader)?;
|
||||||
|
|
||||||
|
match hdr.encoding {
|
||||||
|
0 => {
|
||||||
|
// Raw: read w*h bytes
|
||||||
|
let size = hdr.w as usize * hdr.h as usize;
|
||||||
|
let data = read_exact(&mut self.reader, size)?;
|
||||||
|
self.framebuffer
|
||||||
|
.apply_raw(hdr.x, hdr.y, hdr.w, hdr.h, &data);
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
// CopyRect: read src_x, src_y (u16 each)
|
||||||
|
let src_x = proto::read_u16_be(&mut self.reader)?;
|
||||||
|
let src_y = proto::read_u16_be(&mut self.reader)?;
|
||||||
|
self.framebuffer
|
||||||
|
.copy_rect(src_x, src_y, hdr.x, hdr.y, hdr.w, hdr.h);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(SessionError::UnsupportedEncoding(other));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user