cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

main
goq / crates / sigil-protocol / src / input_v2.rs
input_v2.rs 10.9K
1use serde::{Deserialize, Deserializer, Serialize, Serializer};2use tokio::io::{AsyncRead, AsyncWrite};34use crate::framing::{read_json, write_json};5use crate::{6 Capability, InputAck, InputEvent, MAX_INPUT_MESSAGE_LEN, PROTOCOL_VERSION_V2, ProtocolError,7 Result,8};910const MAX_AGENT_LEN: usize = 128;11const MAX_MESSAGE_LEN: usize = 512;12const MAX_CAPABILITIES: usize = 16;1314#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]15pub struct ControllerSlot(u8);1617impl ControllerSlot {18 pub const ZERO: Self = Self(0);1920 pub fn new(value: u8) -> Result<Self> {21 if value != 0 {22 return Err(ProtocolError::InvalidMessage {23 message_type: "controller slot",24 reason: "only controller slot 0 is supported",25 });26 }27 Ok(Self(value))28 }2930 pub const fn get(self) -> u8 {31 self.032 }3334 pub fn validate(self) -> Result<()> {35 Self::new(self.0).map(|_| ())36 }37}3839impl Serialize for ControllerSlot {40 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>41 where42 S: Serializer,43 {44 serializer.serialize_u8(self.0)45 }46}4748impl<'de> Deserialize<'de> for ControllerSlot {49 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>50 where51 D: Deserializer<'de>,52 {53 let value = u8::deserialize(deserializer)?;54 Self::new(value).map_err(serde::de::Error::custom)55 }56}5758#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]59#[serde(deny_unknown_fields)]60pub struct InputClientHelloV2 {61 pub version: u16,62 pub agent: String,63 pub nonce: [u8; 16],64 pub session_id: u64,65 pub slot: ControllerSlot,66 pub focus_generation: u64,67 pub capabilities: Vec<Capability>,68}6970impl InputClientHelloV2 {71 pub fn new(72 agent: impl Into<String>,73 nonce: [u8; 16],74 session_id: u64,75 slot: ControllerSlot,76 focus_generation: u64,77 capabilities: Vec<Capability>,78 ) -> Self {79 Self {80 version: PROTOCOL_VERSION_V2,81 agent: agent.into(),82 nonce,83 session_id,84 slot,85 focus_generation,86 capabilities,87 }88 }8990 pub fn validate(&self) -> Result<()> {91 validate_version(self.version)?;92 if self.agent.is_empty() || self.agent.len() > MAX_AGENT_LEN {93 return invalid(94 "input client hello v2",95 "agent must contain 1..=128 UTF-8 bytes",96 );97 }98 validate_binding(self.session_id, self.slot, self.focus_generation)?;99 validate_capabilities(&self.capabilities)100 }101}102103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]104#[serde(deny_unknown_fields)]105pub struct InputHostHelloV2 {106 pub version: u16,107 pub accepted: bool,108 pub session_id: Option<u64>,109 pub slot: Option<ControllerSlot>,110 pub focus_generation: Option<u64>,111 pub capabilities: Vec<Capability>,112 pub message: Option<String>,113}114115impl InputHostHelloV2 {116 pub fn accepted(117 session_id: u64,118 slot: ControllerSlot,119 focus_generation: u64,120 capabilities: Vec<Capability>,121 ) -> Self {122 Self {123 version: PROTOCOL_VERSION_V2,124 accepted: true,125 session_id: Some(session_id),126 slot: Some(slot),127 focus_generation: Some(focus_generation),128 capabilities,129 message: None,130 }131 }132133 pub fn rejected(message: impl Into<String>) -> Self {134 Self {135 version: PROTOCOL_VERSION_V2,136 accepted: false,137 session_id: None,138 slot: None,139 focus_generation: None,140 capabilities: Vec::new(),141 message: Some(message.into()),142 }143 }144145 pub fn validate(&self) -> Result<()> {146 validate_version(self.version)?;147 validate_capabilities(&self.capabilities)?;148 if self.accepted {149 let session_id = self.session_id.unwrap_or(0);150 let focus_generation = self.focus_generation.unwrap_or(0);151 let slot = self.slot.ok_or(ProtocolError::InvalidMessage {152 message_type: "input host hello v2",153 reason: "accepted responses require a controller slot",154 })?;155 validate_binding(session_id, slot, focus_generation)?;156 if self.message.is_some() {157 return invalid("input host hello v2", "accepted responses forbid a message");158 }159 } else if self.session_id.is_some()160 || self.slot.is_some()161 || self.focus_generation.is_some()162 || self163 .message164 .as_ref()165 .is_none_or(|message| message.is_empty() || message.len() > MAX_MESSAGE_LEN)166 {167 return invalid(168 "input host hello v2",169 "rejections require a bounded message and forbid focus binding",170 );171 }172 Ok(())173 }174}175176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]177#[serde(deny_unknown_fields)]178pub struct InputEventV2 {179 pub session_id: u64,180 pub slot: ControllerSlot,181 pub focus_generation: u64,182 pub event: InputEvent,183}184185impl InputEventV2 {186 pub fn validate(&self) -> Result<()> {187 validate_binding(self.session_id, self.slot, self.focus_generation)?;188 self.event.validate()189 }190}191192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]193#[serde(deny_unknown_fields)]194pub struct InputAckV2 {195 pub session_id: u64,196 pub slot: ControllerSlot,197 pub focus_generation: u64,198 pub ack: InputAck,199}200201impl InputAckV2 {202 pub fn validate(&self) -> Result<()> {203 validate_binding(self.session_id, self.slot, self.focus_generation)?;204 self.ack.validate()205 }206}207208macro_rules! json_io {209 ($read:ident, $write:ident, $ty:ty) => {210 pub async fn $read<R>(reader: &mut R) -> Result<Option<$ty>>211 where212 R: AsyncRead + Unpin,213 {214 let value: Option<$ty> = read_json(reader, MAX_INPUT_MESSAGE_LEN).await?;215 if let Some(value) = &value {216 value.validate()?;217 }218 Ok(value)219 }220221 pub async fn $write<W>(writer: &mut W, value: &$ty) -> Result<()>222 where223 W: AsyncWrite + Unpin,224 {225 value.validate()?;226 write_json(writer, value, MAX_INPUT_MESSAGE_LEN).await227 }228 };229}230231json_io!(232 read_input_client_hello_v2,233 write_input_client_hello_v2,234 InputClientHelloV2235);236json_io!(237 read_input_host_hello_v2,238 write_input_host_hello_v2,239 InputHostHelloV2240);241json_io!(read_input_event_v2, write_input_event_v2, InputEventV2);242json_io!(read_input_ack_v2, write_input_ack_v2, InputAckV2);243244fn validate_binding(session_id: u64, slot: ControllerSlot, focus_generation: u64) -> Result<()> {245 if session_id == 0 || focus_generation == 0 {246 return invalid(247 "input v2 focus binding",248 "session and focus generations must be non-zero",249 );250 }251 slot.validate()252}253254fn validate_version(version: u16) -> Result<()> {255 if version != PROTOCOL_VERSION_V2 {256 return Err(ProtocolError::UnsupportedVersion {257 expected: PROTOCOL_VERSION_V2,258 actual: version,259 });260 }261 Ok(())262}263264fn validate_capabilities(capabilities: &[Capability]) -> Result<()> {265 if capabilities.len() > MAX_CAPABILITIES {266 return invalid("input hello v2", "too many capabilities");267 }268 for (index, capability) in capabilities.iter().enumerate() {269 if capabilities[..index].contains(capability) {270 return invalid("input hello v2", "duplicate capability");271 }272 if matches!(capability, Capability::VideoH264 | Capability::AudioOpus) {273 return invalid("input hello v2", "media capability is invalid on input v2");274 }275 }276 Ok(())277}278279fn invalid<T>(message_type: &'static str, reason: &'static str) -> Result<T> {280 Err(ProtocolError::InvalidMessage {281 message_type,282 reason,283 })284}285286#[cfg(test)]287mod tests {288 use tokio::io::{AsyncWriteExt, duplex};289290 use super::*;291 use crate::GamepadState;292293 #[test]294 fn controller_slot_accepts_only_slot_zero() {295 assert_eq!(ControllerSlot::new(0).unwrap(), ControllerSlot::ZERO);296 assert!(ControllerSlot::new(1).is_err());297 assert!(serde_json::from_str::<ControllerSlot>("1").is_err());298 }299300 #[test]301 fn input_v2_gamepad_json_carries_slot_and_focus_generation() {302 let event = InputEventV2 {303 session_id: 7,304 slot: ControllerSlot::ZERO,305 focus_generation: 9,306 event: InputEvent::Gamepad {307 state: GamepadState::default(),308 },309 };310 let json = serde_json::to_string(&event).unwrap();311 assert!(312 json.starts_with(r#"{"session_id":7,"slot":0,"focus_generation":9,"event":{"t":"gp""#)313 );314 event.validate().unwrap();315 }316317 #[tokio::test]318 async fn input_v2_hello_event_and_ack_round_trip() {319 let hello = InputClientHelloV2::new(320 "portal/0.1.0",321 [3; 16],322 7,323 ControllerSlot::ZERO,324 9,325 vec![Capability::Gamepad, Capability::InputAck],326 );327 let event = InputEventV2 {328 session_id: 7,329 slot: ControllerSlot::ZERO,330 focus_generation: 9,331 event: InputEvent::Probe,332 };333 let ack = InputAckV2 {334 session_id: 7,335 slot: ControllerSlot::ZERO,336 focus_generation: 9,337 ack: InputAck {338 sequence: 1,339 pointer_position: None,340 pointer_visible: None,341 },342 };343 let (mut sender, mut receiver) = duplex(2048);344 write_input_client_hello_v2(&mut sender, &hello)345 .await346 .unwrap();347 write_input_event_v2(&mut sender, &event).await.unwrap();348 write_input_ack_v2(&mut sender, &ack).await.unwrap();349 sender.shutdown().await.unwrap();350 assert_eq!(351 read_input_client_hello_v2(&mut receiver).await.unwrap(),352 Some(hello)353 );354 assert_eq!(355 read_input_event_v2(&mut receiver).await.unwrap(),356 Some(event)357 );358 assert_eq!(read_input_ack_v2(&mut receiver).await.unwrap(), Some(ack));359 }360361 #[test]362 fn input_v2_rejects_zero_generations_unknown_fields_and_media_capabilities() {363 let mut hello =364 InputClientHelloV2::new("portal", [0; 16], 0, ControllerSlot::ZERO, 1, vec![]);365 assert!(hello.validate().is_err());366 hello.session_id = 1;367 hello.capabilities = vec![Capability::VideoH264];368 assert!(hello.validate().is_err());369 assert!(370 serde_json::from_str::<InputEventV2>(371 r#"{"session_id":1,"slot":0,"focus_generation":1,"event":{"t":"ip"},"extra":true}"#372 )373 .is_err()374 );375 }376}