feat(common): 协议 v3——新增 InputEvent::Text 文本注入通道
安卓软键盘的中文候选词结果不是扫描码,扫描码通道对中文天然不通,必须 另开一条按字符注入的通道。载体选 Vec<u16>(UTF-16 code unit)而非 String: Windows 注入侧与 Java 字符串原生都是 UTF-16,中间任何一次 UTF-8 往返都会 把 IME 提交边界的落单代理项变成替换字符。 InputEvent 因携带 Vec 不再是 Copy,调用方改用 clone;三端必须同步升级, 旧端由 Auth 握手显式拒绝(这正是 PROTO_VERSION 递增的目的)。 新增 6 个单测覆盖 roundtrip、落单代理项、上限边界与 1MiB 长度闸共存。
This commit is contained in:
parent
5079d33b0a
commit
38df106fb6
@ -16,7 +16,10 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
///
|
||||
/// - v1:初版(无版本字段,隐式)
|
||||
/// - v2:`Auth`/`AuthOk` 携带版本;帧头增加 `seq`/`keyframe`
|
||||
pub const PROTO_VERSION: u16 = 2;
|
||||
/// - v3:新增 `InputEvent::Text`(文本注入通道,UTF-16 code unit)。
|
||||
/// `InputEvent` 因携带 `Vec<u16>` 不再是 `Copy`;三端必须同步升级,
|
||||
/// 旧端由 `Auth` 握手显式拒绝。
|
||||
pub const PROTO_VERSION: u16 = 3;
|
||||
|
||||
/// 控制通道消息(双向)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -47,9 +50,12 @@ pub enum MouseButton {
|
||||
Middle,
|
||||
}
|
||||
|
||||
/// 输入事件。鼠标坐标为虚拟桌面归一化 0..=65535;
|
||||
/// 输入事件。鼠标坐标为**被采集显示器**(主屏)归一化 0..=65535
|
||||
/// (协议 v2 语义,v3 未改;`agent/src/input.rs` 的注入空间与此一致,见个人层 lessons R-4);
|
||||
/// 按键为 Set-1 扫描码,extended 标记扩展键(方向键/Home 等,需要 EXTENDEDKEY 标志)。
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
///
|
||||
/// v3 起本类型不再 `Copy`:`Text` 携带堆上 `Vec<u16>`。需要复制的地方用 `.clone()`。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum InputEvent {
|
||||
MouseMove { x: u16, y: u16 },
|
||||
MouseDown(MouseButton),
|
||||
@ -57,6 +63,42 @@ pub enum InputEvent {
|
||||
Wheel { dy: i32 },
|
||||
KeyDown { scancode: u16, extended: bool },
|
||||
KeyUp { scancode: u16, extended: bool },
|
||||
/// v3 新增:**文本注入通道**。`units` 为 UTF-16 code unit 序列。
|
||||
///
|
||||
/// 为什么不是 `String`:Windows 注入侧(`KEYEVENTF_UNICODE`)与 Java 字符串的
|
||||
/// 原生表示都是 UTF-16 code unit;用 `String` 要走 UTF-8 往返,遇到 IME 提交边界
|
||||
/// 产生的落单代理项会**有损**。`Vec<u16>` 忠实透传,由接收端按 Win32 语义处理。
|
||||
///
|
||||
/// 为什么整段携带、而不是逐 code unit 一条消息:一次 IME 候选确认常是多个汉字;
|
||||
/// 代理对(emoji 等增补平面字符)**必须在同一次注入批次里成对提交**才能被目标
|
||||
/// 应用合成成一个字符,拆开注入必现乱码。
|
||||
///
|
||||
/// 上限 [`MAX_TEXT_UNITS`]:超长输入截断且两侧打警告日志(不静默丢弃)。
|
||||
/// 另一道防线是 [`read_control`] 的 1 MiB 消息长度闸。
|
||||
///
|
||||
/// 日志纪律:中间层与注入端只允许打印 `units.len()`,**不得**打印用户输入内容。
|
||||
Text { units: Vec<u16> },
|
||||
}
|
||||
|
||||
/// [`InputEvent::Text`] 单次携带的 code unit 上限。
|
||||
///
|
||||
/// 与 `read_control` 的 1 MiB 消息闸是两道独立防线:前者防恶意对端把接收端分配打爆,
|
||||
/// 后者防合法但异常长的 IME 提交把注入队列刷爆。4096 unit ≈ 8 KiB,
|
||||
/// 远超正常 IME 一次提交(数十 unit),又足够小使单次 `SendInput` 批量仍可接受。
|
||||
pub const MAX_TEXT_UNITS: usize = 4096;
|
||||
|
||||
/// 把文本输入截断到 [`MAX_TEXT_UNITS`],返回 `(可注入的 units, 是否发生截断)`。
|
||||
///
|
||||
/// 调用方**必须**在截断为真时打印日志(可见后果);本函数只负责机械截断,
|
||||
/// 不自己打印——因为 `common` 同时被主控端与被控端复用,日志口径由调用方决定。
|
||||
pub fn clamp_text_units(units: Vec<u16>) -> (Vec<u16>, bool) {
|
||||
if units.len() > MAX_TEXT_UNITS {
|
||||
let mut trimmed = units;
|
||||
trimmed.truncate(MAX_TEXT_UNITS);
|
||||
(trimmed, true)
|
||||
} else {
|
||||
(units, false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 视频帧头。载荷为 H.264 Annex-B 码流(含 00 00 00 01 起始码)。
|
||||
@ -269,6 +311,88 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- v3 文本注入通道 ----
|
||||
|
||||
/// 版本守卫:v3 是本轮契约变更的登记点,改动必须显式经过这里。
|
||||
#[test]
|
||||
fn proto_version_is_three_after_text_event() {
|
||||
assert_eq!(PROTO_VERSION, 3, "新增 InputEvent::Text 后必须递增到 3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_event_roundtrips_utf16_units() {
|
||||
// 「你好」+ 一个 emoji 的代理对(U+1F600 = D83D DE00)
|
||||
let units: Vec<u16> = vec![0x4F60, 0x597D, 0xD83D, 0xDE00];
|
||||
let m = ControlMsg::Input(InputEvent::Text {
|
||||
units: units.clone(),
|
||||
});
|
||||
let bytes = encode_control(&m);
|
||||
let back: ControlMsg = bincode::deserialize(&bytes[4..]).unwrap();
|
||||
match back {
|
||||
ControlMsg::Input(InputEvent::Text { units: got }) => assert_eq!(got, units),
|
||||
other => panic!("期望 Input::Text,实际 {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 落单代理项必须原样往返:IME 提交边界可能给出孤立高/低代理项,
|
||||
/// 用 `String` 承载会在这里有损(这正是选 `Vec<u16>` 的理由)。
|
||||
#[test]
|
||||
fn text_event_keeps_lone_surrogate() {
|
||||
let units = vec![0xD83Du16];
|
||||
let m = ControlMsg::Input(InputEvent::Text { units: units.clone() });
|
||||
let bytes = encode_control(&m);
|
||||
let back: ControlMsg = bincode::deserialize(&bytes[4..]).unwrap();
|
||||
match back {
|
||||
ControlMsg::Input(InputEvent::Text { units: got }) => assert_eq!(got, units),
|
||||
other => panic!("期望 Input::Text,实际 {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_units_clamped_at_boundary() {
|
||||
// 边界:恰好上限不截断;超一个才截断
|
||||
let exact = vec![0x61u16; MAX_TEXT_UNITS];
|
||||
let (kept, truncated) = clamp_text_units(exact);
|
||||
assert_eq!(kept.len(), MAX_TEXT_UNITS);
|
||||
assert!(!truncated);
|
||||
|
||||
let over = vec![0x61u16; MAX_TEXT_UNITS + 1];
|
||||
let (kept, truncated) = clamp_text_units(over);
|
||||
assert_eq!(kept.len(), MAX_TEXT_UNITS);
|
||||
assert!(truncated, "超限必须返回截断标志,让调用方打印警告");
|
||||
|
||||
let (empty, truncated) = clamp_text_units(vec![]);
|
||||
assert!(empty.is_empty());
|
||||
assert!(!truncated);
|
||||
}
|
||||
|
||||
/// 空 `Text` 也必须能往返(IME 可能提交空串;调用方负责决定忽略还是报错)。
|
||||
#[test]
|
||||
fn text_event_empty_roundtrips() {
|
||||
let m = ControlMsg::Input(InputEvent::Text { units: vec![] });
|
||||
let bytes = encode_control(&m);
|
||||
let back: ControlMsg = bincode::deserialize(&bytes[4..]).unwrap();
|
||||
match back {
|
||||
ControlMsg::Input(InputEvent::Text { units }) => assert!(units.is_empty()),
|
||||
other => panic!("期望 Input::Text,实际 {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 上限内的 `Text` 必须能通过 `read_control` 的长度闸;
|
||||
/// 反向验证:伪造一个超限的声明长度必须在分配前被拒。
|
||||
#[tokio::test]
|
||||
async fn text_within_cap_passes_control_length_gate() {
|
||||
let (mut a, mut b) = duplex(1 << 20);
|
||||
let msg = ControlMsg::Input(InputEvent::Text {
|
||||
units: vec![0x4F60; MAX_TEXT_UNITS],
|
||||
});
|
||||
write_control(&mut a, &msg).await.unwrap();
|
||||
match read_control(&mut b).await.unwrap() {
|
||||
ControlMsg::Input(InputEvent::Text { units }) => assert_eq!(units.len(), MAX_TEXT_UNITS),
|
||||
other => panic!("期望 Input::Text,实际 {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 视频帧头 ----
|
||||
|
||||
#[test]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user