feat: phase 6 — keyboard/mouse input + fix Tight zlib init
All checks were successful
CI / fmt (push) Successful in 30s
CI / check (push) Successful in 1m9s
CI / clippy (push) Successful in 1m9s

input.rs:
- write_key_press/release/tap: scancode | 0x80 = press, bare = release
- JavaScript KeyboardEvent.code → e-RIC scancode mapping (104pc layout)
- Hotkey sequence sender (raw hex byte strings from applet params)
- write_ctrl_alt_del() using HOTKEYCODE_0 "36 f0 37 f0 4e"
- PointerEvent writer already in msg.rs (8 bytes, absolute mode)
- 5 unit tests

Tight zlib fix:
- Changed Decompress::new(true) for zlib-wrapped format (matching
  Java's Inflater() which expects zlib header 78 9C)
- Added output length verification after decompression
- Tested: Tight-encoded frames now decode correctly from real device

37 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 14:51:50 +03:00
parent c8f981f045
commit ab74f607e8
3 changed files with 239 additions and 2 deletions

View File

@@ -228,11 +228,24 @@ pub fn decode_tight(
let decompressor = zlib.get_or_init(stream_idx); let decompressor = zlib.get_or_init(stream_idx);
let mut output = vec![0u8; total_bytes]; let mut output = vec![0u8; total_bytes];
let before_out = decompressor.total_out();
decompressor decompressor
.decompress(&compressed, &mut output, flate2::FlushDecompress::Sync) .decompress(&compressed, &mut output, flate2::FlushDecompress::None)
.map_err(|e| { .map_err(|e| {
proto::ProtoError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)) proto::ProtoError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"zlib: {e} (stream {stream_idx}, in={comp_len}, expected_out={total_bytes})"
),
))
})?; })?;
let produced = (decompressor.total_out() - before_out) as usize;
if produced != total_bytes {
return Err(proto::ProtoError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("zlib: produced {produced} bytes, expected {total_bytes}"),
)));
}
output output
}; };

223
crates/ericrfb/src/input.rs Normal file
View File

