diff --git a/common/src/lib.rs b/common/src/lib.rs index f3d8891..57a5867 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -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` 不再是 `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`。需要复制的地方用 `.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` 忠实透传,由接收端按 Win32 语义处理。 + /// + /// 为什么整段携带、而不是逐 code unit 一条消息:一次 IME 候选确认常是多个汉字; + /// 代理对(emoji 等增补平面字符)**必须在同一次注入批次里成对提交**才能被目标 + /// 应用合成成一个字符,拆开注入必现乱码。 + /// + /// 上限 [`MAX_TEXT_UNITS`]:超长输入截断且两侧打警告日志(不静默丢弃)。 + /// 另一道防线是 [`read_control`] 的 1 MiB 消息长度闸。 + /// + /// 日志纪律:中间层与注入端只允许打印 `units.len()`,**不得**打印用户输入内容。 + Text { units: Vec }, +} + +/// [`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) -> (Vec, 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 = 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` 的理由)。 + #[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]