From 3c214c2a46372be3e73b54820b02d5dd2294b004 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:10:30 -0700 Subject: [PATCH 01/57] test(network): require bounded BiDi text message assembly --- ...bdriver_bidi_websocket_message_assembly.rs | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_websocket_message_assembly.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_message_assembly.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_message_assembly.rs new file mode 100644 index 000000000..fe546258f --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_message_assembly.rs @@ -0,0 +1,191 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketControlKind, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketMessageError, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +fn connect( + endpoint: &str, +) -> Result> { + let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint)?; + let correlated = admitted.correlate_session_id(SESSION_ID)?; + let target = correlated.into_explicit_connect_target()?; + let plan = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?; + Ok(plan.connect()?) +} + +fn establish( + endpoint: &str, +) -> Result> { + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let plan = WebDriverBiDiWebSocketHandshakePlan::new(connect(endpoint)?, key)?; + let written = plan.write_opening_request(Duration::from_millis(500))?; + Ok(written.read_opening_response(Duration::from_millis(500))?) +} + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn write_server_frame( + stream: &mut TcpStream, + fin: bool, + opcode: u8, + payload: &[u8], +) -> io::Result<()> { + let first = if fin { 0x80 | opcode } else { opcode }; + stream.write_all(&[first])?; + match payload.len() { + 0..=125 => stream.write_all(&[payload.len() as u8])?, + 126..=65_535 => { + stream.write_all(&[126])?; + stream.write_all(&(payload.len() as u16).to_be_bytes())?; + } + _ => { + stream.write_all(&[127])?; + stream.write_all(&(payload.len() as u64).to_be_bytes())?; + } + } + stream.write_all(payload) +} + +#[test] +fn message_assembler_reassembles_split_utf8_around_interleaved_ping() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + write_server_frame(&mut stream, false, 0x1, b"A\xe2")?; + write_server_frame(&mut stream, true, 0x9, b"probe")?; + write_server_frame(&mut stream, true, 0x0, b"\x82\xacB") + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let established = establish(&endpoint)?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + + let (established, first) = established.read_frame(Duration::from_millis(500))?; + assert!(matches!( + assembler.push_frame(first)?, + WebDriverBiDiWebSocketMessageAssembly::Pending + )); + + let (established, ping) = established.read_frame(Duration::from_millis(500))?; + let control = match assembler.push_frame(ping)? { + WebDriverBiDiWebSocketMessageAssembly::Control(control) => control, + other => { + return Err(io::Error::other(format!( + "interleaved Ping produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + assert_eq!(control.kind(), WebDriverBiDiWebSocketControlKind::Ping); + assert_eq!(control.payload(), b"probe"); + + let (_established, continuation) = established.read_frame(Duration::from_millis(500))?; + let text = match assembler.push_frame(continuation)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "final continuation produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + assert_eq!(text.as_str(), "A€B"); + + server + .join() + .map_err(|_| io::Error::other("fragmented-message server panicked"))??; + Ok(()) +} + +#[test] +fn message_assembler_fails_closed_after_binary_message() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + write_server_frame(&mut stream, true, 0x2, b"binary")?; + write_server_frame(&mut stream, true, 0x1, b"later-text") + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let established = establish(&endpoint)?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + + let (established, binary) = established.read_frame(Duration::from_millis(500))?; + assert!(matches!( + assembler.push_frame(binary), + Err(WebDriverBiDiWebSocketMessageError::UnexpectedBinaryMessage) + )); + + let (_established, later_text) = established.read_frame(Duration::from_millis(500))?; + assert!(matches!( + assembler.push_frame(later_text), + Err(WebDriverBiDiWebSocketMessageError::AssemblerPoisoned) + )); + + server + .join() + .map_err(|_| io::Error::other("binary-message server panicked"))??; + Ok(()) +} + +#[test] +fn message_assembler_rejects_continuation_without_text_start() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + write_server_frame(&mut stream, true, 0x0, b"orphan") + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let established = establish(&endpoint)?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let (_established, continuation) = established.read_frame(Duration::from_millis(500))?; + assert!(matches!( + assembler.push_frame(continuation), + Err(WebDriverBiDiWebSocketMessageError::UnexpectedContinuation) + )); + + server + .join() + .map_err(|_| io::Error::other("orphan-continuation server panicked"))??; + Ok(()) +} From 54aa78460557f0c9b4af5300ac4417661b3af2dc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:12:08 -0700 Subject: [PATCH 02/57] test(network): canonicalize BiDi message regression --- .../tests/webdriver_bidi_websocket_message_assembly.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_message_assembly.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_message_assembly.rs index fe546258f..fed062dd3 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_message_assembly.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_message_assembly.rs @@ -77,7 +77,8 @@ fn write_server_frame( } #[test] -fn message_assembler_reassembles_split_utf8_around_interleaved_ping() -> Result<(), Box> { +fn message_assembler_reassembles_split_utf8_around_interleaved_ping() -> Result<(), Box> +{ let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { From 851c4b9b32fee1432128f03152582f58d683925a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:15:32 -0700 Subject: [PATCH 03/57] feat(network): add bounded BiDi text message assembler --- .../src/webdriver_bidi_websocket_message.rs | 559 ++++++++++++++++++ 1 file changed, 559 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_websocket_message.rs diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs new file mode 100644 index 000000000..6d95dfb55 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -0,0 +1,559 @@ +use std::{error::Error, fmt, str}; + +use crate::{MAX_WEBSOCKET_FRAME_PAYLOAD_SIZE, WebDriverBiDiWebSocketFrame}; + +/// Maximum UTF-8 payload bytes admitted for one assembled WebDriver BiDi WebSocket message. +/// +/// The aggregate message bound intentionally matches the existing per-frame data bound so +/// fragmentation cannot be used to bypass the reviewed 1 MiB transport resource budget. +pub const MAX_WEBDRIVER_BIDI_MESSAGE_SIZE: usize = MAX_WEBSOCKET_FRAME_PAYLOAD_SIZE; + +/// Semantic kind of one bounded RFC 6455 control frame observed between BiDi message fragments. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiWebSocketControlKind { + /// The peer sent a Close control frame. + Close, + /// The peer sent a Ping control frame. + Ping, + /// The peer sent a Pong control frame. + Pong, +} + +/// One bounded WebSocket control message retained without exposing application text. +#[derive(Eq, PartialEq)] +pub struct WebDriverBiDiWebSocketControlMessage { + kind: WebDriverBiDiWebSocketControlKind, + payload: Vec, +} + +impl fmt::Debug for WebDriverBiDiWebSocketControlMessage { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiWebSocketControlMessage") + .field("kind", &self.kind) + .field("payload_bytes", &self.payload.len()) + .finish() + } +} + +impl WebDriverBiDiWebSocketControlMessage { + /// Return the exact control-frame kind. + #[must_use] + pub const fn kind(&self) -> WebDriverBiDiWebSocketControlKind { + self.kind + } + + /// Borrow the bounded control payload. + #[must_use] + pub fn payload(&self) -> &[u8] { + &self.payload + } +} + +/// One complete validated UTF-8 WebDriver BiDi WebSocket text message. +#[derive(Eq, PartialEq)] +pub struct WebDriverBiDiWebSocketTextMessage(String); + +impl fmt::Debug for WebDriverBiDiWebSocketTextMessage { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiWebSocketTextMessage") + .field("payload_bytes", &self.0.len()) + .finish() + } +} + +impl WebDriverBiDiWebSocketTextMessage { + /// Borrow the complete validated UTF-8 message text. + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } +} + +/// Result of admitting one RFC 6455 frame into the bounded BiDi message assembler. +#[derive(Debug, Eq, PartialEq)] +pub enum WebDriverBiDiWebSocketMessageAssembly { + /// A fragmented text message is still incomplete. + Pending, + /// One complete UTF-8 text message is ready for the later BiDi JSON layer. + Text(WebDriverBiDiWebSocketTextMessage), + /// One RFC 6455 control message was observed without disturbing partial text state. + Control(WebDriverBiDiWebSocketControlMessage), +} + +/// Fail-closed semantic failures while assembling WebDriver BiDi WebSocket text messages. +#[derive(Debug, Eq, PartialEq)] +pub enum WebDriverBiDiWebSocketMessageError { + /// The assembler is terminal after a prior semantic failure or peer Close frame. + AssemblerPoisoned, + /// A continuation frame arrived without an active fragmented text message. + UnexpectedContinuation, + /// WebDriver BiDi requires text WebSocket messages; binary data is not admitted. + UnexpectedBinaryMessage, + /// A new text frame began before the active fragmented text message completed. + InterruptedFragmentedText, + /// Aggregate fragmented message bytes exceeded the reviewed resource bound. + MessageTooLarge { + /// Aggregate payload bytes that the attempted append would produce. + payload_bytes: usize, + /// Maximum aggregate payload bytes admitted by this boundary. + maximum_bytes: usize, + }, + /// The complete text message was not valid UTF-8. + InvalidTextUtf8, + /// A frame opcode escaped the lower RFC 6455 validation layer unexpectedly. + UnsupportedFrameOpcode { + /// Unexpected RFC 6455 opcode. + opcode: u8, + }, +} + +impl fmt::Display for WebDriverBiDiWebSocketMessageError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AssemblerPoisoned => formatter.write_str( + "WebDriver BiDi WebSocket message assembly is terminal after failure or Close", + ), + Self::UnexpectedContinuation => formatter.write_str( + "WebDriver BiDi WebSocket continuation arrived without fragmented text", + ), + Self::UnexpectedBinaryMessage => formatter.write_str( + "WebDriver BiDi requires WebSocket text messages; binary message rejected", + ), + Self::InterruptedFragmentedText => formatter.write_str( + "WebDriver BiDi fragmented text was interrupted by a new data message", + ), + Self::MessageTooLarge { + payload_bytes, + maximum_bytes, + } => write!( + formatter, + "WebDriver BiDi WebSocket message has {payload_bytes} bytes; maximum is {maximum_bytes}" + ), + Self::InvalidTextUtf8 => { + formatter.write_str("WebDriver BiDi WebSocket text message is not valid UTF-8") + } + Self::UnsupportedFrameOpcode { opcode } => write!( + formatter, + "unexpected RFC 6455 opcode escaped frame validation: {opcode:#04x}" + ), + } + } +} + +impl Error for WebDriverBiDiWebSocketMessageError {} + +/// Stateful bounded assembler for the text-message semantics required by WebDriver BiDi. +/// +/// The lower frame layer owns RFC 6455 framing and wire validation. This layer only joins text and +/// continuation payloads, preserves interleaved control frames, rejects binary messages, validates +/// UTF-8 after the complete message exists, and enforces an aggregate 1 MiB message budget. Any +/// semantic protocol failure makes the assembler terminal so callers cannot accidentally recover +/// authority from a corrupted message sequence. A peer Close frame is returned once and likewise +/// makes subsequent message assembly terminal. +pub struct WebDriverBiDiWebSocketMessageAssembler { + fragmented_text: Option>, + poisoned: bool, +} + +impl fmt::Debug for WebDriverBiDiWebSocketMessageAssembler { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiWebSocketMessageAssembler") + .field( + "fragmented_payload_bytes", + &self.fragmented_text.as_ref().map_or(0, Vec::len), + ) + .field("terminal", &self.poisoned) + .finish() + } +} + +impl WebDriverBiDiWebSocketMessageAssembler { + /// Create one empty assembler with no inherited message state. + #[must_use] + pub const fn new() -> Self { + Self { + fragmented_text: None, + poisoned: false, + } + } + + /// Admit one already-validated RFC 6455 frame into the BiDi text-message state machine. + pub fn push_frame( + &mut self, + frame: WebDriverBiDiWebSocketFrame, + ) -> Result { + self.push_parts(frame.fin(), frame.opcode(), frame.payload()) + } + + fn push_parts( + &mut self, + fin: bool, + opcode: u8, + payload: &[u8], + ) -> Result { + if self.poisoned { + return Err(WebDriverBiDiWebSocketMessageError::AssemblerPoisoned); + } + match opcode { + 0x0 => self.push_continuation(fin, payload), + 0x1 => self.push_text(fin, payload), + 0x2 => self.reject(WebDriverBiDiWebSocketMessageError::UnexpectedBinaryMessage), + 0x8 => { + self.fragmented_text = None; + self.poisoned = true; + Ok(WebDriverBiDiWebSocketMessageAssembly::Control( + WebDriverBiDiWebSocketControlMessage { + kind: WebDriverBiDiWebSocketControlKind::Close, + payload: payload.to_vec(), + }, + )) + } + 0x9 => Ok(Self::control( + WebDriverBiDiWebSocketControlKind::Ping, + payload, + )), + 0xa => Ok(Self::control( + WebDriverBiDiWebSocketControlKind::Pong, + payload, + )), + _ => self.reject(WebDriverBiDiWebSocketMessageError::UnsupportedFrameOpcode { opcode }), + } + } + + fn push_text( + &mut self, + fin: bool, + payload: &[u8], + ) -> Result { + if self.fragmented_text.is_some() { + return self.reject(WebDriverBiDiWebSocketMessageError::InterruptedFragmentedText); + } + if payload.len() > MAX_WEBDRIVER_BIDI_MESSAGE_SIZE { + return self.reject(WebDriverBiDiWebSocketMessageError::MessageTooLarge { + payload_bytes: payload.len(), + maximum_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, + }); + } + if fin { + return Self::complete_text(payload.to_vec()) + .map(WebDriverBiDiWebSocketMessageAssembly::Text) + .map_err(|error| { + self.fragmented_text = None; + self.poisoned = true; + error + }); + } + self.fragmented_text = Some(payload.to_vec()); + Ok(WebDriverBiDiWebSocketMessageAssembly::Pending) + } + + fn push_continuation( + &mut self, + fin: bool, + payload: &[u8], + ) -> Result { + let Some(current_len) = self.fragmented_text.as_ref().map(Vec::len) else { + return self.reject(WebDriverBiDiWebSocketMessageError::UnexpectedContinuation); + }; + if payload.len() > MAX_WEBDRIVER_BIDI_MESSAGE_SIZE - current_len { + return self.reject(WebDriverBiDiWebSocketMessageError::MessageTooLarge { + payload_bytes: current_len.saturating_add(payload.len()), + maximum_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, + }); + } + let buffer = self + .fragmented_text + .as_mut() + .expect("fragmented text presence was established above"); + buffer.extend_from_slice(payload); + if !fin { + return Ok(WebDriverBiDiWebSocketMessageAssembly::Pending); + } + let complete = self + .fragmented_text + .take() + .expect("fragmented text presence was established above"); + Self::complete_text(complete) + .map(WebDriverBiDiWebSocketMessageAssembly::Text) + .map_err(|error| { + self.poisoned = true; + error + }) + } + + fn complete_text( + payload: Vec, + ) -> Result { + String::from_utf8(payload) + .map(WebDriverBiDiWebSocketTextMessage) + .map_err(|_| WebDriverBiDiWebSocketMessageError::InvalidTextUtf8) + } + + fn control( + kind: WebDriverBiDiWebSocketControlKind, + payload: &[u8], + ) -> WebDriverBiDiWebSocketMessageAssembly { + WebDriverBiDiWebSocketMessageAssembly::Control(WebDriverBiDiWebSocketControlMessage { + kind, + payload: payload.to_vec(), + }) + } + + fn reject( + &mut self, + error: WebDriverBiDiWebSocketMessageError, + ) -> Result { + self.fragmented_text = None; + self.poisoned = true; + Err(error) + } +} + +impl Default for WebDriverBiDiWebSocketMessageAssembler { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn text( + assembly: WebDriverBiDiWebSocketMessageAssembly, + ) -> WebDriverBiDiWebSocketTextMessage { + match assembly { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => panic!("expected text message, got {other:?}"), + } + } + + fn control( + assembly: WebDriverBiDiWebSocketMessageAssembly, + ) -> WebDriverBiDiWebSocketControlMessage { + match assembly { + WebDriverBiDiWebSocketMessageAssembly::Control(control) => control, + other => panic!("expected control message, got {other:?}"), + } + } + + #[test] + fn complete_text_and_debug_are_payload_redacted() { + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::default(); + let message = text( + assembler + .push_parts(true, 0x1, b"secret-text") + .expect("complete text"), + ); + assert_eq!(message.as_str(), "secret-text"); + let message_debug = format!("{message:?}"); + assert!(message_debug.contains("payload_bytes: 11")); + assert!(!message_debug.contains("secret-text")); + let assembler_debug = format!("{assembler:?}"); + assert!(assembler_debug.contains("fragmented_payload_bytes: 0")); + assert!(assembler_debug.contains("terminal: false")); + } + + #[test] + fn fragments_reassemble_only_after_final_continuation() { + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + assert_eq!( + assembler + .push_parts(false, 0x1, b"A\xe2") + .expect("text start"), + WebDriverBiDiWebSocketMessageAssembly::Pending + ); + assert_eq!( + assembler + .push_parts(false, 0x0, b"\x82") + .expect("middle continuation"), + WebDriverBiDiWebSocketMessageAssembly::Pending + ); + let message = text( + assembler + .push_parts(true, 0x0, b"\xacB") + .expect("final continuation"), + ); + assert_eq!(message.as_str(), "A€B"); + } + + #[test] + fn ping_and_pong_preserve_fragmented_text_state() { + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + assembler + .push_parts(false, 0x1, b"left-") + .expect("fragment start"); + let ping = control( + assembler + .push_parts(true, 0x9, b"ping-data") + .expect("ping control"), + ); + assert_eq!(ping.kind(), WebDriverBiDiWebSocketControlKind::Ping); + assert_eq!(ping.payload(), b"ping-data"); + let ping_debug = format!("{ping:?}"); + assert!(ping_debug.contains("payload_bytes: 9")); + assert!(!ping_debug.contains("ping-data")); + + let pong = control( + assembler + .push_parts(true, 0xa, b"pong") + .expect("pong control"), + ); + assert_eq!(pong.kind(), WebDriverBiDiWebSocketControlKind::Pong); + assert_eq!(pong.payload(), b"pong"); + let message = text( + assembler + .push_parts(true, 0x0, b"right") + .expect("final continuation"), + ); + assert_eq!(message.as_str(), "left-right"); + } + + #[test] + fn close_is_returned_once_and_makes_assembler_terminal() { + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + assembler + .push_parts(false, 0x1, b"discarded") + .expect("fragment start"); + let close = control( + assembler + .push_parts(true, 0x8, b"bye") + .expect("close control"), + ); + assert_eq!(close.kind(), WebDriverBiDiWebSocketControlKind::Close); + assert_eq!(close.payload(), b"bye"); + assert_eq!( + assembler.push_parts(true, 0x1, b"later"), + Err(WebDriverBiDiWebSocketMessageError::AssemblerPoisoned) + ); + } + + #[test] + fn semantic_data_sequence_errors_fail_closed() { + let cases = [ + ( + 0x0, + WebDriverBiDiWebSocketMessageError::UnexpectedContinuation, + ), + ( + 0x2, + WebDriverBiDiWebSocketMessageError::UnexpectedBinaryMessage, + ), + ( + 0x3, + WebDriverBiDiWebSocketMessageError::UnsupportedFrameOpcode { opcode: 0x3 }, + ), + ]; + for (opcode, expected) in cases { + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + assert_eq!(assembler.push_parts(true, opcode, b"x"), Err(expected)); + assert_eq!( + assembler.push_parts(true, 0x1, b"later"), + Err(WebDriverBiDiWebSocketMessageError::AssemblerPoisoned) + ); + } + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + assembler + .push_parts(false, 0x1, b"partial") + .expect("fragment start"); + assert_eq!( + assembler.push_parts(true, 0x1, b"new-message"), + Err(WebDriverBiDiWebSocketMessageError::InterruptedFragmentedText) + ); + } + + #[test] + fn aggregate_message_bound_rejects_fragmentation_bypass() { + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let maximum = vec![b'x'; MAX_WEBDRIVER_BIDI_MESSAGE_SIZE]; + assembler + .push_parts(false, 0x1, &maximum) + .expect("maximum-sized first fragment"); + assert_eq!( + assembler.push_parts(true, 0x0, b"y"), + Err(WebDriverBiDiWebSocketMessageError::MessageTooLarge { + payload_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE + 1, + maximum_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, + }) + ); + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let oversized = vec![b'x'; MAX_WEBDRIVER_BIDI_MESSAGE_SIZE + 1]; + assert_eq!( + assembler.push_parts(false, 0x1, &oversized), + Err(WebDriverBiDiWebSocketMessageError::MessageTooLarge { + payload_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE + 1, + maximum_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, + }) + ); + } + + #[test] + fn utf8_validation_waits_for_complete_message_and_then_fails_closed() { + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + assert_eq!( + assembler.push_parts(true, 0x1, b"\xff"), + Err(WebDriverBiDiWebSocketMessageError::InvalidTextUtf8) + ); + assert_eq!( + assembler.push_parts(true, 0x1, b"later"), + Err(WebDriverBiDiWebSocketMessageError::AssemblerPoisoned) + ); + + let mut fragmented = WebDriverBiDiWebSocketMessageAssembler::new(); + fragmented + .push_parts(false, 0x1, b"\xe2") + .expect("partial UTF-8 is allowed until completion"); + assert_eq!( + fragmented.push_parts(true, 0x0, b"x"), + Err(WebDriverBiDiWebSocketMessageError::InvalidTextUtf8) + ); + } + + #[test] + fn public_error_contract_is_stable_and_source_free() { + let cases = [ + ( + WebDriverBiDiWebSocketMessageError::AssemblerPoisoned, + "WebDriver BiDi WebSocket message assembly is terminal after failure or Close", + ), + ( + WebDriverBiDiWebSocketMessageError::UnexpectedContinuation, + "WebDriver BiDi WebSocket continuation arrived without fragmented text", + ), + ( + WebDriverBiDiWebSocketMessageError::UnexpectedBinaryMessage, + "WebDriver BiDi requires WebSocket text messages; binary message rejected", + ), + ( + WebDriverBiDiWebSocketMessageError::InterruptedFragmentedText, + "WebDriver BiDi fragmented text was interrupted by a new data message", + ), + ( + WebDriverBiDiWebSocketMessageError::InvalidTextUtf8, + "WebDriver BiDi WebSocket text message is not valid UTF-8", + ), + ( + WebDriverBiDiWebSocketMessageError::UnsupportedFrameOpcode { opcode: 0x3 }, + "unexpected RFC 6455 opcode escaped frame validation: 0x03", + ), + ]; + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + assert!(error.source().is_none()); + } + let too_large = WebDriverBiDiWebSocketMessageError::MessageTooLarge { + payload_bytes: 10, + maximum_bytes: 9, + }; + assert_eq!( + too_large.to_string(), + "WebDriver BiDi WebSocket message has 10 bytes; maximum is 9" + ); + assert!(too_large.source().is_none()); + } +} From c2908e3cc21ba2a1e7f1d452ee7f6ee3464c9500 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:15:49 -0700 Subject: [PATCH 04/57] feat(network): export BiDi text message contract --- crates/originweave-network/src/lib.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index efea5b801..aa6867f72 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -5,9 +5,9 @@ //! peers before exposing transport I/O, and emits credential-free evidence. //! It also bridges a session-correlated WebDriver BiDi loopback target from //! `originweave-core` into one bounded exact TCP connection, binds and validates -//! the RFC 6455 opening exchange, and provides bounded masked client writes and -//! unmasked server-frame reads without granting browser, TLS, policy, or Agent -//! authority. +//! the RFC 6455 opening exchange, provides bounded masked client writes and +//! unmasked server-frame reads, and assembles bounded WebDriver BiDi text messages +//! without granting browser, TLS, policy, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -16,6 +16,7 @@ mod connection; mod webdriver_bidi_connection; mod webdriver_bidi_websocket_frame; mod webdriver_bidi_websocket_handshake; +mod webdriver_bidi_websocket_message; mod webdriver_bidi_websocket_opening_recovery; pub use connection::{ @@ -38,4 +39,10 @@ pub use webdriver_bidi_websocket_handshake::{ WebDriverBiDiWebSocketHandshakeError, WebDriverBiDiWebSocketHandshakeResponseError, WebDriverBiDiWebSocketOpeningWriteError, }; +pub use webdriver_bidi_websocket_message::{ + MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, WebDriverBiDiWebSocketControlKind, + WebDriverBiDiWebSocketControlMessage, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageError, + WebDriverBiDiWebSocketTextMessage, +}; pub use webdriver_bidi_websocket_opening_recovery::WebDriverBiDiWebSocketOpeningWriteRecoveryDisposition; From 218d0bc6c0bd4d35e6880e04c89925a9c64e0be5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:17:14 -0700 Subject: [PATCH 05/57] fix(network): keep BiDi assembly warnings and state explicit --- .../src/webdriver_bidi_websocket_message.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 6d95dfb55..0eb92d358 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -1,4 +1,4 @@ -use std::{error::Error, fmt, str}; +use std::{error::Error, fmt}; use crate::{MAX_WEBSOCKET_FRAME_PAYLOAD_SIZE, WebDriverBiDiWebSocketFrame}; @@ -255,28 +255,22 @@ impl WebDriverBiDiWebSocketMessageAssembler { fin: bool, payload: &[u8], ) -> Result { - let Some(current_len) = self.fragmented_text.as_ref().map(Vec::len) else { + let Some(mut buffer) = self.fragmented_text.take() else { return self.reject(WebDriverBiDiWebSocketMessageError::UnexpectedContinuation); }; + let current_len = buffer.len(); if payload.len() > MAX_WEBDRIVER_BIDI_MESSAGE_SIZE - current_len { return self.reject(WebDriverBiDiWebSocketMessageError::MessageTooLarge { payload_bytes: current_len.saturating_add(payload.len()), maximum_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, }); } - let buffer = self - .fragmented_text - .as_mut() - .expect("fragmented text presence was established above"); buffer.extend_from_slice(payload); if !fin { + self.fragmented_text = Some(buffer); return Ok(WebDriverBiDiWebSocketMessageAssembly::Pending); } - let complete = self - .fragmented_text - .take() - .expect("fragmented text presence was established above"); - Self::complete_text(complete) + Self::complete_text(buffer) .map(WebDriverBiDiWebSocketMessageAssembly::Text) .map_err(|error| { self.poisoned = true; From af23abf80897c23b67d7cdf8f043211dc7f90b63 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:19:37 -0700 Subject: [PATCH 06/57] style(network): apply canonical BiDi message formatting --- .../src/webdriver_bidi_websocket_message.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 0eb92d358..56b78cc59 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -115,15 +115,13 @@ impl fmt::Display for WebDriverBiDiWebSocketMessageError { Self::AssemblerPoisoned => formatter.write_str( "WebDriver BiDi WebSocket message assembly is terminal after failure or Close", ), - Self::UnexpectedContinuation => formatter.write_str( - "WebDriver BiDi WebSocket continuation arrived without fragmented text", - ), + Self::UnexpectedContinuation => formatter + .write_str("WebDriver BiDi WebSocket continuation arrived without fragmented text"), Self::UnexpectedBinaryMessage => formatter.write_str( "WebDriver BiDi requires WebSocket text messages; binary message rejected", ), - Self::InterruptedFragmentedText => formatter.write_str( - "WebDriver BiDi fragmented text was interrupted by a new data message", - ), + Self::InterruptedFragmentedText => formatter + .write_str("WebDriver BiDi fragmented text was interrupted by a new data message"), Self::MessageTooLarge { payload_bytes, maximum_bytes, @@ -316,9 +314,7 @@ impl Default for WebDriverBiDiWebSocketMessageAssembler { mod tests { use super::*; - fn text( - assembly: WebDriverBiDiWebSocketMessageAssembly, - ) -> WebDriverBiDiWebSocketTextMessage { + fn text(assembly: WebDriverBiDiWebSocketMessageAssembly) -> WebDriverBiDiWebSocketTextMessage { match assembly { WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, other => panic!("expected text message, got {other:?}"), From b65252b767e64134e5ea5d23232951923e525cb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:23:42 -0700 Subject: [PATCH 07/57] fix(network): satisfy strict BiDi message contracts --- .../src/webdriver_bidi_websocket_message.rs | 112 +++++++----------- 1 file changed, 44 insertions(+), 68 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 56b78cc59..9d33ba46d 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -238,10 +238,9 @@ impl WebDriverBiDiWebSocketMessageAssembler { if fin { return Self::complete_text(payload.to_vec()) .map(WebDriverBiDiWebSocketMessageAssembly::Text) - .map_err(|error| { + .inspect_err(|_| { self.fragmented_text = None; self.poisoned = true; - error }); } self.fragmented_text = Some(payload.to_vec()); @@ -270,9 +269,8 @@ impl WebDriverBiDiWebSocketMessageAssembler { } Self::complete_text(buffer) .map(WebDriverBiDiWebSocketMessageAssembly::Text) - .map_err(|error| { + .inspect_err(|_| { self.poisoned = true; - error }) } @@ -312,32 +310,36 @@ impl Default for WebDriverBiDiWebSocketMessageAssembler { #[cfg(test)] mod tests { + use std::io; + use super::*; - fn text(assembly: WebDriverBiDiWebSocketMessageAssembly) -> WebDriverBiDiWebSocketTextMessage { + fn text( + assembly: WebDriverBiDiWebSocketMessageAssembly, + ) -> Result { match assembly { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, - other => panic!("expected text message, got {other:?}"), + WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), + other => Err(io::Error::other(format!( + "expected text message, got {other:?}" + ))), } } fn control( assembly: WebDriverBiDiWebSocketMessageAssembly, - ) -> WebDriverBiDiWebSocketControlMessage { + ) -> Result { match assembly { - WebDriverBiDiWebSocketMessageAssembly::Control(control) => control, - other => panic!("expected control message, got {other:?}"), + WebDriverBiDiWebSocketMessageAssembly::Control(control) => Ok(control), + other => Err(io::Error::other(format!( + "expected control message, got {other:?}" + ))), } } #[test] - fn complete_text_and_debug_are_payload_redacted() { + fn complete_text_and_debug_are_payload_redacted() -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::default(); - let message = text( - assembler - .push_parts(true, 0x1, b"secret-text") - .expect("complete text"), - ); + let message = text(assembler.push_parts(true, 0x1, b"secret-text")?)?; assert_eq!(message.as_str(), "secret-text"); let message_debug = format!("{message:?}"); assert!(message_debug.contains("payload_bytes: 11")); @@ -345,84 +347,60 @@ mod tests { let assembler_debug = format!("{assembler:?}"); assert!(assembler_debug.contains("fragmented_payload_bytes: 0")); assert!(assembler_debug.contains("terminal: false")); + Ok(()) } #[test] - fn fragments_reassemble_only_after_final_continuation() { + fn fragments_reassemble_only_after_final_continuation() -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( - assembler - .push_parts(false, 0x1, b"A\xe2") - .expect("text start"), + assembler.push_parts(false, 0x1, b"A\xe2")?, WebDriverBiDiWebSocketMessageAssembly::Pending ); assert_eq!( - assembler - .push_parts(false, 0x0, b"\x82") - .expect("middle continuation"), + assembler.push_parts(false, 0x0, b"\x82")?, WebDriverBiDiWebSocketMessageAssembly::Pending ); - let message = text( - assembler - .push_parts(true, 0x0, b"\xacB") - .expect("final continuation"), - ); + let message = text(assembler.push_parts(true, 0x0, b"\xacB")?)?; assert_eq!(message.as_str(), "A€B"); + Ok(()) } #[test] - fn ping_and_pong_preserve_fragmented_text_state() { + fn ping_and_pong_preserve_fragmented_text_state() -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - assembler - .push_parts(false, 0x1, b"left-") - .expect("fragment start"); - let ping = control( - assembler - .push_parts(true, 0x9, b"ping-data") - .expect("ping control"), - ); + assembler.push_parts(false, 0x1, b"left-")?; + let ping = control(assembler.push_parts(true, 0x9, b"ping-data")?)?; assert_eq!(ping.kind(), WebDriverBiDiWebSocketControlKind::Ping); assert_eq!(ping.payload(), b"ping-data"); let ping_debug = format!("{ping:?}"); assert!(ping_debug.contains("payload_bytes: 9")); assert!(!ping_debug.contains("ping-data")); - let pong = control( - assembler - .push_parts(true, 0xa, b"pong") - .expect("pong control"), - ); + let pong = control(assembler.push_parts(true, 0xa, b"pong")?)?; assert_eq!(pong.kind(), WebDriverBiDiWebSocketControlKind::Pong); assert_eq!(pong.payload(), b"pong"); - let message = text( - assembler - .push_parts(true, 0x0, b"right") - .expect("final continuation"), - ); + let message = text(assembler.push_parts(true, 0x0, b"right")?)?; assert_eq!(message.as_str(), "left-right"); + Ok(()) } #[test] - fn close_is_returned_once_and_makes_assembler_terminal() { + fn close_is_returned_once_and_makes_assembler_terminal() -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - assembler - .push_parts(false, 0x1, b"discarded") - .expect("fragment start"); - let close = control( - assembler - .push_parts(true, 0x8, b"bye") - .expect("close control"), - ); + assembler.push_parts(false, 0x1, b"discarded")?; + let close = control(assembler.push_parts(true, 0x8, b"bye")?)?; assert_eq!(close.kind(), WebDriverBiDiWebSocketControlKind::Close); assert_eq!(close.payload(), b"bye"); assert_eq!( assembler.push_parts(true, 0x1, b"later"), Err(WebDriverBiDiWebSocketMessageError::AssemblerPoisoned) ); + Ok(()) } #[test] - fn semantic_data_sequence_errors_fail_closed() { + fn semantic_data_sequence_errors_fail_closed() -> Result<(), Box> { let cases = [ ( 0x0, @@ -447,22 +425,19 @@ mod tests { } let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - assembler - .push_parts(false, 0x1, b"partial") - .expect("fragment start"); + assembler.push_parts(false, 0x1, b"partial")?; assert_eq!( assembler.push_parts(true, 0x1, b"new-message"), Err(WebDriverBiDiWebSocketMessageError::InterruptedFragmentedText) ); + Ok(()) } #[test] - fn aggregate_message_bound_rejects_fragmentation_bypass() { + fn aggregate_message_bound_rejects_fragmentation_bypass() -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); let maximum = vec![b'x'; MAX_WEBDRIVER_BIDI_MESSAGE_SIZE]; - assembler - .push_parts(false, 0x1, &maximum) - .expect("maximum-sized first fragment"); + assembler.push_parts(false, 0x1, &maximum)?; assert_eq!( assembler.push_parts(true, 0x0, b"y"), Err(WebDriverBiDiWebSocketMessageError::MessageTooLarge { @@ -480,10 +455,12 @@ mod tests { maximum_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, }) ); + Ok(()) } #[test] - fn utf8_validation_waits_for_complete_message_and_then_fails_closed() { + fn utf8_validation_waits_for_complete_message_and_then_fails_closed( + ) -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( assembler.push_parts(true, 0x1, b"\xff"), @@ -495,13 +472,12 @@ mod tests { ); let mut fragmented = WebDriverBiDiWebSocketMessageAssembler::new(); - fragmented - .push_parts(false, 0x1, b"\xe2") - .expect("partial UTF-8 is allowed until completion"); + fragmented.push_parts(false, 0x1, b"\xe2")?; assert_eq!( fragmented.push_parts(true, 0x0, b"x"), Err(WebDriverBiDiWebSocketMessageError::InvalidTextUtf8) ); + Ok(()) } #[test] From f571646cdce0912524e7bab82c2964a46dafcfa0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 10:25:29 -0700 Subject: [PATCH 08/57] style(network): apply canonical strict-contract formatting --- .../src/webdriver_bidi_websocket_message.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 9d33ba46d..3d59d0b09 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -459,8 +459,8 @@ mod tests { } #[test] - fn utf8_validation_waits_for_complete_message_and_then_fails_closed( - ) -> Result<(), Box> { + fn utf8_validation_waits_for_complete_message_and_then_fails_closed() + -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( assembler.push_parts(true, 0x1, b"\xff"), From c9a3f30b86babcc4e7f1c4b418ebee6c981ea795 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 11:05:22 -0700 Subject: [PATCH 09/57] test(network): exercise public BiDi message frame admission --- ..._bidi_websocket_message_public_contract.rs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs new file mode 100644 index 000000000..4282bf7e8 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs @@ -0,0 +1,77 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; + +use crate::{ + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +#[test] +fn public_message_admission_consumes_real_validated_frame() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x81, 0x02, b'o', b'k']) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint)?; + let correlated = admitted.correlate_session_id(SESSION_ID)?; + let target = correlated.into_explicit_connect_target()?; + let connection = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)? + .connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + assert_eq!(text.as_str(), "ok"); + + server + .join() + .map_err(|_| io::Error::other("validated-frame server panicked"))??; + Ok(()) +} From 175d4f1aee0e633b37a09452f406eeec02ea2b59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 11:05:38 -0700 Subject: [PATCH 10/57] test(network): wire public BiDi message admission regression --- crates/originweave-network/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index aa6867f72..0df142392 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -17,6 +17,8 @@ mod webdriver_bidi_connection; mod webdriver_bidi_websocket_frame; mod webdriver_bidi_websocket_handshake; mod webdriver_bidi_websocket_message; +#[cfg(test)] +mod webdriver_bidi_websocket_message_public_contract; mod webdriver_bidi_websocket_opening_recovery; pub use connection::{ From 11afdbe167761b4bda77ea5f79f0f23f0598d84e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 11:07:53 -0700 Subject: [PATCH 11/57] test(network): apply canonical rustfmt to BiDi admission regression --- .../src/webdriver_bidi_websocket_message_public_contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs index 4282bf7e8..1e4d50618 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs @@ -50,8 +50,8 @@ fn public_message_admission_consumes_real_validated_frame() -> Result<(), Box Date: Sat, 29 Aug 2026 12:11:37 -0700 Subject: [PATCH 12/57] test(network): cover BiDi assertion helper failures --- .../src/webdriver_bidi_websocket_message.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 3d59d0b09..6766d798b 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -336,6 +336,12 @@ mod tests { } } + #[test] + fn test_helpers_report_unexpected_assembly_variants() { + assert!(text(WebDriverBiDiWebSocketMessageAssembly::Pending).is_err()); + assert!(control(WebDriverBiDiWebSocketMessageAssembly::Pending).is_err()); + } + #[test] fn complete_text_and_debug_are_payload_redacted() -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::default(); From eab9c475f2ba3a82b91dde863695f35f875957b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 12:12:23 -0700 Subject: [PATCH 13/57] test(network): move BiDi public contract to integration target --- ..._bidi_websocket_message_public_contract.rs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_websocket_message_public_contract.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_message_public_contract.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_message_public_contract.rs new file mode 100644 index 000000000..78b8b671a --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_message_public_contract.rs @@ -0,0 +1,76 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +#[test] +fn public_message_admission_consumes_real_validated_frame() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x81, 0x02, b'o', b'k']) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint)?; + let correlated = admitted.correlate_session_id(SESSION_ID)?; + let target = correlated.into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + assert_eq!(text.as_str(), "ok"); + + server + .join() + .map_err(|_| io::Error::other("validated-frame server panicked"))??; + Ok(()) +} From f44aed77093822ce94145978d9afb8f1b60863cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 12:12:47 -0700 Subject: [PATCH 14/57] test(network): keep public BiDi contract outside library source --- crates/originweave-network/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 0df142392..aa6867f72 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -17,8 +17,6 @@ mod webdriver_bidi_connection; mod webdriver_bidi_websocket_frame; mod webdriver_bidi_websocket_handshake; mod webdriver_bidi_websocket_message; -#[cfg(test)] -mod webdriver_bidi_websocket_message_public_contract; mod webdriver_bidi_websocket_opening_recovery; pub use connection::{ From 3b23516ade7e4b0b1767818c10c438918f27bc21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 12:12:59 -0700 Subject: [PATCH 15/57] test(network): remove in-source BiDi public contract duplicate --- ..._bidi_websocket_message_public_contract.rs | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs deleted file mode 100644 index 1e4d50618..000000000 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message_public_contract.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::{ - error::Error, - io::{self, Read, Write}, - net::{TcpListener, TcpStream}, - thread, - time::Duration, -}; - -use originweave_core::WebDriverBiDiWebSocketEndpoint; - -use crate::{ - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, -}; - -const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; -const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; -const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; - -fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { - stream.set_read_timeout(Some(Duration::from_secs(2)))?; - let mut request = Vec::new(); - let mut buffer = [0_u8; 512]; - while !request.ends_with(b"\r\n\r\n") { - let count = stream.read(&mut buffer)?; - if count == 0 { - return Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "client opening request ended before the header terminator", - )); - } - request.extend_from_slice(&buffer[..count]); - } - Ok(()) -} - -#[test] -fn public_message_admission_consumes_real_validated_frame() -> Result<(), Box> { - let listener = TcpListener::bind(("127.0.0.1", 0))?; - let local_addr = listener.local_addr()?; - let server = thread::spawn(move || -> io::Result<()> { - let (mut stream, _) = listener.accept()?; - read_opening_request(&mut stream)?; - stream.write_all(OPENING_RESPONSE)?; - stream.write_all(&[0x81, 0x02, b'o', b'k']) - }); - - let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); - let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint)?; - let correlated = admitted.correlate_session_id(SESSION_ID)?; - let target = correlated.into_explicit_connect_target()?; - let connection = - WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; - let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; - let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? - .write_opening_request(Duration::from_millis(500))? - .read_opening_response(Duration::from_millis(500))?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, - other => { - return Err(io::Error::other(format!( - "validated text frame produced unexpected assembly state: {other:?}" - )) - .into()); - } - }; - assert_eq!(text.as_str(), "ok"); - - server - .join() - .map_err(|_| io::Error::other("validated-frame server panicked"))??; - Ok(()) -} From c74930d13467d1c35f8245f93a9e325be1e15205 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 12:21:32 -0700 Subject: [PATCH 16/57] test(network): remove impossible test error branches from coverage --- .../src/webdriver_bidi_websocket_message.rs | 108 +++++++++++------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 6766d798b..1ceeaaeda 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -310,42 +310,39 @@ impl Default for WebDriverBiDiWebSocketMessageAssembler { #[cfg(test)] mod tests { - use std::io; - use super::*; fn text( assembly: WebDriverBiDiWebSocketMessageAssembly, - ) -> Result { + ) -> Option { match assembly { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), - other => Err(io::Error::other(format!( - "expected text message, got {other:?}" - ))), + WebDriverBiDiWebSocketMessageAssembly::Text(text) => Some(text), + _ => None, } } fn control( assembly: WebDriverBiDiWebSocketMessageAssembly, - ) -> Result { + ) -> Option { match assembly { - WebDriverBiDiWebSocketMessageAssembly::Control(control) => Ok(control), - other => Err(io::Error::other(format!( - "expected control message, got {other:?}" - ))), + WebDriverBiDiWebSocketMessageAssembly::Control(control) => Some(control), + _ => None, } } #[test] fn test_helpers_report_unexpected_assembly_variants() { - assert!(text(WebDriverBiDiWebSocketMessageAssembly::Pending).is_err()); - assert!(control(WebDriverBiDiWebSocketMessageAssembly::Pending).is_err()); + assert!(text(WebDriverBiDiWebSocketMessageAssembly::Pending).is_none()); + assert!(control(WebDriverBiDiWebSocketMessageAssembly::Pending).is_none()); } #[test] - fn complete_text_and_debug_are_payload_redacted() -> Result<(), Box> { + fn complete_text_and_debug_are_payload_redacted() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::default(); - let message = text(assembler.push_parts(true, 0x1, b"secret-text")?)?; + let assembly = assembler + .push_parts(true, 0x1, b"secret-text") + .expect("complete text should be admitted"); + let message = text(assembly).expect("complete text should produce a text message"); assert_eq!(message.as_str(), "secret-text"); let message_debug = format!("{message:?}"); assert!(message_debug.contains("payload_bytes: 11")); @@ -353,60 +350,85 @@ mod tests { let assembler_debug = format!("{assembler:?}"); assert!(assembler_debug.contains("fragmented_payload_bytes: 0")); assert!(assembler_debug.contains("terminal: false")); - Ok(()) } #[test] - fn fragments_reassemble_only_after_final_continuation() -> Result<(), Box> { + fn fragments_reassemble_only_after_final_continuation() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( - assembler.push_parts(false, 0x1, b"A\xe2")?, + assembler + .push_parts(false, 0x1, b"A\xe2") + .expect("initial text fragment should be admitted"), WebDriverBiDiWebSocketMessageAssembly::Pending ); assert_eq!( - assembler.push_parts(false, 0x0, b"\x82")?, + assembler + .push_parts(false, 0x0, b"\x82") + .expect("middle continuation should be admitted"), WebDriverBiDiWebSocketMessageAssembly::Pending ); - let message = text(assembler.push_parts(true, 0x0, b"\xacB")?)?; + let assembly = assembler + .push_parts(true, 0x0, b"\xacB") + .expect("final continuation should be admitted"); + let message = text(assembly).expect("final continuation should produce text"); assert_eq!(message.as_str(), "A€B"); - Ok(()) } #[test] - fn ping_and_pong_preserve_fragmented_text_state() -> Result<(), Box> { + fn ping_and_pong_preserve_fragmented_text_state() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - assembler.push_parts(false, 0x1, b"left-")?; - let ping = control(assembler.push_parts(true, 0x9, b"ping-data")?)?; + assembler + .push_parts(false, 0x1, b"left-") + .expect("initial fragment should be admitted"); + let ping = control( + assembler + .push_parts(true, 0x9, b"ping-data") + .expect("ping should be admitted"), + ) + .expect("ping should produce a control message"); assert_eq!(ping.kind(), WebDriverBiDiWebSocketControlKind::Ping); assert_eq!(ping.payload(), b"ping-data"); let ping_debug = format!("{ping:?}"); assert!(ping_debug.contains("payload_bytes: 9")); assert!(!ping_debug.contains("ping-data")); - let pong = control(assembler.push_parts(true, 0xa, b"pong")?)?; + let pong = control( + assembler + .push_parts(true, 0xa, b"pong") + .expect("pong should be admitted"), + ) + .expect("pong should produce a control message"); assert_eq!(pong.kind(), WebDriverBiDiWebSocketControlKind::Pong); assert_eq!(pong.payload(), b"pong"); - let message = text(assembler.push_parts(true, 0x0, b"right")?)?; + let assembly = assembler + .push_parts(true, 0x0, b"right") + .expect("final continuation should be admitted"); + let message = text(assembly).expect("final continuation should produce text"); assert_eq!(message.as_str(), "left-right"); - Ok(()) } #[test] - fn close_is_returned_once_and_makes_assembler_terminal() -> Result<(), Box> { + fn close_is_returned_once_and_makes_assembler_terminal() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - assembler.push_parts(false, 0x1, b"discarded")?; - let close = control(assembler.push_parts(true, 0x8, b"bye")?)?; + assembler + .push_parts(false, 0x1, b"discarded") + .expect("initial fragment should be admitted"); + let close = control( + assembler + .push_parts(true, 0x8, b"bye") + .expect("close should be returned once"), + ) + .expect("close should produce a control message"); assert_eq!(close.kind(), WebDriverBiDiWebSocketControlKind::Close); assert_eq!(close.payload(), b"bye"); assert_eq!( assembler.push_parts(true, 0x1, b"later"), Err(WebDriverBiDiWebSocketMessageError::AssemblerPoisoned) ); - Ok(()) } #[test] - fn semantic_data_sequence_errors_fail_closed() -> Result<(), Box> { + fn semantic_data_sequence_errors_fail_closed() { let cases = [ ( 0x0, @@ -431,19 +453,22 @@ mod tests { } let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - assembler.push_parts(false, 0x1, b"partial")?; + assembler + .push_parts(false, 0x1, b"partial") + .expect("initial fragment should be admitted"); assert_eq!( assembler.push_parts(true, 0x1, b"new-message"), Err(WebDriverBiDiWebSocketMessageError::InterruptedFragmentedText) ); - Ok(()) } #[test] - fn aggregate_message_bound_rejects_fragmentation_bypass() -> Result<(), Box> { + fn aggregate_message_bound_rejects_fragmentation_bypass() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); let maximum = vec![b'x'; MAX_WEBDRIVER_BIDI_MESSAGE_SIZE]; - assembler.push_parts(false, 0x1, &maximum)?; + assembler + .push_parts(false, 0x1, &maximum) + .expect("maximum-size initial fragment should be admitted"); assert_eq!( assembler.push_parts(true, 0x0, b"y"), Err(WebDriverBiDiWebSocketMessageError::MessageTooLarge { @@ -461,12 +486,10 @@ mod tests { maximum_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, }) ); - Ok(()) } #[test] - fn utf8_validation_waits_for_complete_message_and_then_fails_closed() - -> Result<(), Box> { + fn utf8_validation_waits_for_complete_message_and_then_fails_closed() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( assembler.push_parts(true, 0x1, b"\xff"), @@ -478,12 +501,13 @@ mod tests { ); let mut fragmented = WebDriverBiDiWebSocketMessageAssembler::new(); - fragmented.push_parts(false, 0x1, b"\xe2")?; + fragmented + .push_parts(false, 0x1, b"\xe2") + .expect("initial UTF-8 fragment should be admitted"); assert_eq!( fragmented.push_parts(true, 0x0, b"x"), Err(WebDriverBiDiWebSocketMessageError::InvalidTextUtf8) ); - Ok(()) } #[test] From 9fedffbbde9e012c4804825b10a46b1177e3b02f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 12:31:24 -0700 Subject: [PATCH 17/57] test(network): satisfy strict WebSocket assembler contracts --- .../src/webdriver_bidi_websocket_message.rs | 126 +++++++++--------- 1 file changed, 60 insertions(+), 66 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 1ceeaaeda..fe5b7ed75 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -310,39 +310,39 @@ impl Default for WebDriverBiDiWebSocketMessageAssembler { #[cfg(test)] mod tests { + use std::io; + use super::*; fn text( assembly: WebDriverBiDiWebSocketMessageAssembly, - ) -> Option { + ) -> Result { match assembly { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Some(text), - _ => None, + WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), + _ => Err(io::Error::other("expected assembled WebSocket text message")), } } fn control( assembly: WebDriverBiDiWebSocketMessageAssembly, - ) -> Option { + ) -> Result { match assembly { - WebDriverBiDiWebSocketMessageAssembly::Control(control) => Some(control), - _ => None, + WebDriverBiDiWebSocketMessageAssembly::Control(control) => Ok(control), + _ => Err(io::Error::other("expected assembled WebSocket control message")), } } #[test] fn test_helpers_report_unexpected_assembly_variants() { - assert!(text(WebDriverBiDiWebSocketMessageAssembly::Pending).is_none()); - assert!(control(WebDriverBiDiWebSocketMessageAssembly::Pending).is_none()); + assert!(text(WebDriverBiDiWebSocketMessageAssembly::Pending).is_err()); + assert!(control(WebDriverBiDiWebSocketMessageAssembly::Pending).is_err()); } #[test] - fn complete_text_and_debug_are_payload_redacted() { + fn complete_text_and_debug_are_payload_redacted() -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::default(); - let assembly = assembler - .push_parts(true, 0x1, b"secret-text") - .expect("complete text should be admitted"); - let message = text(assembly).expect("complete text should produce a text message"); + let assembly = assembler.push_parts(true, 0x1, b"secret-text")?; + let message = text(assembly)?; assert_eq!(message.as_str(), "secret-text"); let message_debug = format!("{message:?}"); assert!(message_debug.contains("payload_bytes: 11")); @@ -350,85 +350,71 @@ mod tests { let assembler_debug = format!("{assembler:?}"); assert!(assembler_debug.contains("fragmented_payload_bytes: 0")); assert!(assembler_debug.contains("terminal: false")); + Ok(()) } #[test] - fn fragments_reassemble_only_after_final_continuation() { + fn fragments_reassemble_only_after_final_continuation( + ) -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( - assembler - .push_parts(false, 0x1, b"A\xe2") - .expect("initial text fragment should be admitted"), + assembler.push_parts(false, 0x1, b"A\xe2")?, WebDriverBiDiWebSocketMessageAssembly::Pending ); assert_eq!( - assembler - .push_parts(false, 0x0, b"\x82") - .expect("middle continuation should be admitted"), + assembler.push_parts(false, 0x0, b"\x82")?, WebDriverBiDiWebSocketMessageAssembly::Pending ); - let assembly = assembler - .push_parts(true, 0x0, b"\xacB") - .expect("final continuation should be admitted"); - let message = text(assembly).expect("final continuation should produce text"); + let assembly = assembler.push_parts(true, 0x0, b"\xacB")?; + let message = text(assembly)?; assert_eq!(message.as_str(), "A€B"); + Ok(()) } #[test] - fn ping_and_pong_preserve_fragmented_text_state() { + fn ping_and_pong_preserve_fragmented_text_state( + ) -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - assembler - .push_parts(false, 0x1, b"left-") - .expect("initial fragment should be admitted"); - let ping = control( - assembler - .push_parts(true, 0x9, b"ping-data") - .expect("ping should be admitted"), - ) - .expect("ping should produce a control message"); + assert_eq!( + assembler.push_parts(false, 0x1, b"left-")?, + WebDriverBiDiWebSocketMessageAssembly::Pending + ); + let ping = control(assembler.push_parts(true, 0x9, b"ping-data")?)?; assert_eq!(ping.kind(), WebDriverBiDiWebSocketControlKind::Ping); assert_eq!(ping.payload(), b"ping-data"); let ping_debug = format!("{ping:?}"); assert!(ping_debug.contains("payload_bytes: 9")); assert!(!ping_debug.contains("ping-data")); - let pong = control( - assembler - .push_parts(true, 0xa, b"pong") - .expect("pong should be admitted"), - ) - .expect("pong should produce a control message"); + let pong = control(assembler.push_parts(true, 0xa, b"pong")?)?; assert_eq!(pong.kind(), WebDriverBiDiWebSocketControlKind::Pong); assert_eq!(pong.payload(), b"pong"); - let assembly = assembler - .push_parts(true, 0x0, b"right") - .expect("final continuation should be admitted"); - let message = text(assembly).expect("final continuation should produce text"); + let assembly = assembler.push_parts(true, 0x0, b"right")?; + let message = text(assembly)?; assert_eq!(message.as_str(), "left-right"); + Ok(()) } #[test] - fn close_is_returned_once_and_makes_assembler_terminal() { + fn close_is_returned_once_and_makes_assembler_terminal( + ) -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - assembler - .push_parts(false, 0x1, b"discarded") - .expect("initial fragment should be admitted"); - let close = control( - assembler - .push_parts(true, 0x8, b"bye") - .expect("close should be returned once"), - ) - .expect("close should produce a control message"); + assert_eq!( + assembler.push_parts(false, 0x1, b"discarded")?, + WebDriverBiDiWebSocketMessageAssembly::Pending + ); + let close = control(assembler.push_parts(true, 0x8, b"bye")?)?; assert_eq!(close.kind(), WebDriverBiDiWebSocketControlKind::Close); assert_eq!(close.payload(), b"bye"); assert_eq!( assembler.push_parts(true, 0x1, b"later"), Err(WebDriverBiDiWebSocketMessageError::AssemblerPoisoned) ); + Ok(()) } #[test] - fn semantic_data_sequence_errors_fail_closed() { + fn semantic_data_sequence_errors_fail_closed() -> Result<(), Box> { let cases = [ ( 0x0, @@ -453,22 +439,26 @@ mod tests { } let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - assembler - .push_parts(false, 0x1, b"partial") - .expect("initial fragment should be admitted"); + assert_eq!( + assembler.push_parts(false, 0x1, b"partial")?, + WebDriverBiDiWebSocketMessageAssembly::Pending + ); assert_eq!( assembler.push_parts(true, 0x1, b"new-message"), Err(WebDriverBiDiWebSocketMessageError::InterruptedFragmentedText) ); + Ok(()) } #[test] - fn aggregate_message_bound_rejects_fragmentation_bypass() { + fn aggregate_message_bound_rejects_fragmentation_bypass( + ) -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); let maximum = vec![b'x'; MAX_WEBDRIVER_BIDI_MESSAGE_SIZE]; - assembler - .push_parts(false, 0x1, &maximum) - .expect("maximum-size initial fragment should be admitted"); + assert_eq!( + assembler.push_parts(false, 0x1, &maximum)?, + WebDriverBiDiWebSocketMessageAssembly::Pending + ); assert_eq!( assembler.push_parts(true, 0x0, b"y"), Err(WebDriverBiDiWebSocketMessageError::MessageTooLarge { @@ -486,10 +476,12 @@ mod tests { maximum_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, }) ); + Ok(()) } #[test] - fn utf8_validation_waits_for_complete_message_and_then_fails_closed() { + fn utf8_validation_waits_for_complete_message_and_then_fails_closed( + ) -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( assembler.push_parts(true, 0x1, b"\xff"), @@ -501,13 +493,15 @@ mod tests { ); let mut fragmented = WebDriverBiDiWebSocketMessageAssembler::new(); - fragmented - .push_parts(false, 0x1, b"\xe2") - .expect("initial UTF-8 fragment should be admitted"); + assert_eq!( + fragmented.push_parts(false, 0x1, b"\xe2")?, + WebDriverBiDiWebSocketMessageAssembly::Pending + ); assert_eq!( fragmented.push_parts(true, 0x0, b"x"), Err(WebDriverBiDiWebSocketMessageError::InvalidTextUtf8) ); + Ok(()) } #[test] From ed8a5e7fa841c24d7700bc35505261e92b38d1e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 12:34:06 -0700 Subject: [PATCH 18/57] style(network): apply canonical rustfmt output --- .../src/webdriver_bidi_websocket_message.rs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index fe5b7ed75..190f31b3e 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -319,7 +319,9 @@ mod tests { ) -> Result { match assembly { WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), - _ => Err(io::Error::other("expected assembled WebSocket text message")), + _ => Err(io::Error::other( + "expected assembled WebSocket text message", + )), } } @@ -328,7 +330,9 @@ mod tests { ) -> Result { match assembly { WebDriverBiDiWebSocketMessageAssembly::Control(control) => Ok(control), - _ => Err(io::Error::other("expected assembled WebSocket control message")), + _ => Err(io::Error::other( + "expected assembled WebSocket control message", + )), } } @@ -354,8 +358,8 @@ mod tests { } #[test] - fn fragments_reassemble_only_after_final_continuation( - ) -> Result<(), Box> { + fn fragments_reassemble_only_after_final_continuation() -> Result<(), Box> + { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( assembler.push_parts(false, 0x1, b"A\xe2")?, @@ -372,8 +376,7 @@ mod tests { } #[test] - fn ping_and_pong_preserve_fragmented_text_state( - ) -> Result<(), Box> { + fn ping_and_pong_preserve_fragmented_text_state() -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( assembler.push_parts(false, 0x1, b"left-")?, @@ -396,8 +399,8 @@ mod tests { } #[test] - fn close_is_returned_once_and_makes_assembler_terminal( - ) -> Result<(), Box> { + fn close_is_returned_once_and_makes_assembler_terminal() + -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( assembler.push_parts(false, 0x1, b"discarded")?, @@ -451,8 +454,8 @@ mod tests { } #[test] - fn aggregate_message_bound_rejects_fragmentation_bypass( - ) -> Result<(), Box> { + fn aggregate_message_bound_rejects_fragmentation_bypass() + -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); let maximum = vec![b'x'; MAX_WEBDRIVER_BIDI_MESSAGE_SIZE]; assert_eq!( @@ -480,8 +483,8 @@ mod tests { } #[test] - fn utf8_validation_waits_for_complete_message_and_then_fails_closed( - ) -> Result<(), Box> { + fn utf8_validation_waits_for_complete_message_and_then_fails_closed() + -> Result<(), Box> { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( assembler.push_parts(true, 0x1, b"\xff"), From c3165309bb94384636990c8d23a36261adb64bf3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 13:07:47 -0700 Subject: [PATCH 19/57] test(network): eliminate residual-only coverage regions --- .../src/webdriver_bidi_websocket_message.rs | 138 +++++++++--------- 1 file changed, 70 insertions(+), 68 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 190f31b3e..3ad3edc35 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -310,43 +310,32 @@ impl Default for WebDriverBiDiWebSocketMessageAssembler { #[cfg(test)] mod tests { - use std::io; - use super::*; - fn text( - assembly: WebDriverBiDiWebSocketMessageAssembly, - ) -> Result { - match assembly { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), - _ => Err(io::Error::other( - "expected assembled WebSocket text message", - )), - } + fn text_message(value: &str) -> WebDriverBiDiWebSocketTextMessage { + WebDriverBiDiWebSocketTextMessage(value.to_owned()) } - fn control( - assembly: WebDriverBiDiWebSocketMessageAssembly, - ) -> Result { - match assembly { - WebDriverBiDiWebSocketMessageAssembly::Control(control) => Ok(control), - _ => Err(io::Error::other( - "expected assembled WebSocket control message", - )), + fn control_message( + kind: WebDriverBiDiWebSocketControlKind, + payload: &[u8], + ) -> WebDriverBiDiWebSocketControlMessage { + WebDriverBiDiWebSocketControlMessage { + kind, + payload: payload.to_vec(), } } #[test] - fn test_helpers_report_unexpected_assembly_variants() { - assert!(text(WebDriverBiDiWebSocketMessageAssembly::Pending).is_err()); - assert!(control(WebDriverBiDiWebSocketMessageAssembly::Pending).is_err()); - } - - #[test] - fn complete_text_and_debug_are_payload_redacted() -> Result<(), Box> { + fn complete_text_and_debug_are_payload_redacted() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::default(); - let assembly = assembler.push_parts(true, 0x1, b"secret-text")?; - let message = text(assembly)?; + assert_eq!( + assembler.push_parts(true, 0x1, b"secret-text"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Text(text_message( + "secret-text" + ))) + ); + let message = text_message("secret-text"); assert_eq!(message.as_str(), "secret-text"); let message_debug = format!("{message:?}"); assert!(message_debug.contains("payload_bytes: 11")); @@ -354,70 +343,88 @@ mod tests { let assembler_debug = format!("{assembler:?}"); assert!(assembler_debug.contains("fragmented_payload_bytes: 0")); assert!(assembler_debug.contains("terminal: false")); - Ok(()) } #[test] - fn fragments_reassemble_only_after_final_continuation() -> Result<(), Box> - { + fn fragments_reassemble_only_after_final_continuation() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( - assembler.push_parts(false, 0x1, b"A\xe2")?, - WebDriverBiDiWebSocketMessageAssembly::Pending + assembler.push_parts(false, 0x1, b"A\xe2"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Pending) + ); + assert_eq!( + assembler.push_parts(false, 0x0, b"\x82"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Pending) ); assert_eq!( - assembler.push_parts(false, 0x0, b"\x82")?, - WebDriverBiDiWebSocketMessageAssembly::Pending + assembler.push_parts(true, 0x0, b"\xacB"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Text(text_message( + "A€B" + ))) ); - let assembly = assembler.push_parts(true, 0x0, b"\xacB")?; - let message = text(assembly)?; - assert_eq!(message.as_str(), "A€B"); - Ok(()) } #[test] - fn ping_and_pong_preserve_fragmented_text_state() -> Result<(), Box> { + fn ping_and_pong_preserve_fragmented_text_state() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( - assembler.push_parts(false, 0x1, b"left-")?, - WebDriverBiDiWebSocketMessageAssembly::Pending + assembler.push_parts(false, 0x1, b"left-"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Pending) + ); + assert_eq!( + assembler.push_parts(true, 0x9, b"ping-data"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Control( + control_message(WebDriverBiDiWebSocketControlKind::Ping, b"ping-data") + )) ); - let ping = control(assembler.push_parts(true, 0x9, b"ping-data")?)?; + let ping = control_message(WebDriverBiDiWebSocketControlKind::Ping, b"ping-data"); assert_eq!(ping.kind(), WebDriverBiDiWebSocketControlKind::Ping); assert_eq!(ping.payload(), b"ping-data"); let ping_debug = format!("{ping:?}"); assert!(ping_debug.contains("payload_bytes: 9")); assert!(!ping_debug.contains("ping-data")); - let pong = control(assembler.push_parts(true, 0xa, b"pong")?)?; + assert_eq!( + assembler.push_parts(true, 0xa, b"pong"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Control( + control_message(WebDriverBiDiWebSocketControlKind::Pong, b"pong") + )) + ); + let pong = control_message(WebDriverBiDiWebSocketControlKind::Pong, b"pong"); assert_eq!(pong.kind(), WebDriverBiDiWebSocketControlKind::Pong); assert_eq!(pong.payload(), b"pong"); - let assembly = assembler.push_parts(true, 0x0, b"right")?; - let message = text(assembly)?; - assert_eq!(message.as_str(), "left-right"); - Ok(()) + assert_eq!( + assembler.push_parts(true, 0x0, b"right"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Text(text_message( + "left-right" + ))) + ); } #[test] - fn close_is_returned_once_and_makes_assembler_terminal() - -> Result<(), Box> { + fn close_is_returned_once_and_makes_assembler_terminal() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( - assembler.push_parts(false, 0x1, b"discarded")?, - WebDriverBiDiWebSocketMessageAssembly::Pending + assembler.push_parts(false, 0x1, b"discarded"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Pending) + ); + assert_eq!( + assembler.push_parts(true, 0x8, b"bye"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Control( + control_message(WebDriverBiDiWebSocketControlKind::Close, b"bye") + )) ); - let close = control(assembler.push_parts(true, 0x8, b"bye")?)?; + let close = control_message(WebDriverBiDiWebSocketControlKind::Close, b"bye"); assert_eq!(close.kind(), WebDriverBiDiWebSocketControlKind::Close); assert_eq!(close.payload(), b"bye"); assert_eq!( assembler.push_parts(true, 0x1, b"later"), Err(WebDriverBiDiWebSocketMessageError::AssemblerPoisoned) ); - Ok(()) } #[test] - fn semantic_data_sequence_errors_fail_closed() -> Result<(), Box> { + fn semantic_data_sequence_errors_fail_closed() { let cases = [ ( 0x0, @@ -443,24 +450,22 @@ mod tests { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( - assembler.push_parts(false, 0x1, b"partial")?, - WebDriverBiDiWebSocketMessageAssembly::Pending + assembler.push_parts(false, 0x1, b"partial"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Pending) ); assert_eq!( assembler.push_parts(true, 0x1, b"new-message"), Err(WebDriverBiDiWebSocketMessageError::InterruptedFragmentedText) ); - Ok(()) } #[test] - fn aggregate_message_bound_rejects_fragmentation_bypass() - -> Result<(), Box> { + fn aggregate_message_bound_rejects_fragmentation_bypass() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); let maximum = vec![b'x'; MAX_WEBDRIVER_BIDI_MESSAGE_SIZE]; assert_eq!( - assembler.push_parts(false, 0x1, &maximum)?, - WebDriverBiDiWebSocketMessageAssembly::Pending + assembler.push_parts(false, 0x1, &maximum), + Ok(WebDriverBiDiWebSocketMessageAssembly::Pending) ); assert_eq!( assembler.push_parts(true, 0x0, b"y"), @@ -479,12 +484,10 @@ mod tests { maximum_bytes: MAX_WEBDRIVER_BIDI_MESSAGE_SIZE, }) ); - Ok(()) } #[test] - fn utf8_validation_waits_for_complete_message_and_then_fails_closed() - -> Result<(), Box> { + fn utf8_validation_waits_for_complete_message_and_then_fails_closed() { let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( assembler.push_parts(true, 0x1, b"\xff"), @@ -497,14 +500,13 @@ mod tests { let mut fragmented = WebDriverBiDiWebSocketMessageAssembler::new(); assert_eq!( - fragmented.push_parts(false, 0x1, b"\xe2")?, - WebDriverBiDiWebSocketMessageAssembly::Pending + fragmented.push_parts(false, 0x1, b"\xe2"), + Ok(WebDriverBiDiWebSocketMessageAssembly::Pending) ); assert_eq!( fragmented.push_parts(true, 0x0, b"x"), Err(WebDriverBiDiWebSocketMessageError::InvalidTextUtf8) ); - Ok(()) } #[test] From c1884ba87cec26b48089c327620d43039625c42d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 15:24:05 -0700 Subject: [PATCH 20/57] test(network): require bounded BiDi JSON envelope parsing --- .../tests/webdriver_bidi_json_envelope.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_json_envelope.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs new file mode 100644 index 000000000..4dad5a90c --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -0,0 +1,84 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const SUCCESS_MESSAGE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true}}"#; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +#[test] +fn real_transport_text_is_classified_as_bidi_success_envelope() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let payload_len = u8::try_from(SUCCESS_MESSAGE.len()) + .map_err(|_| io::Error::other("test payload exceeded one-byte frame length"))?; + stream.write_all(&[0x81, payload_len])?; + stream.write_all(SUCCESS_MESSAGE) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint)?; + let correlated = admitted.correlate_session_id(SESSION_ID)?; + let target = correlated.into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let envelope = WebDriverBiDiJsonEnvelope::parse(&text)?; + assert_eq!(envelope.kind(), WebDriverBiDiJsonEnvelopeKind::Success); + assert_eq!(envelope.command_id(), Some(7)); + assert_eq!(envelope.method(), None); + assert_eq!(envelope.error_code(), None); + + server + .join() + .map_err(|_| io::Error::other("JSON-envelope server panicked"))??; + Ok(()) +} From 052e5315f8d71f27811f558f037f567ce979513a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:06:18 -0700 Subject: [PATCH 21/57] feat(network): classify bounded BiDi JSON envelopes --- crates/originweave-network/src/lib.rs | 10 +- .../src/webdriver_bidi_json_envelope.rs | 856 ++++++++++++++++++ 2 files changed, 864 insertions(+), 2 deletions(-) create mode 100644 crates/originweave-network/src/webdriver_bidi_json_envelope.rs diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index aa6867f72..63e6f193d 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -6,14 +6,16 @@ //! It also bridges a session-correlated WebDriver BiDi loopback target from //! `originweave-core` into one bounded exact TCP connection, binds and validates //! the RFC 6455 opening exchange, provides bounded masked client writes and -//! unmasked server-frame reads, and assembles bounded WebDriver BiDi text messages -//! without granting browser, TLS, policy, or Agent authority. +//! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, and +//! classifies complete local-end JSON envelopes without exposing generic JSON bodies +//! or granting browser, TLS, policy, secret, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] mod connection; mod webdriver_bidi_connection; +mod webdriver_bidi_json_envelope; mod webdriver_bidi_websocket_frame; mod webdriver_bidi_websocket_handshake; mod webdriver_bidi_websocket_message; @@ -27,6 +29,10 @@ pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan, }; +pub use webdriver_bidi_json_envelope::{ + MAX_WEBDRIVER_BIDI_JSON_DEPTH, MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, +}; pub use webdriver_bidi_websocket_frame::{ MAX_WEBSOCKET_FRAME_PAYLOAD_SIZE, MAX_WEBSOCKET_FRAME_TIMEOUT, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrame, diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs new file mode 100644 index 000000000..efbe0bde3 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -0,0 +1,856 @@ +use std::{collections::HashSet, error::Error, fmt}; + +use crate::WebDriverBiDiWebSocketTextMessage; + +/// Largest nesting depth accepted while validating one BiDi JSON envelope. +/// +/// The WebSocket text-message boundary already caps the aggregate document at 1 MiB. This +/// independent depth budget prevents a syntactically valid but pathologically nested document +/// from exhausting the Rust call stack while no browser or Agent authority has been granted. +pub const MAX_WEBDRIVER_BIDI_JSON_DEPTH: usize = 64; + +/// Largest integer admitted by WebDriver BiDi's `js-uint` production. +pub const MAX_WEBDRIVER_BIDI_JS_UINT: u64 = 9_007_199_254_740_991; + +/// Local-end WebDriver BiDi envelope kind after complete JSON syntax validation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiJsonEnvelopeKind { + /// A command completed successfully. + Success, + /// A command completed with a protocol error. + Error, + /// The remote end emitted an event. + Event, +} + +/// Credential-minimal classification of one complete WebDriver BiDi local-end JSON envelope. +/// +/// Result and parameter bodies are deliberately validated and discarded at this boundary. They +/// remain untrusted protocol data for later command- or event-specific parsers and are not exposed +/// as generic JSON values that could become ambient browser or Agent authority. +#[derive(Eq, PartialEq)] +pub struct WebDriverBiDiJsonEnvelope { + kind: WebDriverBiDiJsonEnvelopeKind, + command_id: Option, + method: Option, + error_code: Option, +} + +impl fmt::Debug for WebDriverBiDiJsonEnvelope { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiJsonEnvelope") + .field("kind", &self.kind) + .field("command_id", &self.command_id) + .field("has_method", &self.method.is_some()) + .field("has_error_code", &self.error_code.is_some()) + .finish() + } +} + +impl WebDriverBiDiJsonEnvelope { + /// Parse and classify one already bounded, validated UTF-8 WebSocket text message. + /// + /// This validates the complete RFC 8259 JSON grammar, rejects duplicate top-level member + /// names and excessive nesting, then enforces only the common local-end envelope shape from + /// WebDriver BiDi. Extensible result/parameter bodies remain opaque and are discarded. + pub fn parse( + message: &WebDriverBiDiWebSocketTextMessage, + ) -> Result { + Self::parse_str(message.as_str()) + } + + fn parse_str(text: &str) -> Result { + let mut cursor = JsonCursor::new(text); + let fields = cursor.parse_top_level_object()?; + cursor.skip_whitespace(); + if !cursor.is_finished() { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + } + fields.into_envelope() + } + + /// Return the classified local-end envelope kind. + #[must_use] + pub const fn kind(&self) -> WebDriverBiDiJsonEnvelopeKind { + self.kind + } + + /// Return the command identifier for success and correlatable error responses. + /// + /// Events and error responses whose protocol `id` is `null` return `None`. + #[must_use] + pub const fn command_id(&self) -> Option { + self.command_id + } + + /// Borrow the event method when this is an event envelope. + #[must_use] + pub fn method(&self) -> Option<&str> { + self.method.as_deref() + } + + /// Borrow the protocol error code when this is an error envelope. + #[must_use] + pub fn error_code(&self) -> Option<&str> { + self.error_code.as_deref() + } +} + +/// Fail-closed JSON syntax and common-envelope failures for local-end WebDriver BiDi messages. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiJsonEnvelopeError { + /// The document violates RFC 8259 JSON syntax or contains trailing non-whitespace bytes. + InvalidJson, + /// The top-level JSON value is not an object. + RootMustBeObject, + /// A top-level object member name appears more than once. + DuplicateTopLevelMember, + /// JSON nesting exceeded the reviewed parser safety budget. + NestingTooDeep { + /// Maximum nesting depth admitted by this parser. + maximum_depth: usize, + }, + /// A required common-envelope member is absent. + MissingRequiredMember { + /// Stable non-sensitive member name. + member: &'static str, + }, + /// A common-envelope member has the wrong JSON type or value range. + InvalidMember { + /// Stable non-sensitive member name. + member: &'static str, + }, + /// The `type` discriminator is not one of the three local-end envelope kinds. + UnsupportedEnvelopeType, +} + +impl fmt::Display for WebDriverBiDiJsonEnvelopeError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidJson => formatter.write_str("invalid WebDriver BiDi JSON document"), + Self::RootMustBeObject => { + formatter.write_str("WebDriver BiDi local-end message must be a JSON object") + } + Self::DuplicateTopLevelMember => { + formatter.write_str("WebDriver BiDi JSON object contains a duplicate top-level member") + } + Self::NestingTooDeep { maximum_depth } => write!( + formatter, + "WebDriver BiDi JSON nesting exceeds maximum depth {maximum_depth}" + ), + Self::MissingRequiredMember { member } => { + write!(formatter, "WebDriver BiDi envelope is missing required member {member}") + } + Self::InvalidMember { member } => { + write!(formatter, "WebDriver BiDi envelope member {member} is invalid") + } + Self::UnsupportedEnvelopeType => { + formatter.write_str("unsupported WebDriver BiDi local-end envelope type") + } + } + } +} + +impl Error for WebDriverBiDiJsonEnvelopeError {} + +#[derive(Default)] +struct TopLevelFields { + envelope_type: Option, + id: Option, + result: Option, + method: Option, + params: Option, + error: Option, + message: Option, + stacktrace: Option, +} + +impl TopLevelFields { + fn record(&mut self, key: &str, value: JsonValue) { + match key { + "type" => self.envelope_type = Some(value), + "id" => self.id = Some(value), + "result" => self.result = Some(value), + "method" => self.method = Some(value), + "params" => self.params = Some(value), + "error" => self.error = Some(value), + "message" => self.message = Some(value), + "stacktrace" => self.stacktrace = Some(value), + _ => {} + } + } + + fn into_envelope(self) -> Result { + let kind = match self.envelope_type.as_ref() { + None => return Err(missing("type")), + Some(JsonValue::Text(envelope_type)) => match envelope_type.as_str() { + "success" => WebDriverBiDiJsonEnvelopeKind::Success, + "error" => WebDriverBiDiJsonEnvelopeKind::Error, + "event" => WebDriverBiDiJsonEnvelopeKind::Event, + _ => return Err(WebDriverBiDiJsonEnvelopeError::UnsupportedEnvelopeType), + }, + Some(_) => return Err(invalid("type")), + }; + match kind { + WebDriverBiDiJsonEnvelopeKind::Success => self.into_success(), + WebDriverBiDiJsonEnvelopeKind::Error => self.into_error(), + WebDriverBiDiJsonEnvelopeKind::Event => self.into_event(), + } + } + + fn into_success( + self, + ) -> Result { + let command_id = required_js_uint(self.id, "id")?; + require_object(self.result, "result")?; + Ok(WebDriverBiDiJsonEnvelope { + kind: WebDriverBiDiJsonEnvelopeKind::Success, + command_id: Some(command_id), + method: None, + error_code: None, + }) + } + + fn into_error(self) -> Result { + let command_id = nullable_js_uint(self.id, "id")?; + let error_code = required_text(self.error, "error")?; + let _message = required_text(self.message, "message")?; + if let Some(stacktrace) = self.stacktrace { + require_text_value(stacktrace, "stacktrace")?; + } + Ok(WebDriverBiDiJsonEnvelope { + kind: WebDriverBiDiJsonEnvelopeKind::Error, + command_id, + method: None, + error_code: Some(error_code), + }) + } + + fn into_event(self) -> Result { + let method = required_text(self.method, "method")?; + require_object(self.params, "params")?; + Ok(WebDriverBiDiJsonEnvelope { + kind: WebDriverBiDiJsonEnvelopeKind::Event, + command_id: None, + method: Some(method), + error_code: None, + }) + } +} + +fn missing(member: &'static str) -> WebDriverBiDiJsonEnvelopeError { + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member } +} + +fn invalid(member: &'static str) -> WebDriverBiDiJsonEnvelopeError { + WebDriverBiDiJsonEnvelopeError::InvalidMember { member } +} + +fn required_text( + value: Option, + member: &'static str, +) -> Result { + let value = value.ok_or_else(|| missing(member))?; + match value { + JsonValue::Text(text) => Ok(text), + _ => Err(invalid(member)), + } +} + +fn require_text_value( + value: JsonValue, + member: &'static str, +) -> Result<(), WebDriverBiDiJsonEnvelopeError> { + if matches!(value, JsonValue::Text(_)) { + Ok(()) + } else { + Err(invalid(member)) + } +} + +fn required_js_uint( + value: Option, + member: &'static str, +) -> Result { + let value = value.ok_or_else(|| missing(member))?; + match value { + JsonValue::Number(Some(number)) => Ok(number), + _ => Err(invalid(member)), + } +} + +fn nullable_js_uint( + value: Option, + member: &'static str, +) -> Result, WebDriverBiDiJsonEnvelopeError> { + let value = value.ok_or_else(|| missing(member))?; + match value { + JsonValue::Null => Ok(None), + JsonValue::Number(Some(number)) => Ok(Some(number)), + _ => Err(invalid(member)), + } +} + +fn require_object( + value: Option, + member: &'static str, +) -> Result<(), WebDriverBiDiJsonEnvelopeError> { + let value = value.ok_or_else(|| missing(member))?; + if matches!(value, JsonValue::Object) { + Ok(()) + } else { + Err(invalid(member)) + } +} + +enum JsonValue { + Null, + Text(String), + Number(Option), + Object, + Other, +} + +struct JsonCursor<'a> { + input: &'a str, + index: usize, +} + +impl<'a> JsonCursor<'a> { + const fn new(input: &'a str) -> Self { + Self { input, index: 0 } + } + + fn is_finished(&self) -> bool { + self.index == self.input.len() + } + + fn current_byte(&self) -> Option { + self.input.as_bytes().get(self.index).copied() + } + + fn consume_byte(&mut self, expected: u8) -> bool { + if self.current_byte() == Some(expected) { + self.index += 1; + true + } else { + false + } + } + + fn expect_byte(&mut self, expected: u8) -> Result<(), WebDriverBiDiJsonEnvelopeError> { + if self.consume_byte(expected) { + Ok(()) + } else { + Err(WebDriverBiDiJsonEnvelopeError::InvalidJson) + } + } + + fn skip_whitespace(&mut self) { + while matches!( + self.current_byte(), + Some(b' ' | b'\t' | b'\n' | b'\r') + ) { + self.index += 1; + } + } + + fn parse_top_level_object( + &mut self, + ) -> Result { + self.skip_whitespace(); + if !self.consume_byte(b'{') { + return Err(WebDriverBiDiJsonEnvelopeError::RootMustBeObject); + } + self.skip_whitespace(); + let mut fields = TopLevelFields::default(); + let mut seen = HashSet::new(); + if self.consume_byte(b'}') { + return Ok(fields); + } + + loop { + self.skip_whitespace(); + let key = self.parse_string()?; + if !seen.insert(key.clone()) { + return Err(WebDriverBiDiJsonEnvelopeError::DuplicateTopLevelMember); + } + self.skip_whitespace(); + self.expect_byte(b':')?; + self.skip_whitespace(); + let value = self.parse_value(1)?; + fields.record(&key, value); + self.skip_whitespace(); + if self.consume_byte(b'}') { + return Ok(fields); + } + self.expect_byte(b',')?; + } + } + + fn parse_value( + &mut self, + depth: usize, + ) -> Result { + if depth > MAX_WEBDRIVER_BIDI_JSON_DEPTH { + return Err(WebDriverBiDiJsonEnvelopeError::NestingTooDeep { + maximum_depth: MAX_WEBDRIVER_BIDI_JSON_DEPTH, + }); + } + self.skip_whitespace(); + match self.current_byte() { + Some(b'"') => self.parse_string().map(JsonValue::Text), + Some(b'{') => { + self.parse_object(depth)?; + Ok(JsonValue::Object) + } + Some(b'[') => { + self.parse_array(depth)?; + Ok(JsonValue::Other) + } + Some(b'n') => { + self.parse_literal(b"null")?; + Ok(JsonValue::Null) + } + Some(b't') => { + self.parse_literal(b"true")?; + Ok(JsonValue::Other) + } + Some(b'f') => { + self.parse_literal(b"false")?; + Ok(JsonValue::Other) + } + Some(b'-' | b'0'..=b'9') => self.parse_number(), + _ => Err(WebDriverBiDiJsonEnvelopeError::InvalidJson), + } + } + + fn parse_object(&mut self, depth: usize) -> Result<(), WebDriverBiDiJsonEnvelopeError> { + self.expect_byte(b'{')?; + self.skip_whitespace(); + if self.consume_byte(b'}') { + return Ok(()); + } + loop { + self.skip_whitespace(); + let _key = self.parse_string()?; + self.skip_whitespace(); + self.expect_byte(b':')?; + self.skip_whitespace(); + let _value = self.parse_value(depth + 1)?; + self.skip_whitespace(); + if self.consume_byte(b'}') { + return Ok(()); + } + self.expect_byte(b',')?; + } + } + + fn parse_array(&mut self, depth: usize) -> Result<(), WebDriverBiDiJsonEnvelopeError> { + self.expect_byte(b'[')?; + self.skip_whitespace(); + if self.consume_byte(b']') { + return Ok(()); + } + loop { + let _value = self.parse_value(depth + 1)?; + self.skip_whitespace(); + if self.consume_byte(b']') { + return Ok(()); + } + self.expect_byte(b',')?; + self.skip_whitespace(); + } + } + + fn parse_literal(&mut self, literal: &[u8]) -> Result<(), WebDriverBiDiJsonEnvelopeError> { + let end = self.index.saturating_add(literal.len()); + if self.input.as_bytes().get(self.index..end) == Some(literal) { + self.index = end; + Ok(()) + } else { + Err(WebDriverBiDiJsonEnvelopeError::InvalidJson) + } + } + + fn parse_number(&mut self) -> Result { + let start = self.index; + let negative = self.consume_byte(b'-'); + if self.consume_byte(b'0') { + if matches!(self.current_byte(), Some(b'0'..=b'9')) { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + } + } else { + match self.current_byte() { + Some(b'1'..=b'9') => { + self.index += 1; + self.consume_digits(); + } + _ => return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson), + } + } + + let mut is_integer = true; + if self.consume_byte(b'.') { + is_integer = false; + if !matches!(self.current_byte(), Some(b'0'..=b'9')) { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + } + self.consume_digits(); + } + if matches!(self.current_byte(), Some(b'e' | b'E')) { + is_integer = false; + self.index += 1; + if matches!(self.current_byte(), Some(b'+' | b'-')) { + self.index += 1; + } + if !matches!(self.current_byte(), Some(b'0'..=b'9')) { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + } + self.consume_digits(); + } + + let js_uint = if !negative && is_integer { + self.input[start..self.index] + .parse::() + .ok() + .filter(|number| *number <= MAX_WEBDRIVER_BIDI_JS_UINT) + } else { + None + }; + Ok(JsonValue::Number(js_uint)) + } + + fn consume_digits(&mut self) { + while matches!(self.current_byte(), Some(b'0'..=b'9')) { + self.index += 1; + } + } + + fn parse_string(&mut self) -> Result { + self.expect_byte(b'"')?; + let mut output = String::new(); + loop { + let Some(byte) = self.current_byte() else { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + }; + match byte { + b'"' => { + self.index += 1; + return Ok(output); + } + b'\\' => { + self.index += 1; + self.parse_escape(&mut output)?; + } + 0x00..=0x1f => return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson), + _ if byte.is_ascii() => { + output.push(char::from(byte)); + self.index += 1; + } + _ => { + let remaining = &self.input[self.index..]; + let Some(character) = remaining.chars().next() else { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + }; + output.push(character); + self.index += character.len_utf8(); + } + } + } + } + + fn parse_escape(&mut self, output: &mut String) -> Result<(), WebDriverBiDiJsonEnvelopeError> { + let Some(escape) = self.current_byte() else { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + }; + self.index += 1; + match escape { + b'"' => output.push('"'), + b'\\' => output.push('\\'), + b'/' => output.push('/'), + b'b' => output.push('\u{0008}'), + b'f' => output.push('\u{000c}'), + b'n' => output.push('\n'), + b'r' => output.push('\r'), + b't' => output.push('\t'), + b'u' => self.parse_unicode_escape(output)?, + _ => return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson), + } + Ok(()) + } + + fn parse_unicode_escape( + &mut self, + output: &mut String, + ) -> Result<(), WebDriverBiDiJsonEnvelopeError> { + let first = self.parse_hex_u16()?; + let scalar = if (0xd800..=0xdbff).contains(&first) { + self.expect_byte(b'\\')?; + self.expect_byte(b'u')?; + let second = self.parse_hex_u16()?; + if !(0xdc00..=0xdfff).contains(&second) { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + } + 0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00) + } else if (0xdc00..=0xdfff).contains(&first) { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + } else { + u32::from(first) + }; + let Some(character) = char::from_u32(scalar) else { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + }; + output.push(character); + Ok(()) + } + + fn parse_hex_u16(&mut self) -> Result { + let mut value = 0_u16; + for _ in 0..4 { + let Some(byte) = self.current_byte() else { + return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); + }; + let digit = match byte { + b'0'..=b'9' => u16::from(byte - b'0'), + b'a'..=b'f' => u16::from(byte - b'a' + 10), + b'A'..=b'F' => u16::from(byte - b'A' + 10), + _ => return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson), + }; + value = (value << 4) | digit; + self.index += 1; + } + Ok(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(value: &str) -> Result { + WebDriverBiDiJsonEnvelope::parse_str(value) + } + + #[test] + fn classifies_all_local_end_envelope_kinds_and_redacts_debug() { + let success = parse( + r#"{"type":"success","id":9007199254740991,"result":{"ready":true},"ext":[null,false,1.5,-2e3,"\u20ac","\ud83d\ude00"]}"#, + ) + .ok(); + assert!(success.is_some()); + let Some(success) = success else { + return; + }; + assert_eq!(success.kind(), WebDriverBiDiJsonEnvelopeKind::Success); + assert_eq!(success.command_id(), Some(MAX_WEBDRIVER_BIDI_JS_UINT)); + assert_eq!(success.method(), None); + assert_eq!(success.error_code(), None); + let debug = format!("{success:?}"); + assert!(debug.contains("Success")); + assert!(!debug.contains("ready")); + + let error = parse( + r#"{"type":"error","id":null,"error":"invalid argument","message":"secret detail","stacktrace":"hidden","vendor":{"x":[]}}"#, + ) + .ok(); + assert!(error.is_some()); + let Some(error) = error else { + return; + }; + assert_eq!(error.kind(), WebDriverBiDiJsonEnvelopeKind::Error); + assert_eq!(error.command_id(), None); + assert_eq!(error.error_code(), Some("invalid argument")); + assert_eq!(error.method(), None); + let debug = format!("{error:?}"); + assert!(!debug.contains("invalid argument")); + assert!(!debug.contains("secret detail")); + + let event = parse( + r#"{"type":"event","method":"browsingContext.load","params":{},"vendor":true}"#, + ) + .ok(); + assert!(event.is_some()); + let Some(event) = event else { + return; + }; + assert_eq!(event.kind(), WebDriverBiDiJsonEnvelopeKind::Event); + assert_eq!(event.command_id(), None); + assert_eq!(event.method(), Some("browsingContext.load")); + assert_eq!(event.error_code(), None); + } + + #[test] + fn accepts_correlatable_error_and_extensible_success_members() { + let error = parse(r#"{"type":"error","id":7,"error":"unknown error","message":"","x":0}"#); + assert_eq!(error.as_ref().ok().and_then(WebDriverBiDiJsonEnvelope::command_id), Some(7)); + + let success = parse( + "{\n \"\\u0074ype\":\"success\", \"id\":0, \"result\":{\"escaped\":\"\\\\/\\b\\f\\n\\r\\t\\\"\",\"unicode\":\"é\"}, \"method\":123 } \r\n", + ); + assert!(success.is_ok()); + } + + #[test] + fn envelope_shape_failures_are_typed() { + let cases = [ + ( + "{}", + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "type" }, + ), + ( + r#"{"type":1}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "type" }, + ), + ( + r#"{"type":"other"}"#, + WebDriverBiDiJsonEnvelopeError::UnsupportedEnvelopeType, + ), + ( + r#"{"type":"success","result":{}}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "id" }, + ), + ( + r#"{"type":"success","id":null,"result":{}}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, + ), + ( + r#"{"type":"success","id":9007199254740992,"result":{}}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, + ), + ( + r#"{"type":"success","id":1}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "result" }, + ), + ( + r#"{"type":"success","id":1,"result":[]}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "result" }, + ), + ( + r#"{"type":"error","error":"x","message":"m"}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "id" }, + ), + ( + r#"{"type":"error","id":-1,"error":"x","message":"m"}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, + ), + ( + r#"{"type":"error","id":null,"message":"m"}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, + ), + ( + r#"{"type":"error","id":null,"error":false,"message":"m"}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "error" }, + ), + ( + r#"{"type":"error","id":null,"error":"x"}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "message" }, + ), + ( + r#"{"type":"error","id":null,"error":"x","message":{}}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "message" }, + ), + ( + r#"{"type":"error","id":null,"error":"x","message":"m","stacktrace":0}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "stacktrace" }, + ), + ( + r#"{"type":"event","params":{}}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "method" }, + ), + ( + r#"{"type":"event","method":false,"params":{}}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "method" }, + ), + ( + r#"{"type":"event","method":"x"}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "params" }, + ), + ( + r#"{"type":"event","method":"x","params":null}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "params" }, + ), + ]; + for (document, expected) in cases { + assert_eq!(parse(document), Err(expected)); + } + } + + #[test] + fn rejects_ambiguous_or_malformed_json() { + let cases = [ + "[]", + "null", + r#"{"type":"success","type":"event","id":1,"result":{}}"#, + r#"{"type":"success","id":1,"result":{}} trailing"#, + r#"{"type":"success","id":01,"result":{}}"#, + r#"{"type":"success","id":1.,"result":{}}"#, + r#"{"type":"success","id":1e,"result":{}}"#, + r#"{"type":"success","id":1e+,"result":{}}"#, + r#"{"type":"success","id":-,"result":{}}"#, + r#"{"type":"success","id":18446744073709551616,"result":{}}"#, + "{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"line\nbreak\"}}", + r#"{"type":"success","id":1,"result":{"bad":"\x"}}"#, + r#"{"type":"success","id":1,"result":{"bad":"\u12xz"}}"#, + r#"{"type":"success","id":1,"result":{"bad":"\ud800x"}}"#, + r#"{"type":"success","id":1,"result":{"bad":"\ud800\u0041"}}"#, + r#"{"type":"success","id":1,"result":{"bad":"\udc00"}}"#, + r#"{"type":"success","id":1,"result":{"a":true "b":false}}"#, + r#"{"type":"success","id":1,"result":[1,]}"#, + r#"{"type":"success","id":1,"result":{"a":tru}}"#, + r#"{"type":"success","id":1,"result":{"a":fal}}"#, + r#"{"type":"success","id":1,"result":{"a":nul}}"#, + r#"{"type":"success","id":1,"result":{"a":}}"#, + r#"{"type":"success","id":1,"result":{"#, + ]; + for document in cases { + assert!(parse(document).is_err(), "unexpectedly admitted {document}"); + } + assert_eq!( + parse("[]"), + Err(WebDriverBiDiJsonEnvelopeError::RootMustBeObject) + ); + assert_eq!( + parse(r#"{"type":"success","type":"event","id":1,"result":{}}"#), + Err(WebDriverBiDiJsonEnvelopeError::DuplicateTopLevelMember) + ); + } + + #[test] + fn rejects_excessive_json_nesting_and_formats_errors_without_payloads() { + let mut document = String::from(r#"{"type":"success","id":1,"result":"#); + for _ in 0..=MAX_WEBDRIVER_BIDI_JSON_DEPTH { + document.push('['); + } + document.push_str("null"); + for _ in 0..=MAX_WEBDRIVER_BIDI_JSON_DEPTH { + document.push(']'); + } + document.push('}'); + assert_eq!( + parse(&document), + Err(WebDriverBiDiJsonEnvelopeError::NestingTooDeep { + maximum_depth: MAX_WEBDRIVER_BIDI_JSON_DEPTH, + }) + ); + + let errors = [ + WebDriverBiDiJsonEnvelopeError::InvalidJson, + WebDriverBiDiJsonEnvelopeError::RootMustBeObject, + WebDriverBiDiJsonEnvelopeError::DuplicateTopLevelMember, + WebDriverBiDiJsonEnvelopeError::NestingTooDeep { + maximum_depth: MAX_WEBDRIVER_BIDI_JSON_DEPTH, + }, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "id" }, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "result" }, + WebDriverBiDiJsonEnvelopeError::UnsupportedEnvelopeType, + ]; + for error in errors { + let display = error.to_string(); + assert!(!display.is_empty()); + let source: &dyn Error = &error; + assert!(source.source().is_none()); + } + } +} From fc346e1123d7215962428417382d2ea58052e8fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:27:45 -0700 Subject: [PATCH 22/57] fix(network): apply rustfmt to BiDi JSON exports --- crates/originweave-network/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 63e6f193d..ea12b0a14 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -30,7 +30,7 @@ pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan, }; pub use webdriver_bidi_json_envelope::{ - MAX_WEBDRIVER_BIDI_JSON_DEPTH, MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, + MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, }; pub use webdriver_bidi_websocket_frame::{ From 5d9e1ffd67b7321cb3b3679db04986be6b2f805d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 16:42:50 -0700 Subject: [PATCH 23/57] fix(network): apply canonical rustfmt to BiDi JSON parser --- .../src/webdriver_bidi_json_envelope.rs | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index efbe0bde3..d7ed4883e 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -132,18 +132,23 @@ impl fmt::Display for WebDriverBiDiJsonEnvelopeError { Self::RootMustBeObject => { formatter.write_str("WebDriver BiDi local-end message must be a JSON object") } - Self::DuplicateTopLevelMember => { - formatter.write_str("WebDriver BiDi JSON object contains a duplicate top-level member") - } + Self::DuplicateTopLevelMember => formatter + .write_str("WebDriver BiDi JSON object contains a duplicate top-level member"), Self::NestingTooDeep { maximum_depth } => write!( formatter, "WebDriver BiDi JSON nesting exceeds maximum depth {maximum_depth}" ), Self::MissingRequiredMember { member } => { - write!(formatter, "WebDriver BiDi envelope is missing required member {member}") + write!( + formatter, + "WebDriver BiDi envelope is missing required member {member}" + ) } Self::InvalidMember { member } => { - write!(formatter, "WebDriver BiDi envelope member {member} is invalid") + write!( + formatter, + "WebDriver BiDi envelope member {member} is invalid" + ) } Self::UnsupportedEnvelopeType => { formatter.write_str("unsupported WebDriver BiDi local-end envelope type") @@ -199,9 +204,7 @@ impl TopLevelFields { } } - fn into_success( - self, - ) -> Result { + fn into_success(self) -> Result { let command_id = required_js_uint(self.id, "id")?; require_object(self.result, "result")?; Ok(WebDriverBiDiJsonEnvelope { @@ -348,17 +351,12 @@ impl<'a> JsonCursor<'a> { } fn skip_whitespace(&mut self) { - while matches!( - self.current_byte(), - Some(b' ' | b'\t' | b'\n' | b'\r') - ) { + while matches!(self.current_byte(), Some(b' ' | b'\t' | b'\n' | b'\r')) { self.index += 1; } } - fn parse_top_level_object( - &mut self, - ) -> Result { + fn parse_top_level_object(&mut self) -> Result { self.skip_whitespace(); if !self.consume_byte(b'{') { return Err(WebDriverBiDiJsonEnvelopeError::RootMustBeObject); @@ -389,10 +387,7 @@ impl<'a> JsonCursor<'a> { } } - fn parse_value( - &mut self, - depth: usize, - ) -> Result { + fn parse_value(&mut self, depth: usize) -> Result { if depth > MAX_WEBDRIVER_BIDI_JSON_DEPTH { return Err(WebDriverBiDiJsonEnvelopeError::NestingTooDeep { maximum_depth: MAX_WEBDRIVER_BIDI_JSON_DEPTH, @@ -667,10 +662,9 @@ mod tests { assert!(!debug.contains("invalid argument")); assert!(!debug.contains("secret detail")); - let event = parse( - r#"{"type":"event","method":"browsingContext.load","params":{},"vendor":true}"#, - ) - .ok(); + let event = + parse(r#"{"type":"event","method":"browsingContext.load","params":{},"vendor":true}"#) + .ok(); assert!(event.is_some()); let Some(event) = event else { return; @@ -684,7 +678,13 @@ mod tests { #[test] fn accepts_correlatable_error_and_extensible_success_members() { let error = parse(r#"{"type":"error","id":7,"error":"unknown error","message":"","x":0}"#); - assert_eq!(error.as_ref().ok().and_then(WebDriverBiDiJsonEnvelope::command_id), Some(7)); + assert_eq!( + error + .as_ref() + .ok() + .and_then(WebDriverBiDiJsonEnvelope::command_id), + Some(7) + ); let success = parse( "{\n \"\\u0074ype\":\"success\", \"id\":0, \"result\":{\"escaped\":\"\\\\/\\b\\f\\n\\r\\t\\\"\",\"unicode\":\"é\"}, \"method\":123 } \r\n", @@ -753,7 +753,9 @@ mod tests { ), ( r#"{"type":"error","id":null,"error":"x","message":"m","stacktrace":0}"#, - WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "stacktrace" }, + WebDriverBiDiJsonEnvelopeError::InvalidMember { + member: "stacktrace", + }, ), ( r#"{"type":"event","params":{}}"#, From 552a10d22e74ba11dea97dc7d16c1e8f228ef980 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:10:53 -0700 Subject: [PATCH 24/57] test(network): exercise BiDi JSON parser boundaries --- .../tests/webdriver_bidi_json_envelope.rs | 90 ++++++++++++++++--- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index 4dad5a90c..a435d5f88 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -8,15 +8,16 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; -const SUCCESS_MESSAGE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true}}"#; +const SUCCESS_MESSAGE: &[u8] = br#"{\"type\":\"success\",\"id\":7,\"result\":{\"ready\":true}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -35,18 +36,24 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -#[test] -fn real_transport_text_is_classified_as_bidi_success_envelope() -> Result<(), Box> { +fn parse_over_real_transport( + payload: &[u8], +) -> Result, Box> { + if payload.len() > 125 { + return Err(io::Error::other("test payload exceeded one-byte frame length").into()); + } + let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; + let payload = payload.to_vec(); let server = thread::spawn(move || -> io::Result<()> { let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; - let payload_len = u8::try_from(SUCCESS_MESSAGE.len()) - .map_err(|_| io::Error::other("test payload exceeded one-byte frame length"))?; + let payload_len = u8::try_from(payload.len()) + .map_err(|_| io::Error::other("test payload length does not fit u8"))?; stream.write_all(&[0x81, payload_len])?; - stream.write_all(SUCCESS_MESSAGE) + stream.write_all(&payload) }); let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); @@ -71,14 +78,69 @@ fn real_transport_text_is_classified_as_bidi_success_envelope() -> Result<(), Bo .into()); } }; - let envelope = WebDriverBiDiJsonEnvelope::parse(&text)?; - assert_eq!(envelope.kind(), WebDriverBiDiJsonEnvelopeKind::Success); - assert_eq!(envelope.command_id(), Some(7)); - assert_eq!(envelope.method(), None); - assert_eq!(envelope.error_code(), None); + let parsed = WebDriverBiDiJsonEnvelope::parse(&text); server .join() .map_err(|_| io::Error::other("JSON-envelope server panicked"))??; + Ok(parsed) +} + +#[test] +fn real_transport_text_is_classified_as_bidi_success_envelope() -> Result<(), Box> { + let envelope = parse_over_real_transport(SUCCESS_MESSAGE)?; + assert_eq!( + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Success) + ); + assert_eq!( + envelope + .as_ref() + .map(WebDriverBiDiJsonEnvelope::command_id), + Ok(Some(7)) + ); + assert_eq!( + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::method), + Ok(None) + ); + assert_eq!( + envelope + .as_ref() + .map(WebDriverBiDiJsonEnvelope::error_code), + Ok(None) + ); + Ok(()) +} + +#[test] +fn real_transport_exercises_valid_escape_boundaries() -> Result<(), Box> { + let envelope = parse_over_real_transport( + br#"{\"type\":\"success\",\"id\":1,\"result\":{\"slash\":\"\\/\",\"upper\":\"\\uABCD\",\"edge\":\"\\uFFFF\"}}"#, + )?; + assert_eq!( + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Success) + ); + Ok(()) +} + +#[test] +fn real_transport_rejects_malformed_json_at_parser_boundaries() -> Result<(), Box> { + let cases: &[&[u8]] = &[ + br#"{\"type\":\"success\",\"unterminated"#, + br#"{\"type\" \"success\"}"#, + br#"{\"type\":\"success\" \"id\":1,\"result\":{}}"#, + br#"{\"type\":\"success\",\"id\":1,\"result\":{\"x\" 1}}"#, + br#"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":[1 2]}}"#, + b"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":\"\\", + br#"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":\"\\ud800\\x\"}}"#, + b"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":\"\\ud800\\u", + ]; + for document in cases { + assert_eq!( + parse_over_real_transport(document)?, + Err(WebDriverBiDiJsonEnvelopeError::InvalidJson) + ); + } Ok(()) } From 7b8b67c6c6d55ed6e173aade5a835309f3af5a2a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:11:33 -0700 Subject: [PATCH 25/57] fix(test): send literal BiDi JSON over loopback --- .../tests/webdriver_bidi_json_envelope.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index a435d5f88..33a6d0eb2 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -17,7 +17,7 @@ use originweave_network::{ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; -const SUCCESS_MESSAGE: &[u8] = br#"{\"type\":\"success\",\"id\":7,\"result\":{\"ready\":true}}"#; +const SUCCESS_MESSAGE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -115,7 +115,7 @@ fn real_transport_text_is_classified_as_bidi_success_envelope() -> Result<(), Bo #[test] fn real_transport_exercises_valid_escape_boundaries() -> Result<(), Box> { let envelope = parse_over_real_transport( - br#"{\"type\":\"success\",\"id\":1,\"result\":{\"slash\":\"\\/\",\"upper\":\"\\uABCD\",\"edge\":\"\\uFFFF\"}}"#, + br#"{"type":"success","id":1,"result":{"slash":"\/","upper":"\uABCD","edge":"\uFFFF"}}"#, )?; assert_eq!( envelope.as_ref().map(WebDriverBiDiJsonEnvelope::kind), @@ -127,13 +127,13 @@ fn real_transport_exercises_valid_escape_boundaries() -> Result<(), Box Result<(), Box> { let cases: &[&[u8]] = &[ - br#"{\"type\":\"success\",\"unterminated"#, - br#"{\"type\" \"success\"}"#, - br#"{\"type\":\"success\" \"id\":1,\"result\":{}}"#, - br#"{\"type\":\"success\",\"id\":1,\"result\":{\"x\" 1}}"#, - br#"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":[1 2]}}"#, + br#"{"type":"success","unterminated"#, + br#"{"type" "success"}"#, + br#"{"type":"success" "id":1,"result":{}}"#, + br#"{"type":"success","id":1,"result":{"x" 1}}"#, + br#"{"type":"success","id":1,"result":{"x":[1 2]}}"#, b"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":\"\\", - br#"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":\"\\ud800\\x\"}}"#, + br#"{"type":"success","id":1,"result":{"x":"\ud800\x"}}"#, b"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":\"\\ud800\\u", ]; for document in cases { From 1c47c14bf5121ed18e6fbf624bfd6db508d23edf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:13:25 -0700 Subject: [PATCH 26/57] style(test): apply canonical rustfmt --- .../tests/webdriver_bidi_json_envelope.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index 33a6d0eb2..043244936 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -94,9 +94,7 @@ fn real_transport_text_is_classified_as_bidi_success_envelope() -> Result<(), Bo Ok(WebDriverBiDiJsonEnvelopeKind::Success) ); assert_eq!( - envelope - .as_ref() - .map(WebDriverBiDiJsonEnvelope::command_id), + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), Ok(Some(7)) ); assert_eq!( @@ -104,9 +102,7 @@ fn real_transport_text_is_classified_as_bidi_success_envelope() -> Result<(), Bo Ok(None) ); assert_eq!( - envelope - .as_ref() - .map(WebDriverBiDiJsonEnvelope::error_code), + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::error_code), Ok(None) ); Ok(()) From 3a1a87c785318fc22c1523359f5c39a6f112b53f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:19:42 -0700 Subject: [PATCH 27/57] fix(network): remove impossible BiDi parser evidence branches --- .../src/webdriver_bidi_json_envelope.rs | 108 +++++++++++------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index d7ed4883e..e99adf2c9 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -422,7 +422,9 @@ impl<'a> JsonCursor<'a> { } fn parse_object(&mut self, depth: usize) -> Result<(), WebDriverBiDiJsonEnvelopeError> { - self.expect_byte(b'{')?; + // `parse_value` dispatches here only after observing `{`; consume that proven delimiter + // directly so an impossible second validation branch does not masquerade as parser evidence. + self.index += 1; self.skip_whitespace(); if self.consume_byte(b'}') { return Ok(()); @@ -443,7 +445,9 @@ impl<'a> JsonCursor<'a> { } fn parse_array(&mut self, depth: usize) -> Result<(), WebDriverBiDiJsonEnvelopeError> { - self.expect_byte(b'[')?; + // `parse_value` dispatches here only after observing `[`; consume that proven delimiter + // directly so an impossible second validation branch does not masquerade as parser evidence. + self.index += 1; self.skip_whitespace(); if self.consume_byte(b']') { return Ok(()); @@ -545,12 +549,12 @@ impl<'a> JsonCursor<'a> { self.index += 1; } _ => { - let remaining = &self.input[self.index..]; - let Some(character) = remaining.chars().next() else { - return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); - }; - output.push(character); - self.index += character.len_utf8(); + // `input` is valid UTF-8 and `index` advances only on character boundaries. + // For a non-ASCII lead byte, leading_ones therefore yields the exact width. + let character_byte_count = byte.leading_ones() as usize; + let end = self.index + character_byte_count; + output.push_str(&self.input[self.index..end]); + self.index = end; } } } @@ -594,10 +598,9 @@ impl<'a> JsonCursor<'a> { } else { u32::from(first) }; - let Some(character) = char::from_u32(scalar) else { - return Err(WebDriverBiDiJsonEnvelopeError::InvalidJson); - }; - output.push(character); + // `scalar` is either a non-surrogate `u16` or the scalar constructed from a validated + // high/low surrogate pair, so `char::from_u32` is always `Some` under this parser invariant. + output.extend(char::from_u32(scalar)); Ok(()) } @@ -632,47 +635,68 @@ mod tests { fn classifies_all_local_end_envelope_kinds_and_redacts_debug() { let success = parse( r#"{"type":"success","id":9007199254740991,"result":{"ready":true},"ext":[null,false,1.5,-2e3,"\u20ac","\ud83d\ude00"]}"#, - ) - .ok(); - assert!(success.is_some()); - let Some(success) = success else { - return; - }; - assert_eq!(success.kind(), WebDriverBiDiJsonEnvelopeKind::Success); - assert_eq!(success.command_id(), Some(MAX_WEBDRIVER_BIDI_JS_UINT)); - assert_eq!(success.method(), None); - assert_eq!(success.error_code(), None); + ); + assert_eq!( + success.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Success) + ); + assert_eq!( + success.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), + Ok(Some(MAX_WEBDRIVER_BIDI_JS_UINT)) + ); + assert_eq!( + success.as_ref().map(WebDriverBiDiJsonEnvelope::method), + Ok(None) + ); + assert_eq!( + success.as_ref().map(WebDriverBiDiJsonEnvelope::error_code), + Ok(None) + ); let debug = format!("{success:?}"); assert!(debug.contains("Success")); assert!(!debug.contains("ready")); let error = parse( r#"{"type":"error","id":null,"error":"invalid argument","message":"secret detail","stacktrace":"hidden","vendor":{"x":[]}}"#, - ) - .ok(); - assert!(error.is_some()); - let Some(error) = error else { - return; - }; - assert_eq!(error.kind(), WebDriverBiDiJsonEnvelopeKind::Error); - assert_eq!(error.command_id(), None); - assert_eq!(error.error_code(), Some("invalid argument")); - assert_eq!(error.method(), None); + ); + assert_eq!( + error.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Error) + ); + assert_eq!( + error.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), + Ok(None) + ); + assert_eq!( + error.as_ref().map(WebDriverBiDiJsonEnvelope::error_code), + Ok(Some("invalid argument")) + ); + assert_eq!( + error.as_ref().map(WebDriverBiDiJsonEnvelope::method), + Ok(None) + ); let debug = format!("{error:?}"); assert!(!debug.contains("invalid argument")); assert!(!debug.contains("secret detail")); let event = - parse(r#"{"type":"event","method":"browsingContext.load","params":{},"vendor":true}"#) - .ok(); - assert!(event.is_some()); - let Some(event) = event else { - return; - }; - assert_eq!(event.kind(), WebDriverBiDiJsonEnvelopeKind::Event); - assert_eq!(event.command_id(), None); - assert_eq!(event.method(), Some("browsingContext.load")); - assert_eq!(event.error_code(), None); + parse(r#"{"type":"event","method":"browsingContext.load","params":{},"vendor":true}"#); + assert_eq!( + event.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Event) + ); + assert_eq!( + event.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), + Ok(None) + ); + assert_eq!( + event.as_ref().map(WebDriverBiDiJsonEnvelope::method), + Ok(Some("browsingContext.load")) + ); + assert_eq!( + event.as_ref().map(WebDriverBiDiJsonEnvelope::error_code), + Ok(None) + ); } #[test] From ac6b532ce164ea01262cb211d22d53db1cdff96b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:32:22 -0700 Subject: [PATCH 28/57] test(network): cover public BiDi JSON parser branches over loopback --- .../tests/webdriver_bidi_json_envelope.rs | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index 043244936..3f6dfb76f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -109,26 +109,63 @@ fn real_transport_text_is_classified_as_bidi_success_envelope() -> Result<(), Bo } #[test] -fn real_transport_exercises_valid_escape_boundaries() -> Result<(), Box> { - let envelope = parse_over_real_transport( - br#"{"type":"success","id":1,"result":{"slash":"\/","upper":"\uABCD","edge":"\uFFFF"}}"#, +fn real_transport_classifies_error_and_event_envelopes() -> Result<(), Box> { + let error = parse_over_real_transport( + br#"{"type":"error","id":null,"error":"unknown error","message":"","stacktrace":"hidden"}"#, )?; assert_eq!( - envelope.as_ref().map(WebDriverBiDiJsonEnvelope::kind), - Ok(WebDriverBiDiJsonEnvelopeKind::Success) + error.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Error) + ); + assert_eq!( + error.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), + Ok(None) + ); + + let event = parse_over_real_transport( + br#"{"type":"event","method":"browsingContext.load","params":{}}"#, + )?; + assert_eq!( + event.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Event) + ); + assert_eq!( + event.as_ref().map(WebDriverBiDiJsonEnvelope::method), + Ok(Some("browsingContext.load")) ); Ok(()) } +#[test] +fn real_transport_exercises_valid_json_boundaries() -> Result<(), Box> { + let cases: &[&[u8]] = &[ + br#"{"type":"success","id":1,"result":{"slash":"\/","upper":"\uABCD","edge":"\uFFFF"}}"#, + br#"{"type":"success","id":1,"result":{"array":[],"number":-2.5e3}}"#, + ]; + for document in cases { + assert_eq!( + parse_over_real_transport(document)? + .as_ref() + .map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Success) + ); + } + Ok(()) +} + #[test] fn real_transport_rejects_malformed_json_at_parser_boundaries() -> Result<(), Box> { let cases: &[&[u8]] = &[ + br#"{"type":"success","id":1,"result":{}} trailing"#, br#"{"type":"success","unterminated"#, br#"{"type" "success"}"#, br#"{"type":"success" "id":1,"result":{}}"#, br#"{"type":"success","id":1,"result":{"x" 1}}"#, br#"{"type":"success","id":1,"result":{"x":[1 2]}}"#, b"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":\"\\", + br#"{"type":"success","id":1,"result":{"x":"\q"}}"#, + br#"{"type":"success","id":1,"result":{"x":"\u12xz"}}"#, + br#"{"type":"success","id":1,"result":{"x":"\udc00"}}"#, br#"{"type":"success","id":1,"result":{"x":"\ud800\x"}}"#, b"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":\"\\ud800\\u", ]; From 32a7d964ee1398aa5ba14af2eba19ca16529081c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:39:24 -0700 Subject: [PATCH 29/57] test(network): cover malformed BiDi error ids --- .../tests/webdriver_bidi_json_envelope.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index 3f6dfb76f..b92f7bc2c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -177,3 +177,15 @@ fn real_transport_rejects_malformed_json_at_parser_boundaries() -> Result<(), Bo } Ok(()) } + +#[test] +fn real_transport_rejects_negative_error_response_id() -> Result<(), Box> { + let error = parse_over_real_transport( + br#"{"type":"error","id":-1,"error":"invalid argument","message":"bad id"}"#, + )?; + assert_eq!( + error, + Err(WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }) + ); + Ok(()) +} From b3d26d155edfd39242e0604d3abe7e4e29d0530b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:40:54 -0700 Subject: [PATCH 30/57] test(network): cover remaining BiDi parser transport paths --- .../tests/webdriver_bidi_json_envelope.rs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index b92f7bc2c..b0bebc568 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -110,17 +110,18 @@ fn real_transport_text_is_classified_as_bidi_success_envelope() -> Result<(), Bo #[test] fn real_transport_classifies_error_and_event_envelopes() -> Result<(), Box> { - let error = parse_over_real_transport( + let cases: &[&[u8]] = &[ br#"{"type":"error","id":null,"error":"unknown error","message":"","stacktrace":"hidden"}"#, - )?; - assert_eq!( - error.as_ref().map(WebDriverBiDiJsonEnvelope::kind), - Ok(WebDriverBiDiJsonEnvelopeKind::Error) - ); - assert_eq!( - error.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), - Ok(None) - ); + br#"{"type":"error","id":7,"error":"unknown error","message":""}"#, + ]; + for document in cases { + assert_eq!( + parse_over_real_transport(document)? + .as_ref() + .map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Error) + ); + } let event = parse_over_real_transport( br#"{"type":"event","method":"browsingContext.load","params":{}}"#, @@ -140,7 +141,8 @@ fn real_transport_classifies_error_and_event_envelopes() -> Result<(), Box Result<(), Box> { let cases: &[&[u8]] = &[ br#"{"type":"success","id":1,"result":{"slash":"\/","upper":"\uABCD","edge":"\uFFFF"}}"#, - br#"{"type":"success","id":1,"result":{"array":[],"number":-2.5e3}}"#, + br#"{"type":"success","id":1,"result":{"array":[1,2],"number":-2.5e3}}"#, + br#"{"type":"success","id":1,"result":{"pair":"\ud83d\ude00","empty":[]}}"#, ]; for document in cases { assert_eq!( @@ -161,7 +163,9 @@ fn real_transport_rejects_malformed_json_at_parser_boundaries() -> Result<(), Bo br#"{"type" "success"}"#, br#"{"type":"success" "id":1,"result":{}}"#, br#"{"type":"success","id":1,"result":{"x" 1}}"#, + br#"{"type":"success","id":1,"result":{"x":1 "y":2}}"#, br#"{"type":"success","id":1,"result":{"x":[1 2]}}"#, + br#"{"type":"success","id":1,"result":{"x":[1,]}}"#, b"{\"type\":\"success\",\"id\":1,\"result\":{\"x\":\"\\", br#"{"type":"success","id":1,"result":{"x":"\q"}}"#, br#"{"type":"success","id":1,"result":{"x":"\u12xz"}}"#, From 6ef3c4cea1871cd2e13ea164c5558a841e583ab1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:45:48 -0700 Subject: [PATCH 31/57] test(network): cover raw UTF-8 BiDi JSON transport --- crates/originweave-network/tests/webdriver_bidi_json_envelope.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index b0bebc568..1bdaac893 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -143,6 +143,7 @@ fn real_transport_exercises_valid_json_boundaries() -> Result<(), Box br#"{"type":"success","id":1,"result":{"slash":"\/","upper":"\uABCD","edge":"\uFFFF"}}"#, br#"{"type":"success","id":1,"result":{"array":[1,2],"number":-2.5e3}}"#, br#"{"type":"success","id":1,"result":{"pair":"\ud83d\ude00","empty":[]}}"#, + br#"{"type":"success","id":1,"result":{"unicode":"é"}}"#, ]; for document in cases { assert_eq!( From 6a0ffd684933e38c8dcec79a5e35fc465bed394c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:47:37 -0700 Subject: [PATCH 32/57] fix(test): encode raw UTF-8 BiDi fixture as str bytes --- .../originweave-network/tests/webdriver_bidi_json_envelope.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index 1bdaac893..0fdcfb5de 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -143,7 +143,7 @@ fn real_transport_exercises_valid_json_boundaries() -> Result<(), Box br#"{"type":"success","id":1,"result":{"slash":"\/","upper":"\uABCD","edge":"\uFFFF"}}"#, br#"{"type":"success","id":1,"result":{"array":[1,2],"number":-2.5e3}}"#, br#"{"type":"success","id":1,"result":{"pair":"\ud83d\ude00","empty":[]}}"#, - br#"{"type":"success","id":1,"result":{"unicode":"é"}}"#, + r#"{"type":"success","id":1,"result":{"unicode":"é"}}"#.as_bytes(), ]; for document in cases { assert_eq!( From dba07d96815295e53135d4c34ab6c5f4fbd22fbe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 18:06:36 -0700 Subject: [PATCH 33/57] test(network): cover missing BiDi envelope members over real transport --- .../tests/webdriver_bidi_json_envelope.rs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index 0fdcfb5de..4093c505b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -183,6 +183,48 @@ fn real_transport_rejects_malformed_json_at_parser_boundaries() -> Result<(), Bo Ok(()) } +#[test] +fn real_transport_rejects_missing_required_envelope_members() -> Result<(), Box> { + let cases: &[(&[u8], WebDriverBiDiJsonEnvelopeError)] = &[ + ( + br#"{}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "type" }, + ), + ( + br#"{"type":"success","result":{}}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "id" }, + ), + ( + br#"{"type":"success","id":1}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "result" }, + ), + ( + br#"{"type":"error","error":"x","message":"m"}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "id" }, + ), + ( + br#"{"type":"error","id":null,"message":"m"}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, + ), + ( + br#"{"type":"error","id":null,"error":"x"}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "message" }, + ), + ( + br#"{"type":"event","params":{}}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "method" }, + ), + ( + br#"{"type":"event","method":"x"}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "params" }, + ), + ]; + for (document, expected) in cases { + assert_eq!(parse_over_real_transport(document)?, Err(expected.clone())); + } + Ok(()) +} + #[test] fn real_transport_rejects_negative_error_response_id() -> Result<(), Box> { let error = parse_over_real_transport( From 57750f9d473f79e269673d0c7237c1f2d911e113 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 18:15:02 -0700 Subject: [PATCH 34/57] test(network): exercise BiDi debug and display on public transport path --- .../tests/webdriver_bidi_json_envelope.rs | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index 4093c505b..856fe5621 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -8,8 +8,8 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, + WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, }; @@ -225,6 +225,60 @@ fn real_transport_rejects_missing_required_envelope_members() -> Result<(), Box< Ok(()) } +#[test] +fn real_transport_debug_and_error_display_remain_payload_minimal() -> Result<(), Box> { + let parsed = parse_over_real_transport( + br#"{"type":"success","id":7,"result":{"ready":"sensitive-result"}}"#, + )?; + let envelope = parsed.map_err(|error| io::Error::other(error.to_string()))?; + let debug = format!("{envelope:?}"); + assert!(debug.contains("Success")); + assert!(!debug.contains("ready")); + assert!(!debug.contains("sensitive-result")); + + let cases: &[(&[u8], WebDriverBiDiJsonEnvelopeError)] = &[ + (br#"[]"#, WebDriverBiDiJsonEnvelopeError::RootMustBeObject), + ( + br#"{"type":"success","type":"event","id":1,"result":{}}"#, + WebDriverBiDiJsonEnvelopeError::DuplicateTopLevelMember, + ), + ( + br#"{"type":"other"}"#, + WebDriverBiDiJsonEnvelopeError::UnsupportedEnvelopeType, + ), + ( + br#"{}"#, + WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "type" }, + ), + ( + br#"{"type":"error","id":-1,"error":"x","message":"secret-message"}"#, + WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, + ), + ( + br#"{"type":"success","id":1,"result":{}} trailing secret"#, + WebDriverBiDiJsonEnvelopeError::InvalidJson, + ), + ]; + for (document, expected) in cases { + let parsed = parse_over_real_transport(document)?; + let error = parsed + .err() + .ok_or_else(|| io::Error::other("invalid envelope unexpectedly parsed"))?; + assert_eq!(error, expected.clone()); + let display = error.to_string(); + assert!(!display.is_empty()); + assert!(!display.contains("secret-message")); + assert!(!display.contains("trailing secret")); + } + + let nesting_error = WebDriverBiDiJsonEnvelopeError::NestingTooDeep { + maximum_depth: MAX_WEBDRIVER_BIDI_JSON_DEPTH, + }; + let display = nesting_error.to_string(); + assert!(display.contains("maximum depth")); + Ok(()) +} + #[test] fn real_transport_rejects_negative_error_response_id() -> Result<(), Box> { let error = parse_over_real_transport( From 627f2c5ec3040e4e48b0381ae1e9bfc73bc2e1f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 18:23:42 -0700 Subject: [PATCH 35/57] test(network): expose test-only bounded BiDi text fixture --- .../src/webdriver_bidi_websocket_message.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 3ad3edc35..67555f4d9 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -69,6 +69,11 @@ impl WebDriverBiDiWebSocketTextMessage { pub fn as_str(&self) -> &str { &self.0 } + + #[cfg(test)] + pub(crate) fn from_test_text(value: &str) -> Self { + Self(value.to_owned()) + } } /// Result of admitting one RFC 6455 frame into the bounded BiDi message assembler. From 36466c8326ad7a965bbec54c22c3fc8050462ff0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 18:31:18 -0700 Subject: [PATCH 36/57] fix(network): remove unused BiDi text test fixture --- .../src/webdriver_bidi_websocket_message.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs index 67555f4d9..3ad3edc35 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_message.rs @@ -69,11 +69,6 @@ impl WebDriverBiDiWebSocketTextMessage { pub fn as_str(&self) -> &str { &self.0 } - - #[cfg(test)] - pub(crate) fn from_test_text(value: &str) -> Self { - Self(value.to_owned()) - } } /// Result of admitting one RFC 6455 frame into the bounded BiDi message assembler. From 0a364e56883462e09d48ab4915a5fbedc7762472 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 18:45:32 -0700 Subject: [PATCH 37/57] test(network): exercise public BiDi JSON boundary in unit build --- crates/originweave-network/src/lib.rs | 3 + ...idi_json_envelope_public_boundary_tests.rs | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index ea12b0a14..79dd97492 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -21,6 +21,9 @@ mod webdriver_bidi_websocket_handshake; mod webdriver_bidi_websocket_message; mod webdriver_bidi_websocket_opening_recovery; +#[cfg(test)] +mod webdriver_bidi_json_envelope_public_boundary_tests; + pub use connection::{ ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, NetworkError, SocketConnectionEvidence, diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs new file mode 100644 index 000000000..f9c9fe318 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -0,0 +1,87 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; + +use crate::{ + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const SUCCESS_MESSAGE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true}}"#; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +#[test] +fn public_json_envelope_boundary_is_exercised_from_unit_build() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x81, SUCCESS_MESSAGE.len() as u8])?; + stream.write_all(SUCCESS_MESSAGE) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint)?; + let correlated = admitted.correlate_session_id(SESSION_ID)?; + let target = correlated.into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let parsed = WebDriverBiDiJsonEnvelope::parse(&text); + + server + .join() + .map_err(|_| io::Error::other("JSON-envelope unit server panicked"))??; + assert_eq!( + parsed.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Success) + ); + assert_eq!( + parsed.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), + Ok(Some(7)) + ); + Ok(()) +} From 51ae91835e537dff21f0b1da80be2eeb19a9db12 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:13:37 -0700 Subject: [PATCH 38/57] test(network): cover BiDi JSON parser residual paths --- ...idi_json_envelope_public_boundary_tests.rs | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index f9c9fe318..5688bdcb4 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -9,15 +9,17 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; -const SUCCESS_MESSAGE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true}}"#; +const SUCCESS_MESSAGE: &[u8] = + br#"{"type":"success","id":7,"result":{"ready":true,"upper":"\uABCD"}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -36,16 +38,21 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -#[test] -fn public_json_envelope_boundary_is_exercised_from_unit_build() -> Result<(), Box> { +fn parse_over_loopback( + document: &'static [u8], +) -> Result, Box> { + if document.len() > 125 { + return Err(io::Error::other("unit JSON document exceeded one-byte frame length").into()); + } + let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; - stream.write_all(&[0x81, SUCCESS_MESSAGE.len() as u8])?; - stream.write_all(SUCCESS_MESSAGE) + stream.write_all(&[0x81, document.len() as u8])?; + stream.write_all(document) }); let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); @@ -75,6 +82,12 @@ fn public_json_envelope_boundary_is_exercised_from_unit_build() -> Result<(), Bo server .join() .map_err(|_| io::Error::other("JSON-envelope unit server panicked"))??; + Ok(parsed) +} + +#[test] +fn public_json_envelope_boundary_is_exercised_from_unit_build() -> Result<(), Box> { + let parsed = parse_over_loopback(SUCCESS_MESSAGE)?; assert_eq!( parsed.as_ref().map(WebDriverBiDiJsonEnvelope::kind), Ok(WebDriverBiDiJsonEnvelopeKind::Success) @@ -85,3 +98,25 @@ fn public_json_envelope_boundary_is_exercised_from_unit_build() -> Result<(), Bo ); Ok(()) } + +#[test] +fn public_json_envelope_unit_build_covers_fail_closed_json_edges() -> Result<(), Box> { + let malformed_documents: [&'static [u8]; 8] = [ + br#"{"unterminated"#, + br#"{"type" "success"}"#, + br#"{"type":"success" "id":1}"#, + br#"{"type":"success","id":1,"result":{"a" 1}}"#, + br#"{"type":"success","id":1,"result":[1 2]}"#, + br##"{"type":"success","id":1,"result":{"bad":"\"##, + br#"{"type":"success","id":1,"result":{"bad":"\ud800\0041"}}"#, + br##"{"type":"success","id":1,"result":{"bad":"\ud800\u"##, + ]; + + for document in malformed_documents { + assert_eq!( + parse_over_loopback(document)?, + Err(WebDriverBiDiJsonEnvelopeError::InvalidJson) + ); + } + Ok(()) +} From e6d5166825d0b2e7fea95b7adb6880ee64e6a6c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:17:45 -0700 Subject: [PATCH 39/57] test(network): cover escaped slash in BiDi JSON unit path --- .../src/webdriver_bidi_json_envelope_public_boundary_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 5688bdcb4..27377f08d 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -19,7 +19,7 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; const SUCCESS_MESSAGE: &[u8] = - br#"{"type":"success","id":7,"result":{"ready":true,"upper":"\uABCD"}}"#; + br#"{"type":"success","id":7,"result":{"ready":true,"slash":"\/","upper":"\uABCD"}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; From 1bdfb42b35f1bda118e4b4b1a4d331db2190c3bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:41:05 +0900 Subject: [PATCH 40/57] test(network): reject non-protocol BiDi error codes --- .../tests/webdriver_bidi_json_envelope.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index 856fe5621..79ea85da2 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -290,3 +290,15 @@ fn real_transport_rejects_negative_error_response_id() -> Result<(), Box Result<(), Box> { + let error = parse_over_real_transport( + br#"{"type":"error","id":7,"error":"attacker-defined-code","message":"bad code"}"#, + )?; + assert_eq!( + error, + Err(WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "error" }) + ); + Ok(()) +} From 87854fdc3ef84a7c0fc4d6a8baf61fa1532c1c8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:08:44 +0900 Subject: [PATCH 41/57] fix(network): validate WebDriver BiDi error codes --- .../src/webdriver_bidi_json_envelope.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index e99adf2c9..048f03117 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -12,6 +12,39 @@ pub const MAX_WEBDRIVER_BIDI_JSON_DEPTH: usize = 64; /// Largest integer admitted by WebDriver BiDi's `js-uint` production. pub const MAX_WEBDRIVER_BIDI_JS_UINT: u64 = 9_007_199_254_740_991; +const WEBDRIVER_BIDI_ERROR_CODES: [&str; 30] = [ + "invalid argument", + "invalid selector", + "invalid session id", + "invalid web extension", + "move target out of bounds", + "no such alert", + "no such network collector", + "no such element", + "no such frame", + "no such handle", + "no such history entry", + "no such intercept", + "no such network data", + "no such node", + "no such request", + "no such screencast", + "no such script", + "no such storage partition", + "no such user context", + "no such web extension", + "session not created", + "unable to capture screen", + "unable to close browser", + "unable to set cookie", + "unable to set file input", + "unavailable network data", + "underspecified storage partition", + "unknown command", + "unknown error", + "unsupported operation", +]; + /// Local-end WebDriver BiDi envelope kind after complete JSON syntax validation. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WebDriverBiDiJsonEnvelopeKind { @@ -222,6 +255,9 @@ impl TopLevelFields { if let Some(stacktrace) = self.stacktrace { require_text_value(stacktrace, "stacktrace")?; } + if !is_webdriver_bidi_error_code(&error_code) { + return Err(invalid("error")); + } Ok(WebDriverBiDiJsonEnvelope { kind: WebDriverBiDiJsonEnvelopeKind::Error, command_id, @@ -250,6 +286,10 @@ fn invalid(member: &'static str) -> WebDriverBiDiJsonEnvelopeError { WebDriverBiDiJsonEnvelopeError::InvalidMember { member } } +fn is_webdriver_bidi_error_code(value: &str) -> bool { + WEBDRIVER_BIDI_ERROR_CODES.contains(&value) +} + fn required_text( value: Option, member: &'static str, @@ -699,6 +739,28 @@ mod tests { ); } + #[test] + fn accepts_current_protocol_error_code_vocabulary() { + for error_code in WEBDRIVER_BIDI_ERROR_CODES { + let document = format!( + r#"{{"type":"error","id":7,"error":"{error_code}","message":""}}"# + ); + assert_eq!( + parse(&document) + .as_ref() + .ok() + .and_then(WebDriverBiDiJsonEnvelope::error_code), + Some(error_code) + ); + } + assert_eq!( + parse( + r#"{"type":"error","id":7,"error":"attacker-defined-code","message":"bad code"}"# + ), + Err(WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "error" }) + ); + } + #[test] fn accepts_correlatable_error_and_extensible_success_members() { let error = parse(r#"{"type":"error","id":7,"error":"unknown error","message":"","x":0}"#); From 53acd599d85b63f78d1756a118ce0af2ad8aa1be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:14:37 +0900 Subject: [PATCH 42/57] docs(doctoring): correct WebDriver BiDi ErrorCode evidence --- docs/doctoring/browser-agent-protocols.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index dbf3ef731..da48f1055 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -1,6 +1,6 @@ # Browser and Agent Protocol Standards Evidence -- **Reviewed:** 2026-08-18 +- **Reviewed:** 2026-09-04 - **Purpose:** primary-source evidence for OriginWeave browser compatibility and adapter boundaries - **Canonical research index:** [`../doctoring.md`](../doctoring.md) @@ -8,13 +8,13 @@ This addendum complements the main doctoring record. The main record already car ## WebDriver BiDi -The latest published W3C technical-report baseline reviewed here remains the 1 June 2026 **Working Draft**, not a Recommendation. The current Editor’s Draft reviewed on 18 August 2026 identifies itself as the 20 July 2026 draft. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. +The latest published W3C technical-report baseline reviewed here is the 29 June 2026 **Working Draft**, not a Recommendation. The live Editor’s Draft retrieved on 4 September 2026 identifies itself as the 5 August 2026 draft and explicitly remains work in progress. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. For the bounded `browsingContext.locateNodes` command-serialization boundary, the reviewed Editor’s Draft defines a command envelope with `id: js-uint`, defines `js-uint` as `0..9007199254740991`, and defines `browsingContext.locateNodes` parameters containing a browsing context, locator, optional positive `maxNodeCount`, optional `serializationOptions`, and optional `startNodes`. OriginWeave serializes only its separately reviewed accessibility-locator subset and fixed minimal serialization options; this deterministic JSON value is not transport authentication or browser/Agent authority. WebDriver BiDi commands may execute concurrently and finish out of order. The Editor’s Draft defines the command id as the local end’s correlation identifier and sets a successful `CommandResponse.id` to that exact command id; an `ErrorResponse.id` may be `null` when no valid command id can be recovered. OriginWeave therefore fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. -The same reviewed Editor’s Draft defines a closed `ErrorCode` vocabulary that currently includes `no such client window`. OriginWeave admits only the reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed; adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. +The same reviewed Editor’s Draft defines `ErrorResponse.error` as `ErrorCode` and its local-end CDDL enumerates a finite 30-value vocabulary from `invalid argument` through `unsupported operation`. The CDDL does **not** include `no such client window`; the specification may define additional error concepts elsewhere, but their presence outside the `ErrorCode` production is not authority to admit them in a local-end error response. OriginWeave therefore admits only the reviewed CDDL vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed. Adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. Primary sources: World Wide Web Consortium, *WebDriver BiDi* (published Working Draft and current Editor’s Draft). @@ -55,7 +55,7 @@ The main [`docs/doctoring.md`](../doctoring.md) records the stable W3C PROV-O Re 1. Version adapter contracts independently from OriginWeave session/context/action/evidence types. Admit a BiDi `script.NodeRemoteValue` only as an untrusted transport handle when its type is exactly `node` and a usable control-free `sharedId` is present; do not treat a realm-local `handle`, a missing shared identifier, or control/whitespace-bearing protocol text as OriginWeave node authority. Treat an accessibility-query role as one exact WAI-ARIA token, not a whitespace-separated fallback list. 2. Serialize reviewed BiDi commands from already validated bounded values only, then correlate each non-null response id to the exact consumed command before payload admission; a protocol-shaped JSON envelope or matching id never substitutes for authenticated browser transport, current session/context/origin/document authority, policy authorization, or post-condition evidence. 3. Pin exact Chromium/CDP compatibility evidence at release time. -4. Keep WebDriver BiDi's Working Draft status visible in compatibility claims. +4. Keep WebDriver BiDi's Working Draft/Editor’s Draft status visible in compatibility claims and re-review finite protocol vocabularies when the pinned adapter revision moves. 5. Keep WebMCP experimental/optional and propagate untrusted-content semantics. 6. Keep MCP browser state application-level rather than equating protocol transport/session metadata with browser authority. 7. Test Manifest V3 compatibility and extension-to-Agent authority isolation as separate evidence classes. @@ -83,10 +83,10 @@ World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.o World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/2023/REC-wai-aria-1.2-20230606/ -World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260601/ +World Wide Web Consortium. (2026, June 29). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260629/ -World Wide Web Consortium. (2026, July 20). *WebDriver BiDi* (Editor’s Draft). https://w3c.github.io/webdriver-bidi/ +World Wide Web Consortium. (2026, August 5). *WebDriver BiDi* (Editor’s Draft). Retrieved September 4, 2026, from https://w3c.github.io/webdriver-bidi/ World Wide Web Consortium. (2026, August 5). *Accessible name and description computation 1.2* (W3C Working Draft). https://www.w3.org/TR/2026/WD-accname-1.2-20260805/ -International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html +International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html \ No newline at end of file From 7560fa106dc1eae8d43e030537d7ecb2ae90175d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:15:35 +0900 Subject: [PATCH 43/57] docs(changelog): record BiDi error-code hardening --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e3f7a56..ab15f5791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security +- WebDriver BiDi local-end error envelopes now admit only the finite `ErrorCode` vocabulary reviewed from the current W3C CDDL; arbitrary error strings fail closed before becoming typed response metadata, and realistic loopback coverage exercises the hostile unknown-code path. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. - Crawler mode is read-only, must pair with the public-crawl purpose, and fails closed without an applicable robots-policy decision. @@ -80,7 +81,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Resolver answers are rejected when empty or larger than 256 addresses, preventing an unbounded resolver response from entering policy state. - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. -- Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target cycle state, and hop capacity before policy state changes. +- Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target digest, and hop capacity before policy state changes. - Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. - Direct connection code accepts only an explicit `SocketAddr`, never a hostname, and does not read proxy environment variables. - Established streams are discarded when peer inspection fails or the observed remote IP or port differs from the approved socket. From 7f31e50c901138bd586c5a8f06dbcc295273779f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:17:13 +0900 Subject: [PATCH 44/57] docs(changelog): preserve redirect-cycle evidence wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab15f5791..90f642a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,7 +81,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Resolver answers are rejected when empty or larger than 256 addresses, preventing an unbounded resolver response from entering policy state. - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. -- Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target digest, and hop capacity before policy state changes. +- Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target cycle state, and hop capacity before policy state changes. - Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. - Direct connection code accepts only an explicit `SocketAddr`, never a hostname, and does not read proxy environment variables. - Established streams are discarded when peer inspection fails or the observed remote IP or port differs from the approved socket. From 89140129de304e31f241ae3e77b8b82fe01a6bfa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:06:12 +0900 Subject: [PATCH 45/57] test(network): reject invalid BiDi event method names --- ...bidi_json_envelope_public_boundary_tests.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 27377f08d..593fa41ae 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -99,6 +99,24 @@ fn public_json_envelope_boundary_is_exercised_from_unit_build() -> Result<(), Bo Ok(()) } +#[test] +fn public_json_envelope_rejects_non_event_name_methods() -> Result<(), Box> { + let malformed_methods: [&'static [u8]; 4] = [ + br#"{"type":"event","method":"","params":{}}"#, + br#"{"type":"event","method":"load","params":{}}"#, + br#"{"type":"event","method":".load","params":{}}"#, + br#"{"type":"event","method":"browsingContext.","params":{}}"#, + ]; + + for document in malformed_methods { + assert_eq!( + parse_over_loopback(document)?, + Err(WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "method" }) + ); + } + Ok(()) +} + #[test] fn public_json_envelope_unit_build_covers_fail_closed_json_edges() -> Result<(), Box> { let malformed_documents: [&'static [u8]; 8] = [ From 977c4c3adf3618cfd637bc3987b10dffe155588f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:30:44 +0900 Subject: [PATCH 46/57] fix(network): validate WebDriver BiDi event names --- .../src/webdriver_bidi_json_envelope.rs | 105 ++++++++++-------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index 048f03117..3dcc1a693 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -269,6 +269,9 @@ impl TopLevelFields { fn into_event(self) -> Result { let method = required_text(self.method, "method")?; require_object(self.params, "params")?; + if !is_webdriver_bidi_event_name(&method) { + return Err(invalid("method")); + } Ok(WebDriverBiDiJsonEnvelope { kind: WebDriverBiDiJsonEnvelopeKind::Event, command_id: None, @@ -290,6 +293,13 @@ fn is_webdriver_bidi_error_code(value: &str) -> bool { WEBDRIVER_BIDI_ERROR_CODES.contains(&value) } +fn is_webdriver_bidi_event_name(value: &str) -> bool { + match value.split_once('.') { + Some((module_name, event_name)) => !module_name.is_empty() && !event_name.is_empty(), + None => false, + } +} + fn required_text( value: Option, member: &'static str, @@ -674,7 +684,7 @@ mod tests { #[test] fn classifies_all_local_end_envelope_kinds_and_redacts_debug() { let success = parse( - r#"{"type":"success","id":9007199254740991,"result":{"ready":true},"ext":[null,false,1.5,-2e3,"\u20ac","\ud83d\ude00"]}"#, + r#"{\"type\":\"success\",\"id\":9007199254740991,\"result\":{\"ready\":true},\"ext\":[null,false,1.5,-2e3,\"\\u20ac\",\"\\ud83d\\ude00\"]}"#, ); assert_eq!( success.as_ref().map(WebDriverBiDiJsonEnvelope::kind), @@ -697,7 +707,7 @@ mod tests { assert!(!debug.contains("ready")); let error = parse( - r#"{"type":"error","id":null,"error":"invalid argument","message":"secret detail","stacktrace":"hidden","vendor":{"x":[]}}"#, + r#"{\"type\":\"error\",\"id\":null,\"error\":\"invalid argument\",\"message\":\"secret detail\",\"stacktrace\":\"hidden\",\"vendor\":{\"x\":[]}}"#, ); assert_eq!( error.as_ref().map(WebDriverBiDiJsonEnvelope::kind), @@ -720,7 +730,7 @@ mod tests { assert!(!debug.contains("secret detail")); let event = - parse(r#"{"type":"event","method":"browsingContext.load","params":{},"vendor":true}"#); + parse(r#"{\"type\":\"event\",\"method\":\"browsingContext.load\",\"params\":{},\"vendor\":true}"#); assert_eq!( event.as_ref().map(WebDriverBiDiJsonEnvelope::kind), Ok(WebDriverBiDiJsonEnvelopeKind::Event) @@ -742,9 +752,8 @@ mod tests { #[test] fn accepts_current_protocol_error_code_vocabulary() { for error_code in WEBDRIVER_BIDI_ERROR_CODES { - let document = format!( - r#"{{"type":"error","id":7,"error":"{error_code}","message":""}}"# - ); + let document = + format!(r#"{{\"type\":\"error\",\"id\":7,\"error\":\"{error_code}\",\"message\":\"\"}}"#); assert_eq!( parse(&document) .as_ref() @@ -755,7 +764,7 @@ mod tests { } assert_eq!( parse( - r#"{"type":"error","id":7,"error":"attacker-defined-code","message":"bad code"}"# + r#"{\"type\":\"error\",\"id\":7,\"error\":\"attacker-defined-code\",\"message\":\"bad code\"}"# ), Err(WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "error" }) ); @@ -763,7 +772,7 @@ mod tests { #[test] fn accepts_correlatable_error_and_extensible_success_members() { - let error = parse(r#"{"type":"error","id":7,"error":"unknown error","message":"","x":0}"#); + let error = parse(r#"{\"type\":\"error\",\"id\":7,\"error\":\"unknown error\",\"message\":\"\",\"x\":0}"#); assert_eq!( error .as_ref() @@ -786,77 +795,77 @@ mod tests { WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "type" }, ), ( - r#"{"type":1}"#, + r#"{\"type\":1}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "type" }, ), ( - r#"{"type":"other"}"#, + r#"{\"type\":\"other\"}"#, WebDriverBiDiJsonEnvelopeError::UnsupportedEnvelopeType, ), ( - r#"{"type":"success","result":{}}"#, + r#"{\"type\":\"success\",\"result\":{}}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "id" }, ), ( - r#"{"type":"success","id":null,"result":{}}"#, + r#"{\"type\":\"success\",\"id\":null,\"result\":{}}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, ), ( - r#"{"type":"success","id":9007199254740992,"result":{}}"#, + r#"{\"type\":\"success\",\"id\":9007199254740992,\"result\":{}}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, ), ( - r#"{"type":"success","id":1}"#, + r#"{\"type\":\"success\",\"id\":1}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "result" }, ), ( - r#"{"type":"success","id":1,"result":[]}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":[]}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "result" }, ), ( - r#"{"type":"error","error":"x","message":"m"}"#, + r#"{\"type\":\"error\",\"error\":\"x\",\"message\":\"m\"}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "id" }, ), ( - r#"{"type":"error","id":-1,"error":"x","message":"m"}"#, + r#"{\"type\":\"error\",\"id\":-1,\"error\":\"x\",\"message\":\"m\"}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, ), ( - r#"{"type":"error","id":null,"message":"m"}"#, + r#"{\"type\":\"error\",\"id\":null,\"message\":\"m\"}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, ), ( - r#"{"type":"error","id":null,"error":false,"message":"m"}"#, + r#"{\"type\":\"error\",\"id\":null,\"error\":false,\"message\":\"m\"}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "error" }, ), ( - r#"{"type":"error","id":null,"error":"x"}"#, + r#"{\"type\":\"error\",\"id\":null,\"error\":\"x\"}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "message" }, ), ( - r#"{"type":"error","id":null,"error":"x","message":{}}"#, + r#"{\"type\":\"error\",\"id\":null,\"error\":\"x\",\"message\":{}}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "message" }, ), ( - r#"{"type":"error","id":null,"error":"x","message":"m","stacktrace":0}"#, + r#"{\"type\":\"error\",\"id\":null,\"error\":\"x\",\"message\":\"m\",\"stacktrace\":0}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "stacktrace", }, ), ( - r#"{"type":"event","params":{}}"#, + r#"{\"type\":\"event\",\"params\":{}}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "method" }, ), ( - r#"{"type":"event","method":false,"params":{}}"#, + r#"{\"type\":\"event\",\"method\":false,\"params\":{}}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "method" }, ), ( - r#"{"type":"event","method":"x"}"#, + r#"{\"type\":\"event\",\"method\":\"x\"}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "params" }, ), ( - r#"{"type":"event","method":"x","params":null}"#, + r#"{\"type\":\"event\",\"method\":\"x\",\"params\":null}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "params" }, ), ]; @@ -870,27 +879,27 @@ mod tests { let cases = [ "[]", "null", - r#"{"type":"success","type":"event","id":1,"result":{}}"#, - r#"{"type":"success","id":1,"result":{}} trailing"#, - r#"{"type":"success","id":01,"result":{}}"#, - r#"{"type":"success","id":1.,"result":{}}"#, - r#"{"type":"success","id":1e,"result":{}}"#, - r#"{"type":"success","id":1e+,"result":{}}"#, - r#"{"type":"success","id":-,"result":{}}"#, - r#"{"type":"success","id":18446744073709551616,"result":{}}"#, + r#"{\"type\":\"success\",\"type\":\"event\",\"id\":1,\"result\":{}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{}} trailing"#, + r#"{\"type\":\"success\",\"id\":01,\"result\":{}}"#, + r#"{\"type\":\"success\",\"id\":1.,\"result\":{}}"#, + r#"{\"type\":\"success\",\"id\":1e,\"result\":{}}"#, + r#"{\"type\":\"success\",\"id\":1e+,\"result\":{}}"#, + r#"{\"type\":\"success\",\"id\":-,\"result\":{}}"#, + r#"{\"type\":\"success\",\"id\":18446744073709551616,\"result\":{}}"#, "{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"line\nbreak\"}}", - r#"{"type":"success","id":1,"result":{"bad":"\x"}}"#, - r#"{"type":"success","id":1,"result":{"bad":"\u12xz"}}"#, - r#"{"type":"success","id":1,"result":{"bad":"\ud800x"}}"#, - r#"{"type":"success","id":1,"result":{"bad":"\ud800\u0041"}}"#, - r#"{"type":"success","id":1,"result":{"bad":"\udc00"}}"#, - r#"{"type":"success","id":1,"result":{"a":true "b":false}}"#, - r#"{"type":"success","id":1,"result":[1,]}"#, - r#"{"type":"success","id":1,"result":{"a":tru}}"#, - r#"{"type":"success","id":1,"result":{"a":fal}}"#, - r#"{"type":"success","id":1,"result":{"a":nul}}"#, - r#"{"type":"success","id":1,"result":{"a":}}"#, - r#"{"type":"success","id":1,"result":{"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\x\"}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\u12xz\"}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\ud800x\"}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\ud800\\u0041\"}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\udc00\"}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":true \"b\":false}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":[1,]}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":tru}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":fal}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":nul}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":}}"#, + r#"{\"type\":\"success\",\"id\":1,\"result\":{"#, ]; for document in cases { assert!(parse(document).is_err(), "unexpectedly admitted {document}"); @@ -900,14 +909,14 @@ mod tests { Err(WebDriverBiDiJsonEnvelopeError::RootMustBeObject) ); assert_eq!( - parse(r#"{"type":"success","type":"event","id":1,"result":{}}"#), + parse(r#"{\"type\":\"success\",\"type\":\"event\",\"id\":1,\"result\":{}}"#), Err(WebDriverBiDiJsonEnvelopeError::DuplicateTopLevelMember) ); } #[test] fn rejects_excessive_json_nesting_and_formats_errors_without_payloads() { - let mut document = String::from(r#"{"type":"success","id":1,"result":"#); + let mut document = String::from(r#"{\"type\":\"success\",\"id\":1,\"result\":"#); for _ in 0..=MAX_WEBDRIVER_BIDI_JSON_DEPTH { document.push('['); } From c6ab0c597b625c5dcb36f63ed9e82c53176d0492 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:37:15 +0900 Subject: [PATCH 47/57] fix(network): restore JSON envelope test fixtures --- .../src/webdriver_bidi_json_envelope.rs | 105 ++++++++---------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index 3dcc1a693..048f03117 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -269,9 +269,6 @@ impl TopLevelFields { fn into_event(self) -> Result { let method = required_text(self.method, "method")?; require_object(self.params, "params")?; - if !is_webdriver_bidi_event_name(&method) { - return Err(invalid("method")); - } Ok(WebDriverBiDiJsonEnvelope { kind: WebDriverBiDiJsonEnvelopeKind::Event, command_id: None, @@ -293,13 +290,6 @@ fn is_webdriver_bidi_error_code(value: &str) -> bool { WEBDRIVER_BIDI_ERROR_CODES.contains(&value) } -fn is_webdriver_bidi_event_name(value: &str) -> bool { - match value.split_once('.') { - Some((module_name, event_name)) => !module_name.is_empty() && !event_name.is_empty(), - None => false, - } -} - fn required_text( value: Option, member: &'static str, @@ -684,7 +674,7 @@ mod tests { #[test] fn classifies_all_local_end_envelope_kinds_and_redacts_debug() { let success = parse( - r#"{\"type\":\"success\",\"id\":9007199254740991,\"result\":{\"ready\":true},\"ext\":[null,false,1.5,-2e3,\"\\u20ac\",\"\\ud83d\\ude00\"]}"#, + r#"{"type":"success","id":9007199254740991,"result":{"ready":true},"ext":[null,false,1.5,-2e3,"\u20ac","\ud83d\ude00"]}"#, ); assert_eq!( success.as_ref().map(WebDriverBiDiJsonEnvelope::kind), @@ -707,7 +697,7 @@ mod tests { assert!(!debug.contains("ready")); let error = parse( - r#"{\"type\":\"error\",\"id\":null,\"error\":\"invalid argument\",\"message\":\"secret detail\",\"stacktrace\":\"hidden\",\"vendor\":{\"x\":[]}}"#, + r#"{"type":"error","id":null,"error":"invalid argument","message":"secret detail","stacktrace":"hidden","vendor":{"x":[]}}"#, ); assert_eq!( error.as_ref().map(WebDriverBiDiJsonEnvelope::kind), @@ -730,7 +720,7 @@ mod tests { assert!(!debug.contains("secret detail")); let event = - parse(r#"{\"type\":\"event\",\"method\":\"browsingContext.load\",\"params\":{},\"vendor\":true}"#); + parse(r#"{"type":"event","method":"browsingContext.load","params":{},"vendor":true}"#); assert_eq!( event.as_ref().map(WebDriverBiDiJsonEnvelope::kind), Ok(WebDriverBiDiJsonEnvelopeKind::Event) @@ -752,8 +742,9 @@ mod tests { #[test] fn accepts_current_protocol_error_code_vocabulary() { for error_code in WEBDRIVER_BIDI_ERROR_CODES { - let document = - format!(r#"{{\"type\":\"error\",\"id\":7,\"error\":\"{error_code}\",\"message\":\"\"}}"#); + let document = format!( + r#"{{"type":"error","id":7,"error":"{error_code}","message":""}}"# + ); assert_eq!( parse(&document) .as_ref() @@ -764,7 +755,7 @@ mod tests { } assert_eq!( parse( - r#"{\"type\":\"error\",\"id\":7,\"error\":\"attacker-defined-code\",\"message\":\"bad code\"}"# + r#"{"type":"error","id":7,"error":"attacker-defined-code","message":"bad code"}"# ), Err(WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "error" }) ); @@ -772,7 +763,7 @@ mod tests { #[test] fn accepts_correlatable_error_and_extensible_success_members() { - let error = parse(r#"{\"type\":\"error\",\"id\":7,\"error\":\"unknown error\",\"message\":\"\",\"x\":0}"#); + let error = parse(r#"{"type":"error","id":7,"error":"unknown error","message":"","x":0}"#); assert_eq!( error .as_ref() @@ -795,77 +786,77 @@ mod tests { WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "type" }, ), ( - r#"{\"type\":1}"#, + r#"{"type":1}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "type" }, ), ( - r#"{\"type\":\"other\"}"#, + r#"{"type":"other"}"#, WebDriverBiDiJsonEnvelopeError::UnsupportedEnvelopeType, ), ( - r#"{\"type\":\"success\",\"result\":{}}"#, + r#"{"type":"success","result":{}}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "id" }, ), ( - r#"{\"type\":\"success\",\"id\":null,\"result\":{}}"#, + r#"{"type":"success","id":null,"result":{}}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, ), ( - r#"{\"type\":\"success\",\"id\":9007199254740992,\"result\":{}}"#, + r#"{"type":"success","id":9007199254740992,"result":{}}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, ), ( - r#"{\"type\":\"success\",\"id\":1}"#, + r#"{"type":"success","id":1}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "result" }, ), ( - r#"{\"type\":\"success\",\"id\":1,\"result\":[]}"#, + r#"{"type":"success","id":1,"result":[]}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "result" }, ), ( - r#"{\"type\":\"error\",\"error\":\"x\",\"message\":\"m\"}"#, + r#"{"type":"error","error":"x","message":"m"}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "id" }, ), ( - r#"{\"type\":\"error\",\"id\":-1,\"error\":\"x\",\"message\":\"m\"}"#, + r#"{"type":"error","id":-1,"error":"x","message":"m"}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "id" }, ), ( - r#"{\"type\":\"error\",\"id\":null,\"message\":\"m\"}"#, + r#"{"type":"error","id":null,"message":"m"}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, ), ( - r#"{\"type\":\"error\",\"id\":null,\"error\":false,\"message\":\"m\"}"#, + r#"{"type":"error","id":null,"error":false,"message":"m"}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "error" }, ), ( - r#"{\"type\":\"error\",\"id\":null,\"error\":\"x\"}"#, + r#"{"type":"error","id":null,"error":"x"}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "message" }, ), ( - r#"{\"type\":\"error\",\"id\":null,\"error\":\"x\",\"message\":{}}"#, + r#"{"type":"error","id":null,"error":"x","message":{}}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "message" }, ), ( - r#"{\"type\":\"error\",\"id\":null,\"error\":\"x\",\"message\":\"m\",\"stacktrace\":0}"#, + r#"{"type":"error","id":null,"error":"x","message":"m","stacktrace":0}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "stacktrace", }, ), ( - r#"{\"type\":\"event\",\"params\":{}}"#, + r#"{"type":"event","params":{}}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "method" }, ), ( - r#"{\"type\":\"event\",\"method\":false,\"params\":{}}"#, + r#"{"type":"event","method":false,"params":{}}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "method" }, ), ( - r#"{\"type\":\"event\",\"method\":\"x\"}"#, + r#"{"type":"event","method":"x"}"#, WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "params" }, ), ( - r#"{\"type\":\"event\",\"method\":\"x\",\"params\":null}"#, + r#"{"type":"event","method":"x","params":null}"#, WebDriverBiDiJsonEnvelopeError::InvalidMember { member: "params" }, ), ]; @@ -879,27 +870,27 @@ mod tests { let cases = [ "[]", "null", - r#"{\"type\":\"success\",\"type\":\"event\",\"id\":1,\"result\":{}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{}} trailing"#, - r#"{\"type\":\"success\",\"id\":01,\"result\":{}}"#, - r#"{\"type\":\"success\",\"id\":1.,\"result\":{}}"#, - r#"{\"type\":\"success\",\"id\":1e,\"result\":{}}"#, - r#"{\"type\":\"success\",\"id\":1e+,\"result\":{}}"#, - r#"{\"type\":\"success\",\"id\":-,\"result\":{}}"#, - r#"{\"type\":\"success\",\"id\":18446744073709551616,\"result\":{}}"#, + r#"{"type":"success","type":"event","id":1,"result":{}}"#, + r#"{"type":"success","id":1,"result":{}} trailing"#, + r#"{"type":"success","id":01,"result":{}}"#, + r#"{"type":"success","id":1.,"result":{}}"#, + r#"{"type":"success","id":1e,"result":{}}"#, + r#"{"type":"success","id":1e+,"result":{}}"#, + r#"{"type":"success","id":-,"result":{}}"#, + r#"{"type":"success","id":18446744073709551616,"result":{}}"#, "{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"line\nbreak\"}}", - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\x\"}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\u12xz\"}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\ud800x\"}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\ud800\\u0041\"}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"bad\":\"\\udc00\"}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":true \"b\":false}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":[1,]}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":tru}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":fal}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":nul}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{\"a\":}}"#, - r#"{\"type\":\"success\",\"id\":1,\"result\":{"#, + r#"{"type":"success","id":1,"result":{"bad":"\x"}}"#, + r#"{"type":"success","id":1,"result":{"bad":"\u12xz"}}"#, + r#"{"type":"success","id":1,"result":{"bad":"\ud800x"}}"#, + r#"{"type":"success","id":1,"result":{"bad":"\ud800\u0041"}}"#, + r#"{"type":"success","id":1,"result":{"bad":"\udc00"}}"#, + r#"{"type":"success","id":1,"result":{"a":true "b":false}}"#, + r#"{"type":"success","id":1,"result":[1,]}"#, + r#"{"type":"success","id":1,"result":{"a":tru}}"#, + r#"{"type":"success","id":1,"result":{"a":fal}}"#, + r#"{"type":"success","id":1,"result":{"a":nul}}"#, + r#"{"type":"success","id":1,"result":{"a":}}"#, + r#"{"type":"success","id":1,"result":{"#, ]; for document in cases { assert!(parse(document).is_err(), "unexpectedly admitted {document}"); @@ -909,14 +900,14 @@ mod tests { Err(WebDriverBiDiJsonEnvelopeError::RootMustBeObject) ); assert_eq!( - parse(r#"{\"type\":\"success\",\"type\":\"event\",\"id\":1,\"result\":{}}"#), + parse(r#"{"type":"success","type":"event","id":1,"result":{}}"#), Err(WebDriverBiDiJsonEnvelopeError::DuplicateTopLevelMember) ); } #[test] fn rejects_excessive_json_nesting_and_formats_errors_without_payloads() { - let mut document = String::from(r#"{\"type\":\"success\",\"id\":1,\"result\":"#); + let mut document = String::from(r#"{"type":"success","id":1,"result":"#); for _ in 0..=MAX_WEBDRIVER_BIDI_JSON_DEPTH { document.push('['); } From 4a9d7fa4cb8d6e31604d42a4a006a374f473d42a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:14:53 +0900 Subject: [PATCH 48/57] fix(network): reject malformed BiDi event names Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_json_envelope.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index 048f03117..f86d2317c 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -269,6 +269,12 @@ impl TopLevelFields { fn into_event(self) -> Result { let method = required_text(self.method, "method")?; require_object(self.params, "params")?; + if !matches!( + method.split_once('.'), + Some((module, event)) if !module.is_empty() && !event.is_empty() + ) { + return Err(invalid("method")); + } Ok(WebDriverBiDiJsonEnvelope { kind: WebDriverBiDiJsonEnvelopeKind::Event, command_id: None, @@ -742,9 +748,8 @@ mod tests { #[test] fn accepts_current_protocol_error_code_vocabulary() { for error_code in WEBDRIVER_BIDI_ERROR_CODES { - let document = format!( - r#"{{"type":"error","id":7,"error":"{error_code}","message":""}}"# - ); + let document = + format!(r#"{{"type":"error","id":7,"error":"{error_code}","message":""}}"#); assert_eq!( parse(&document) .as_ref() From 9b704036bfbbace93fd2cea6ffadec88d3908d42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:05:55 +0900 Subject: [PATCH 49/57] docs(doctoring): refresh WebDriver BiDi 2026-09-03 evidence --- docs/doctoring/browser-agent-protocols.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index da48f1055..37d0b88ac 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -1,6 +1,6 @@ # Browser and Agent Protocol Standards Evidence -- **Reviewed:** 2026-09-04 +- **Reviewed:** 2026-09-05 - **Purpose:** primary-source evidence for OriginWeave browser compatibility and adapter boundaries - **Canonical research index:** [`../doctoring.md`](../doctoring.md) @@ -8,7 +8,7 @@ This addendum complements the main doctoring record. The main record already car ## WebDriver BiDi -The latest published W3C technical-report baseline reviewed here is the 29 June 2026 **Working Draft**, not a Recommendation. The live Editor’s Draft retrieved on 4 September 2026 identifies itself as the 5 August 2026 draft and explicitly remains work in progress. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. +The latest published W3C technical-report baseline reviewed here is the 3 September 2026 **Working Draft**, not a Recommendation. The live Editor’s Draft retrieved on 5 September 2026 also identifies itself as the 3 September 2026 draft and explicitly remains work in progress. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. For the bounded `browsingContext.locateNodes` command-serialization boundary, the reviewed Editor’s Draft defines a command envelope with `id: js-uint`, defines `js-uint` as `0..9007199254740991`, and defines `browsingContext.locateNodes` parameters containing a browsing context, locator, optional positive `maxNodeCount`, optional `serializationOptions`, and optional `startNodes`. OriginWeave serializes only its separately reviewed accessibility-locator subset and fixed minimal serialization options; this deterministic JSON value is not transport authentication or browser/Agent authority. @@ -83,9 +83,9 @@ World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.o World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/2023/REC-wai-aria-1.2-20230606/ -World Wide Web Consortium. (2026, June 29). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260629/ +World Wide Web Consortium. (2026, September 3). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/ -World Wide Web Consortium. (2026, August 5). *WebDriver BiDi* (Editor’s Draft). Retrieved September 4, 2026, from https://w3c.github.io/webdriver-bidi/ +World Wide Web Consortium. (2026, September 3). *WebDriver BiDi* (Editor’s Draft). Retrieved September 5, 2026, from https://w3c.github.io/webdriver-bidi/ World Wide Web Consortium. (2026, August 5). *Accessible name and description computation 1.2* (W3C Working Draft). https://www.w3.org/TR/2026/WD-accname-1.2-20260805/ From 6cade8968c25a10a29bd9437e1622595fc37918d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:33:10 +0900 Subject: [PATCH 50/57] test(network): require spec-defined client-window error --- .../webdriver_bidi_error_code_interop.rs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_error_code_interop.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_error_code_interop.rs b/crates/originweave-network/tests/webdriver_bidi_error_code_interop.rs new file mode 100644 index 000000000..69b668b78 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_error_code_interop.rs @@ -0,0 +1,106 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn parse_over_real_transport( + payload: &[u8], +) -> Result, Box> { + if payload.len() > 125 { + return Err(io::Error::other("test payload exceeded one-byte frame length").into()); + } + + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let payload = payload.to_vec(); + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let payload_len = u8::try_from(payload.len()) + .map_err(|_| io::Error::other("test payload length does not fit u8"))?; + stream.write_all(&[0x81, payload_len])?; + stream.write_all(&payload) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint)?; + let correlated = admitted.correlate_session_id(SESSION_ID)?; + let target = correlated.into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let parsed = WebDriverBiDiJsonEnvelope::parse(&text); + + server + .join() + .map_err(|_| io::Error::other("JSON-envelope server panicked"))??; + Ok(parsed) +} + +#[test] +fn real_transport_accepts_spec_defined_client_window_error() -> Result<(), Box> { + let envelope = parse_over_real_transport( + br#"{"type":"error","id":7,"error":"no such client window","message":"unknown client window"}"#, + )?; + assert_eq!( + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Error) + ); + assert_eq!( + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), + Ok(Some(7)) + ); + assert_eq!( + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::error_code), + Ok(Some("no such client window")) + ); + Ok(()) +} From 56e70a2cd54e3ea4f85595ce7b5a00f05e0364bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:41:08 +0900 Subject: [PATCH 51/57] test(network): colocate client-window interop regression --- .../tests/webdriver_bidi_json_envelope.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs index 79ea85da2..41bb90524 100644 --- a/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/tests/webdriver_bidi_json_envelope.rs @@ -302,3 +302,23 @@ fn real_transport_rejects_non_protocol_error_code() -> Result<(), Box ); Ok(()) } + +#[test] +fn real_transport_accepts_spec_defined_client_window_error() -> Result<(), Box> { + let envelope = parse_over_real_transport( + br#"{"type":"error","id":7,"error":"no such client window","message":"unknown client window"}"#, + )?; + assert_eq!( + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::kind), + Ok(WebDriverBiDiJsonEnvelopeKind::Error) + ); + assert_eq!( + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), + Ok(Some(7)) + ); + assert_eq!( + envelope.as_ref().map(WebDriverBiDiJsonEnvelope::error_code), + Ok(Some("no such client window")) + ); + Ok(()) +} From 75780785d6a6a9cc3659103e0653880ae6222fd4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:41:22 +0900 Subject: [PATCH 52/57] test(network): remove duplicate interop harness --- .../webdriver_bidi_error_code_interop.rs | 106 ------------------ 1 file changed, 106 deletions(-) delete mode 100644 crates/originweave-network/tests/webdriver_bidi_error_code_interop.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_error_code_interop.rs b/crates/originweave-network/tests/webdriver_bidi_error_code_interop.rs deleted file mode 100644 index 69b668b78..000000000 --- a/crates/originweave-network/tests/webdriver_bidi_error_code_interop.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::{ - error::Error, - io::{self, Read, Write}, - net::{TcpListener, TcpStream}, - thread, - time::Duration, -}; - -use originweave_core::WebDriverBiDiWebSocketEndpoint; -use originweave_network::{ - WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, -}; - -const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; -const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; -const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; - -fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { - stream.set_read_timeout(Some(Duration::from_secs(2)))?; - let mut request = Vec::new(); - let mut buffer = [0_u8; 512]; - while !request.ends_with(b"\r\n\r\n") { - let count = stream.read(&mut buffer)?; - if count == 0 { - return Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "client opening request ended before the header terminator", - )); - } - request.extend_from_slice(&buffer[..count]); - } - Ok(()) -} - -fn parse_over_real_transport( - payload: &[u8], -) -> Result, Box> { - if payload.len() > 125 { - return Err(io::Error::other("test payload exceeded one-byte frame length").into()); - } - - let listener = TcpListener::bind(("127.0.0.1", 0))?; - let local_addr = listener.local_addr()?; - let payload = payload.to_vec(); - let server = thread::spawn(move || -> io::Result<()> { - let (mut stream, _) = listener.accept()?; - read_opening_request(&mut stream)?; - stream.write_all(OPENING_RESPONSE)?; - let payload_len = u8::try_from(payload.len()) - .map_err(|_| io::Error::other("test payload length does not fit u8"))?; - stream.write_all(&[0x81, payload_len])?; - stream.write_all(&payload) - }); - - let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); - let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint)?; - let correlated = admitted.correlate_session_id(SESSION_ID)?; - let target = correlated.into_explicit_connect_target()?; - let connection = - WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; - let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; - let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? - .write_opening_request(Duration::from_millis(500))? - .read_opening_response(Duration::from_millis(500))?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, - other => { - return Err(io::Error::other(format!( - "validated text frame produced unexpected assembly state: {other:?}" - )) - .into()); - } - }; - let parsed = WebDriverBiDiJsonEnvelope::parse(&text); - - server - .join() - .map_err(|_| io::Error::other("JSON-envelope server panicked"))??; - Ok(parsed) -} - -#[test] -fn real_transport_accepts_spec_defined_client_window_error() -> Result<(), Box> { - let envelope = parse_over_real_transport( - br#"{"type":"error","id":7,"error":"no such client window","message":"unknown client window"}"#, - )?; - assert_eq!( - envelope.as_ref().map(WebDriverBiDiJsonEnvelope::kind), - Ok(WebDriverBiDiJsonEnvelopeKind::Error) - ); - assert_eq!( - envelope.as_ref().map(WebDriverBiDiJsonEnvelope::command_id), - Ok(Some(7)) - ); - assert_eq!( - envelope.as_ref().map(WebDriverBiDiJsonEnvelope::error_code), - Ok(Some("no such client window")) - ); - Ok(()) -} From 01ef4296e099d7cae5dc27c5a896cd26443c720b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 07:46:22 +0900 Subject: [PATCH 53/57] fix(network): admit client-window BiDi errors Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + crates/originweave-network/src/webdriver_bidi_json_envelope.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f642a93..9689df7b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Aligned the bounded WebDriver BiDi error-envelope vocabulary with the current specification by admitting the defined `no such client window` response while retaining fail-closed rejection of unknown error codes. - Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. - Separated exact TCP peer proof from authenticated TLS service identity; an observed peer becomes an authenticated HTTPS stream only after explicit-root, fixed-time, SAN-bound WebPKI verification over that same stream. diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index f86d2317c..e8d8e01d1 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -12,13 +12,14 @@ pub const MAX_WEBDRIVER_BIDI_JSON_DEPTH: usize = 64; /// Largest integer admitted by WebDriver BiDi's `js-uint` production. pub const MAX_WEBDRIVER_BIDI_JS_UINT: u64 = 9_007_199_254_740_991; -const WEBDRIVER_BIDI_ERROR_CODES: [&str; 30] = [ +const WEBDRIVER_BIDI_ERROR_CODES: [&str; 31] = [ "invalid argument", "invalid selector", "invalid session id", "invalid web extension", "move target out of bounds", "no such alert", + "no such client window", "no such network collector", "no such element", "no such frame", From 3df49e60c27b423c9218ed772d34db61fd5f5305 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 07:47:17 +0900 Subject: [PATCH 54/57] docs(network): align BiDi error vocabulary evidence Signed-off-by: Seongho Bae --- docs/doctoring/browser-agent-protocols.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index 37d0b88ac..e206c72ac 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -12,9 +12,9 @@ The latest published W3C technical-report baseline reviewed here is the 3 Septem For the bounded `browsingContext.locateNodes` command-serialization boundary, the reviewed Editor’s Draft defines a command envelope with `id: js-uint`, defines `js-uint` as `0..9007199254740991`, and defines `browsingContext.locateNodes` parameters containing a browsing context, locator, optional positive `maxNodeCount`, optional `serializationOptions`, and optional `startNodes`. OriginWeave serializes only its separately reviewed accessibility-locator subset and fixed minimal serialization options; this deterministic JSON value is not transport authentication or browser/Agent authority. -WebDriver BiDi commands may execute concurrently and finish out of order. The Editor’s Draft defines the command id as the local end’s correlation identifier and sets a successful `CommandResponse.id` to that exact command id; an `ErrorResponse.id` may be `null` when no valid command id can be recovered. OriginWeave therefore fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. +WebDriver BiDi commands may execute concurrently and finish out of order. The 3 September 2026 Working Draft defines the command id as the local end’s correlation identifier; its local-end `CommandResponse` production requires `id: js-uint`, while `ErrorResponse.id` is `js-uint / null`. OriginWeave therefore represents a validated success response with a structurally present command id, retains nullable ids only for protocol errors, and fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. -The same reviewed Editor’s Draft defines `ErrorResponse.error` as `ErrorCode` and its local-end CDDL enumerates a finite 30-value vocabulary from `invalid argument` through `unsupported operation`. The CDDL does **not** include `no such client window`; the specification may define additional error concepts elsewhere, but their presence outside the `ErrorCode` production is not authority to admit them in a local-end error response. OriginWeave therefore admits only the reviewed CDDL vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed. Adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. +The same Working Draft defines `ErrorResponse.error` as `ErrorCode` and its local-end CDDL enumerates a finite 31-value vocabulary from `invalid argument` through `unsupported operation`, including `no such client window`. OriginWeave admits only that reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed. Adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. Primary sources: World Wide Web Consortium, *WebDriver BiDi* (published Working Draft and current Editor’s Draft). @@ -89,4 +89,4 @@ World Wide Web Consortium. (2026, September 3). *WebDriver BiDi* (Editor’s Dra World Wide Web Consortium. (2026, August 5). *Accessible name and description computation 1.2* (W3C Working Draft). https://www.w3.org/TR/2026/WD-accname-1.2-20260805/ -International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html \ No newline at end of file +International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html From 7a4594317cccbd08678e3f3fbc7d62b905851109 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:10:06 +0900 Subject: [PATCH 55/57] docs(network): record BiDi ErrorCode inconsistency accurately --- docs/doctoring/browser-agent-protocols.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index e206c72ac..0795642a4 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -14,7 +14,7 @@ For the bounded `browsingContext.locateNodes` command-serialization boundary, th WebDriver BiDi commands may execute concurrently and finish out of order. The 3 September 2026 Working Draft defines the command id as the local end’s correlation identifier; its local-end `CommandResponse` production requires `id: js-uint`, while `ErrorResponse.id` is `js-uint / null`. OriginWeave therefore represents a validated success response with a structurally present command id, retains nullable ids only for protocol errors, and fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. -The same Working Draft defines `ErrorResponse.error` as `ErrorCode` and its local-end CDDL enumerates a finite 31-value vocabulary from `invalid argument` through `unsupported operation`, including `no such client window`. OriginWeave admits only that reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed. Adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. +The same Working Draft defines `ErrorResponse.error` as `ErrorCode`. Its rendered local-end CDDL enumerates 30 values and omits `no such client window`, while §3.5 separately defines `no such client window` and normative client-window algorithms return that error code. OriginWeave therefore admits the finite rendered CDDL vocabulary plus this one separately defined normative error, and still rejects arbitrary error-code text fail closed. This is an explicit interoperability exception for a specification-internal inconsistency, not authority to infer or accept other strings; adding any further code requires fresh primary-source review and regression evidence. Primary sources: World Wide Web Consortium, *WebDriver BiDi* (published Working Draft and current Editor’s Draft). From 9fa4f37f1bbe6b835c157633cdebd8c3c2d3e799 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:12:11 +0900 Subject: [PATCH 56/57] docs(changelog): preserve BiDi error-code discrepancy --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9689df7b0..9a137ee95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,7 +69,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security -- WebDriver BiDi local-end error envelopes now admit only the finite `ErrorCode` vocabulary reviewed from the current W3C CDDL; arbitrary error strings fail closed before becoming typed response metadata, and realistic loopback coverage exercises the hostile unknown-code path. +- WebDriver BiDi local-end error envelopes now admit the 30-value rendered W3C `ErrorCode` CDDL plus the separately defined normative `no such client window` error; arbitrary strings still fail closed, and realistic loopback coverage exercises both that compatibility exception and the hostile unknown-code path. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. - Crawler mode is read-only, must pair with the public-crawl purpose, and fails closed without an applicable robots-policy decision. From 6407895f4db4bee640074cb9c9d3cbe8b0e9e13a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:14:48 +0900 Subject: [PATCH 57/57] test(docs): lock BiDi error-vocabulary evidence --- ...bdriver_bidi_error_vocabulary_doctoring.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/test_webdriver_bidi_error_vocabulary_doctoring.py diff --git a/tests/test_webdriver_bidi_error_vocabulary_doctoring.py b/tests/test_webdriver_bidi_error_vocabulary_doctoring.py new file mode 100644 index 000000000..7c35ff14a --- /dev/null +++ b/tests/test_webdriver_bidi_error_vocabulary_doctoring.py @@ -0,0 +1,46 @@ +"""Lock the reviewed WebDriver BiDi error-vocabulary evidence to the shipped adapter contract.""" + +from __future__ import annotations + +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +DOCTORING = ROOT / "docs" / "doctoring" / "browser-agent-protocols.md" +CHANGELOG = ROOT / "CHANGELOG.md" + + +class WebDriverBiDiErrorVocabularyDoctoringTests(unittest.TestCase): + """Prevent the W3C prose/CDDL discrepancy from being rewritten as false conformance evidence.""" + + def test_doctoring_records_the_reviewed_cddl_exception(self) -> None: + """The adapter's 30+1 admission rule must stay explicit and fail-closed.""" + + doctoring = DOCTORING.read_text(encoding="utf-8") + self.assertIn( + "rendered local-end CDDL enumerates 30 values and omits `no such client window`", + doctoring, + ) + self.assertIn( + "§3.5 separately defines `no such client window` and normative client-window algorithms return that error code", + doctoring, + ) + self.assertIn( + "explicit interoperability exception for a specification-internal inconsistency", + doctoring, + ) + + def test_changelog_does_not_misstate_the_exception_as_cddl_membership(self) -> None: + """Release evidence must distinguish rendered CDDL values from the normative exception.""" + + changelog = CHANGELOG.read_text(encoding="utf-8") + self.assertIn( + "30-value rendered W3C `ErrorCode` CDDL plus the separately defined normative `no such client window` error", + changelog, + ) + self.assertIn("arbitrary strings still fail closed", changelog) + + +if __name__ == "__main__": + unittest.main()