diff --git a/android-ffi/src/lib.rs b/android-ffi/src/lib.rs index 551cd2a..5de1ffb 100644 --- a/android-ffi/src/lib.rs +++ b/android-ffi/src/lib.rs @@ -34,6 +34,90 @@ use viewer_core::{ConnectConfig, Session}; /// (v5:25→30,尾部三字段是光标;放末尾保证 Kotlin 旧偏移量全部不移动) const FRAME_HEADER_LEN: usize = 30; +/// 把一个帧的元数据打包成定长头字节(**不含载荷**)。 +/// +/// 独立成纯函数并被单测钉住的原因:Kotlin 侧 `FrameCodec` 按同样的偏移读这些字节, +/// 任一侧挪一位,两端都编译得过、真机也连得上,只是标记画错位置——静默失败。 +/// 单测用**手写字面量**断言(不是"算出来的期望值"),这样 RustBridgeTest 的 +/// golden bytes 与这里是同一组常数对拍:改了写入布局,两边至少一边必红。 +fn pack_frame_header(f: &viewer_core::Frame) -> [u8; FRAME_HEADER_LEN] { + let mut head = [0u8; FRAME_HEADER_LEN]; + head[0] = f.keyframe as u8; + head[1..9].copy_from_slice(&f.seq.to_le_bytes()); + head[9..17].copy_from_slice(&f.ts_ms.to_le_bytes()); + head[17..21].copy_from_slice(&f.width.to_le_bytes()); + head[21..25].copy_from_slice(&f.height.to_le_bytes()); + // v5 光标三字段(偏移 25/26/28):None → has=0 且坐标字节保持 0(无意义)。 + // 用显式 match 而不是 `f.cursor.is_some() as u8`——两处读同一个 Option, + // 任何「Some 但忘写坐标」的编辑错误都会在这里暴露。 + match f.cursor { + Some((x, y)) => { + head[25] = 1; + head[26..28].copy_from_slice(&x.to_le_bytes()); + head[28..30].copy_from_slice(&y.to_le_bytes()); + } + None => head[25] = 0, + } + head +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 构造最小 Frame(载荷只放一字节,本测试只关心定长头)。 + fn frame(seq: u64, cursor: Option<(i16, i16)>) -> viewer_core::Frame { + viewer_core::Frame { + width: 1920, + height: 1080, + ts_ms: 0x0011_2233_4455_6677, + seq, + keyframe: true, + cursor, + payload: vec![0xAB], + } + } + + /// **写端**字面量对拍:期望值逐字节手写,与 Kotlin 侧 RustBridgeTest 的 + /// goldenHeader 是同一组常数。两侧各握一份手抄常数,改任何一侧的偏移, + /// 另一侧必红——这才是跨端契约的闭环(golden 测试只锁读端时有盲区: + /// 若 Kotlin 的字面量本身抄错了 Rust 布局,读端测试会跟着一起错还全绿)。 + #[test] + fn header_bytes_match_the_declared_layout() { + let head = pack_frame_header(&frame(0x0102_0304_0506_0708, Some((960, 540)))); + assert_eq!( + head, + [ + 0x01, // [0] keyframe=true + 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, // [1..9] seq=0x0102..0708 LE + 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, // [9..17] ts LE + 0x80, 0x07, 0x00, 0x00, // [17..21] w=1920=0x0780 LE + 0x38, 0x04, 0x00, 0x00, // [21..25] h=1080=0x0438 LE + 0x01, // [25] has_cursor + 0xC0, 0x03, // [26..28] x=960=0x03C0 LE + 0x1C, 0x02, // [28..30] y=540=0x021C LE + ] + ); + } + + /// None 分支:has=0 且**坐标字节必须为 0**(vec 初始化保证),Kotlin 侧 + /// 测试锁「has=0 时无条件 null」,这里锁「写端不残留脏坐标」。 + #[test] + fn none_cursor_writes_zero_flag_and_zero_coords() { + let head = pack_frame_header(&frame(1, None)); + assert_eq!(head[25], 0); + assert_eq!(&head[26..30], &[0u8; 4]); + } + + /// 负坐标(协议层不该到达,但写端要忠实 i16 位模式——符号截断是静默错位)。 + #[test] + fn negative_coords_keep_bit_pattern() { + let head = pack_frame_header(&frame(1, Some((-1, -32768)))); + assert_eq!(&head[26..28], &[0xFF, 0xFF]); + assert_eq!(&head[28..30], &[0x00, 0x80]); + } +} + struct SessionBox { session: Arc, runtime: tokio::runtime::Runtime, @@ -167,24 +251,11 @@ pub extern "system" fn Java_com_verify_remote_RustBridge_nativeNextFrame<'local> match rt.block_on(session.next_frame()) { None => std::ptr::null_mut(), Some(f) => { - let mut buf = vec![0u8; FRAME_HEADER_LEN + f.payload.len()]; - buf[0] = f.keyframe as u8; - buf[1..9].copy_from_slice(&f.seq.to_le_bytes()); - buf[9..17].copy_from_slice(&f.ts_ms.to_le_bytes()); - buf[17..21].copy_from_slice(&f.width.to_le_bytes()); - buf[21..25].copy_from_slice(&f.height.to_le_bytes()); - // v5 光标三字段(偏移 25/26/28):None → has=0 且坐标字节保持 0(无意义)。 - // Option 展开成显式分支而不是 f.cursor.is_some() as u8——两处读同一个 - // Option,任何「Some 但忘写坐标」的编辑错误都会在这里编译期暴露。 - match f.cursor { - Some((x, y)) => { - buf[25] = 1; - buf[26..28].copy_from_slice(&x.to_le_bytes()); - buf[28..30].copy_from_slice(&y.to_le_bytes()); - } - None => buf[25] = 0, - } - buf[FRAME_HEADER_LEN..].copy_from_slice(&f.payload); + // 布局单一真源是 pack_frame_header(那里有字面量对拍单测) + let head = pack_frame_header(&f); + let mut buf = Vec::with_capacity(FRAME_HEADER_LEN + f.payload.len()); + buf.extend_from_slice(head.as_slice()); + buf.extend_from_slice(&f.payload); match env.byte_array_from_slice(&buf) { Ok(a) => a.into_raw(), Err(_) => std::ptr::null_mut(),