feat: scaffold fifa-blaze Blaze protocol emulator (Milestone 1)
Two-crate workspace: blaze-proto (Fire2 framing via tokio_util codec, tdf=0.1 for TDF decode/stringify) and server (TLS listeners for redirector + Blaze, JSONL capture with TdfStringifier, pluggable FramingVariant enum). Both listeners bind on startup and write a capture JSONL for every packet. Component/command IDs are unknown — captures reveal them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
use bytes::Bytes;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::frame::{FireFrame, FrameType, PacketOptions};
|
||||
use tdf::stringify::TdfStringifier;
|
||||
use tdf::reader::TdfDeserializer;
|
||||
|
||||
/// A fully framed Blaze packet.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Packet {
|
||||
/// Decoded Fire2 header.
|
||||
pub frame: FireFrame,
|
||||
/// Raw body bytes (unparsed TDF).
|
||||
pub body: Bytes,
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
/// Create a raw-mode packet (no frame header — body only).
|
||||
pub fn raw(body: Bytes) -> Self {
|
||||
Packet {
|
||||
frame: FireFrame {
|
||||
component: 0,
|
||||
command: 0,
|
||||
error: 0,
|
||||
ty: FrameType::Request,
|
||||
options: PacketOptions::NONE,
|
||||
seq: 0,
|
||||
},
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an empty response that mirrors `req`'s routing fields.
|
||||
pub fn response_empty(req: &Packet) -> Self {
|
||||
Packet { frame: req.frame.response(), body: Bytes::new() }
|
||||
}
|
||||
|
||||
/// Build a response with the given TDF body.
|
||||
pub fn response<V: tdf::TdfSerialize>(req: &Packet, value: &V) -> Self {
|
||||
Packet {
|
||||
frame: req.frame.response(),
|
||||
body: Bytes::from(tdf::serialize_vec(value)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to decode the body as TDF and return a human-readable string.
|
||||
/// On any decode error, return the raw hex.
|
||||
pub fn tdf_string(&self) -> String {
|
||||
if self.body.is_empty() {
|
||||
return String::from("(empty)");
|
||||
}
|
||||
let r = TdfDeserializer::new(&self.body);
|
||||
let (s, ok) = TdfStringifier::<String>::new_string(r);
|
||||
if ok {
|
||||
s
|
||||
} else {
|
||||
format!("(decode error) raw={}", hex::encode(&self.body))
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize the packet into a JSON-friendly struct for capture logs.
|
||||
pub fn to_capture(&self) -> CaptureRecord {
|
||||
CaptureRecord {
|
||||
component: self.frame.component,
|
||||
command: self.frame.command,
|
||||
error: self.frame.error,
|
||||
ty: self.frame.ty.to_string(),
|
||||
seq: self.frame.seq,
|
||||
body_len: self.body.len(),
|
||||
raw_hex: hex::encode(&self.body),
|
||||
tdf: self.tdf_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CaptureRecord {
|
||||
pub component: u16,
|
||||
pub command: u16,
|
||||
pub error: u16,
|
||||
pub ty: String,
|
||||
pub seq: u16,
|
||||
pub body_len: usize,
|
||||
pub raw_hex: String,
|
||||
pub tdf: String,
|
||||
}
|
||||
Reference in New Issue
Block a user