@@ -0,0 +1,223 @@
use std::io::Write;
use crate::proto;
/// e-RIC key scancode from KbdLayout_104pc.java.
/// Press = scancode | 0x80, release = scancode.
/// Sent via msg type 4: `[4, code]`.
pub fn write_key_press(w: &mut impl Write, scancode: u8) -> proto::Result<()> {
w.write_all(&[4, scancode | 0x80])?;
w.flush()?;
Ok(())
}
pub fn write_key_release(w: &mut impl Write, scancode: u8) -> proto::Result<()> {
w.write_all(&[4, scancode])?;
w.flush()?;
Ok(())
}
/// Send a complete key tap (press + release).
pub fn write_key_tap(w: &mut impl Write, scancode: u8) -> proto::Result<()> {
write_key_press(w, scancode)?;
write_key_release(w, scancode)?;
Ok(())
}
/// Send a hotkey sequence from the applet's HOTKEYCODE params.
/// Bytes are space-separated hex values, sent raw via msg type 4.
pub fn write_hotkey_sequence(w: &mut impl Write, hex_str: &str) -> proto::Result<()> {
for token in hex_str.split_whitespace() {
if let Ok(byte) = u8::from_str_radix(token, 16) {
w.write_all(&[4, byte])?;
}
}
w.flush()?;
Ok(())
}
/// Ctrl+Alt+Delete hotkey sequence from the OmniView's HOTKEYCODE_0 param.
pub const HOTKEY_CTRL_ALT_DEL: &str = "36 f0 37 f0 4e";
// ---------------------------------------------------------------------------
// JavaScript KeyboardEvent.code → e-RIC scancode mapping
//
// Maps browser key codes to KbdLayout_104pc keycodes.
// keynr == keycode for almost all keys in this layout.
// ---------------------------------------------------------------------------
/// Map a JavaScript `KeyboardEvent.code` string to an e-RIC scancode.
/// Returns `None` for unmapped keys.
pub fn js_code_to_scancode(code: &str) -> Option<u8> {
Some(match code {
// Function row
"Escape" => 0,
"F1" => 59,
"F2" => 60,
"F3" => 61,
"F4" => 62,
"F5" => 63,
"F6" => 64,
"F7" => 65,
"F8" => 66,
"F9" => 67,
"F10" => 68,
"F11" => 69,
"F12" => 70,
// Number row
"Backquote" => 1,
"Digit1" => 2,
"Digit2" => 3,
"Digit3" => 4,
"Digit4" => 5,
"Digit5" => 6,
"Digit6" => 7,
"Digit7" => 8,
"Digit8" => 9,
"Digit9" => 10,
"Digit0" => 11,
"Minus" => 12,
"Equal" => 13,
"Backspace" => 14,
// QWERTY row
"Tab" => 15,
"KeyQ" => 16,
"KeyW" => 17,
"KeyE" => 18,
"KeyR" => 19,
"KeyT" => 20,
"KeyY" => 21,
"KeyU" => 22,
"KeyI" => 23,
"KeyO" => 24,
"KeyP" => 25,
"BracketLeft" => 26,
"BracketRight" => 27,
// Home row
"CapsLock" => 28,
"KeyA" => 29,
"KeyS" => 30,
"KeyD" => 31,
"KeyF" => 32,
"KeyG" => 33,
"KeyH" => 34,
"KeyJ" => 35,
"KeyK" => 36,
"KeyL" => 37,
"Semicolon" => 38,
"Quote" => 39,
"Enter" => 40,
// Bottom row
"ShiftLeft" => 41,
"Backslash" => 42,
"KeyZ" => 43,
"KeyX" => 44,
"KeyC" => 45,
"KeyV" => 46,
"KeyB" => 47,
"KeyN" => 48,
"KeyM" => 49,
"Comma" => 50,
"Period" => 51,
"Slash" => 52,
"ShiftRight" => 53,
// Modifier / bottom row
"ControlLeft" => 54,
"MetaLeft" => 105,
"AltLeft" => 55,
"Space" => 56,
"AltRight" => 57,
"MetaRight" => 106,
"ControlRight" => 58,
// Navigation cluster
"PrintScreen" => 71,
"ScrollLock" => 72,
"Pause" => 73,
"Insert" => 75,
"Home" => 76,
"PageUp" => 77,
"Delete" => 78,
"End" => 79,
"PageDown" => 80,
// Arrow keys
"ArrowUp" => 81,
"ArrowLeft" => 82,
"ArrowDown" => 83,
"ArrowRight" => 84,
// Numpad
"NumLock" => 85,
"NumpadDivide" => 86,
"NumpadMultiply" => 87,
"NumpadSubtract" => 88,
"NumpadAdd" => 89,
"NumpadEnter" => 98,
"Numpad7" => 90,
"Numpad8" => 94,
"Numpad9" => 99,
"Numpad4" => 91,
"Numpad5" => 92,
"Numpad6" => 93,
"Numpad1" => 95,
"Numpad2" => 96,
"Numpad3" => 97,
"Numpad0" => 100,
"NumpadDecimal" => 101,
_ => return None,
})
}
/// Send Ctrl+Alt+Delete via the applet's hotkey sequence.
pub fn write_ctrl_alt_del(w: &mut impl Write) -> proto::Result<()> {
write_hotkey_sequence(w, HOTKEY_CTRL_ALT_DEL)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_press_sets_bit7() {
let mut buf = Vec::new();
write_key_press(&mut buf, 29).unwrap(); // 'A' scancode
assert_eq!(buf, [4, 29 | 0x80]);
}
#[test]
fn test_key_release_no_bit7() {
let mut buf = Vec::new();
write_key_release(&mut buf, 29).unwrap();
assert_eq!(buf, [4, 29]);
}
#[test]
fn test_key_tap() {
let mut buf = Vec::new();
write_key_tap(&mut buf, 29).unwrap();
assert_eq!(buf, [4, 29 | 0x80, 4, 29]);
}
#[test]
fn test_hotkey_sequence() {
let mut buf = Vec::new();
write_hotkey_sequence(&mut buf, "36 f0 37").unwrap();
assert_eq!(buf, [4, 0x36, 4, 0xF0, 4, 0x37]);
}
#[test]
fn test_js_code_mapping() {
assert_eq!(js_code_to_scancode("KeyA"), Some(29));
assert_eq!(js_code_to_scancode("Escape"), Some(0));
assert_eq!(js_code_to_scancode("ControlLeft"), Some(54));
assert_eq!(js_code_to_scancode("Delete"), Some(78));
assert_eq!(js_code_to_scancode("Unknown"), None);
}
}

View File

@@ -1,6 +1,7 @@
pub mod codec; pub mod codec;
pub mod framebuffer; pub mod framebuffer;
pub mod handshake; pub mod handshake;
pub mod input;
pub mod msg; pub mod msg;
pub mod proto; pub mod proto;
pub mod session; pub mod session;