//! Fire2 framing. //! //! ```text //! [0:4] u32 payload length //! [4:6] u16 metadata length //! [6:8] u16 component //! [8:10] u16 command (notification id when msgType == NOTIFICATION) //! [10:13] u24 msgNum //! [13] u8 (msgType << 5) | (userIndex & 0x1F) //! [14] u8 options //! [15] u8 reserved //! //! wire = header(16) || metadata || payload //! ``` //! //! All fields big-endian. Sixteen bytes, not twelve. //! //! # Two wrong layouts this replaces //! //! This is the layout in `blaze_responder_v3b.py::fire2` — the code that //! actually drove a retail FIFA 17 client through login. Two other //! implementations in this repository disagree and are both wrong for FIFA 17: //! //! * `fifa17-recon/tools/heat2.py::build_fire2_frame` packs //! `>IHHHHB3s`, which puts a `u16 msgId` at `[10:12]` and `msgType` at //! `[12]`. Its own module docstring still describes that layout. The //! responder carries a comment telling callers not to use it. //! * `fifa-blaze/crates/blaze-proto/src/frame.rs` implements a *12-byte* //! header with a `u16` length, nibble-packed type/options, and a JUMBO //! flag. That was a documented guess at FIFA 23's variant, written before //! the FIFA 17 recon; nothing has since validated it. //! //! There is **no error field** in a Fire2 header — that belongs to Fire v1's //! 12-byte frame. There is also no jumbo-frame escape: the length is already a //! full `u32`. use crate::error::{Error, Result}; /// Header size in bytes. pub const HEADER_LEN: usize = 16; /// `msgType`, the top three bits of byte 13. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MsgType { Message, Reply, Notification, ErrorReply, Ping, PingReply, /// 6 and 7 are representable in three bits but have never been observed. Unknown(u8), } impl MsgType { pub fn from_bits(bits: u8) -> MsgType { match bits & 0x07 { 0 => MsgType::Message, 1 => MsgType::Reply, 2 => MsgType::Notification, 3 => MsgType::ErrorReply, 4 => MsgType::Ping, 5 => MsgType::PingReply, other => MsgType::Unknown(other), } } pub fn as_bits(self) -> u8 { match self { MsgType::Message => 0, MsgType::Reply => 1, MsgType::Notification => 2, MsgType::ErrorReply => 3, MsgType::Ping => 4, MsgType::PingReply => 5, MsgType::Unknown(v) => v & 0x07, } } pub fn name(self) -> &'static str { match self { MsgType::Message => "MESSAGE", MsgType::Reply => "REPLY", MsgType::Notification => "NOTIFICATION", MsgType::ErrorReply => "ERROR_REPLY", MsgType::Ping => "PING", MsgType::PingReply => "PING_REPLY", MsgType::Unknown(_) => "UNKNOWN", } } } /// A parsed Fire2 header. /// /// `component`/`command` are plain integers on purpose. Naming them is a /// game-specific concern: a FIFA 17 adapter owns the tables that turn /// `0x0009/0x0007` into `Util::preAuth`, and a future FIFA 18 or FIFA 23 /// adapter may map the same numbers differently. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Header { pub payload_len: u32, pub metadata_len: u16, pub component: u16, pub command: u16, /// 24-bit request correlator. pub msg_num: u32, pub msg_type: MsgType, /// 5-bit local user slot. pub user_index: u8, pub options: u8, pub reserved: u8, } impl Header { pub fn new(component: u16, command: u16, msg_num: u32, msg_type: MsgType) -> Header { Header { payload_len: 0, metadata_len: 0, component, command, msg_num, msg_type, user_index: 0, options: 0, reserved: 0, } } /// Total wire size of this frame: header + metadata + payload. pub fn frame_len(&self) -> usize { HEADER_LEN + self.metadata_len as usize + self.payload_len as usize } /// Parse a header from the first 16 bytes of `buf`. pub fn parse(buf: &[u8]) -> Result
{ if buf.len() < HEADER_LEN { return Err(Error::ShortFrame { need: HEADER_LEN, have: buf.len(), }); } Ok(Header { payload_len: u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]), metadata_len: u16::from_be_bytes([buf[4], buf[5]]), component: u16::from_be_bytes([buf[6], buf[7]]), command: u16::from_be_bytes([buf[8], buf[9]]), msg_num: ((buf[10] as u32) << 16) | ((buf[11] as u32) << 8) | buf[12] as u32, msg_type: MsgType::from_bits(buf[13] >> 5), user_index: buf[13] & 0x1F, options: buf[14], reserved: buf[15], }) } /// Serialise the 16 header bytes. pub fn write(&self, out: &mut Vec) { out.extend_from_slice(&self.payload_len.to_be_bytes()); out.extend_from_slice(&self.metadata_len.to_be_bytes()); out.extend_from_slice(&self.component.to_be_bytes()); out.extend_from_slice(&self.command.to_be_bytes()); out.push(((self.msg_num >> 16) & 0xFF) as u8); out.push(((self.msg_num >> 8) & 0xFF) as u8); out.push((self.msg_num & 0xFF) as u8); out.push((self.msg_type.as_bits() << 5) | (self.user_index & 0x1F)); out.push(self.options); out.push(self.reserved); } pub fn to_bytes(&self) -> [u8; HEADER_LEN] { let mut v = Vec::with_capacity(HEADER_LEN); self.write(&mut v); let mut out = [0u8; HEADER_LEN]; out.copy_from_slice(&v); out } } /// A complete Fire2 frame: header plus its metadata and payload bytes. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Frame { pub header: Header, pub metadata: Vec, /// Raw payload bytes. Undecoded on purpose — the payload is Heat2/TDF, but /// framing must stay usable for capture and diagnostics even when the body /// does not parse. pub payload: Vec, } impl Frame { /// Build a frame, deriving the length fields from the buffers. pub fn new( component: u16, command: u16, msg_num: u32, msg_type: MsgType, payload: Vec, ) -> Frame { let mut header = Header::new(component, command, msg_num, msg_type); header.payload_len = payload.len() as u32; Frame { header, metadata: Vec::new(), payload, } } pub fn with_metadata(mut self, metadata: Vec) -> Frame { self.header.metadata_len = metadata.len() as u16; self.metadata = metadata; self } pub fn with_user_index(mut self, user_index: u8) -> Frame { self.header.user_index = user_index & 0x1F; self } pub fn with_options(mut self, options: u8) -> Frame { self.header.options = options; self } /// A reply echoes component, command, msgNum and userIndex verbatim, and /// changes only the msgType bits. pub fn reply_to(request: &Header, payload: Vec) -> Frame { let mut frame = Frame::new( request.component, request.command, request.msg_num, MsgType::Reply, payload, ); frame.header.user_index = request.user_index; frame } /// An unsolicited server push. `msgNum` is 0: notifications are not /// correlated to any request. pub fn notification(component: u16, notify_id: u16, payload: Vec) -> Frame { Frame::new(component, notify_id, 0, MsgType::Notification, payload) } pub fn encode(&self) -> Vec { let mut out = Vec::with_capacity(self.wire_len()); self.encode_into(&mut out); out } pub fn encode_into(&self, out: &mut Vec) { // Trust the buffers over any stale length in the header. let mut header = self.header; header.payload_len = self.payload.len() as u32; header.metadata_len = self.metadata.len() as u16; header.write(out); out.extend_from_slice(&self.metadata); out.extend_from_slice(&self.payload); } pub fn wire_len(&self) -> usize { HEADER_LEN + self.metadata.len() + self.payload.len() } /// Parse one frame from the front of `buf`, returning it and the number of /// bytes consumed. pub fn parse(buf: &[u8]) -> Result<(Frame, usize)> { let header = Header::parse(buf)?; let total = header.frame_len(); if buf.len() < total { return Err(Error::ShortFrame { need: total, have: buf.len(), }); } let meta_end = HEADER_LEN + header.metadata_len as usize; Ok(( Frame { header, metadata: buf[HEADER_LEN..meta_end].to_vec(), payload: buf[meta_end..total].to_vec(), }, total, )) } } /// How many bytes a frame starting at `buf` needs in total, if its header is /// complete. `None` means the header itself has not arrived yet. /// /// This is what a stream reader needs: Blaze frames arrive coalesced and split /// across TCP segments, so a reader must size each frame before consuming it. pub fn frame_size_hint(buf: &[u8]) -> Option { Header::parse(buf).ok().map(|h| h.frame_len()) } /// Split a buffer into as many whole frames as it contains. /// /// Returns the frames plus the number of bytes consumed; any trailing partial /// frame is left for the caller to retry once more bytes arrive. pub fn parse_all(buf: &[u8]) -> Result<(Vec, usize)> { let mut frames = Vec::new(); let mut off = 0; while off < buf.len() { match Frame::parse(&buf[off..]) { Ok((frame, used)) => { off += used; frames.push(frame); } Err(Error::ShortFrame { .. }) => break, Err(e) => return Err(e), } } Ok((frames, off)) } #[cfg(test)] mod tests { use super::*; #[test] fn header_is_sixteen_bytes() { let h = Header::new(0x0009, 0x0007, 1, MsgType::Reply); assert_eq!(h.to_bytes().len(), 16); } #[test] fn field_offsets_match_the_proven_layout() { let mut h = Header::new(0x0009, 0x0007, 0x123456, MsgType::Reply); h.payload_len = 0x11223344; h.metadata_len = 0x5566; h.user_index = 3; h.options = 0x42; let b = h.to_bytes(); assert_eq!(&b[0..4], &[0x11, 0x22, 0x33, 0x44]); // payload len u32 assert_eq!(&b[4..6], &[0x55, 0x66]); // metadata len u16 assert_eq!(&b[6..8], &[0x00, 0x09]); // component assert_eq!(&b[8..10], &[0x00, 0x07]); // command assert_eq!(&b[10..13], &[0x12, 0x34, 0x56]); // msgNum u24 assert_eq!(b[13], (1 << 5) | 3); // msgType | userIndex assert_eq!(b[14], 0x42); assert_eq!(b[15], 0x00); } #[test] fn reply_bit_pattern_is_0x20_and_notification_is_0x40() { let reply = Frame::new(1, 0x0A, 7, MsgType::Reply, vec![]).encode(); assert_eq!(reply[13], 0x20); let notify = Frame::notification(0x7802, 0x0008, vec![]).encode(); assert_eq!(notify[13], 0x40); } #[test] fn payload_length_exceeds_sixteen_bits_without_a_jumbo_flag() { // The stale 12-byte implementation would have needed an escape here. let frame = Frame::new(1, 1, 1, MsgType::Reply, vec![0x5A; 70_000]); let bytes = frame.encode(); let parsed = Header::parse(&bytes).unwrap(); assert_eq!(parsed.payload_len, 70_000); assert_eq!(bytes.len(), 16 + 70_000); } #[test] fn round_trips_with_metadata() { let frame = Frame::new(0x0009, 0x0007, 5, MsgType::Message, b"payload".to_vec()) .with_metadata(vec![0xde, 0xad, 0xbe, 0xef]); let bytes = frame.encode(); let (back, used) = Frame::parse(&bytes).unwrap(); assert_eq!(used, bytes.len()); assert_eq!(back.metadata, vec![0xde, 0xad, 0xbe, 0xef]); assert_eq!(back.payload, b"payload"); assert_eq!(back.encode(), bytes); } #[test] fn reply_echoes_routing_fields() { let req = Header { user_index: 5, ..Header::new(0x0001, 0x000A, 0x2222, MsgType::Message) }; let reply = Frame::reply_to(&req, vec![1, 2, 3]); assert_eq!(reply.header.component, 0x0001); assert_eq!(reply.header.command, 0x000A); assert_eq!(reply.header.msg_num, 0x2222); assert_eq!(reply.header.user_index, 5); assert_eq!(reply.header.msg_type, MsgType::Reply); } #[test] fn msg_num_is_twenty_four_bits() { let frame = Frame::new(1, 1, 0xFFFFFF, MsgType::Reply, vec![]); let back = Header::parse(&frame.encode()).unwrap(); assert_eq!(back.msg_num, 0xFFFFFF); } #[test] fn splits_coalesced_frames_and_leaves_a_partial_tail() { let a = Frame::new(9, 2, 1, MsgType::Reply, vec![1]).encode(); let b = Frame::new(1, 10, 2, MsgType::Reply, vec![2, 3]).encode(); let mut stream = a.clone(); stream.extend_from_slice(&b); stream.extend_from_slice(&[0x00, 0x00]); // partial third header let (frames, used) = parse_all(&stream).unwrap(); assert_eq!(frames.len(), 2); assert_eq!(used, a.len() + b.len()); assert_eq!(frames[1].payload, vec![2, 3]); } #[test] fn short_buffer_reports_what_it_needs() { assert_eq!( Header::parse(&[0u8; 8]), Err(Error::ShortFrame { need: 16, have: 8 }) ); assert_eq!(frame_size_hint(&[0u8; 8]), None); } }