1use bytes::Bytes;
14use thiserror::Error;
15
16pub const SOH: u8 = 0x01;
18
19pub mod tags {
21 pub const BEGIN_STRING: u32 = 8;
22 pub const BODY_LENGTH: u32 = 9;
23 pub const MSG_TYPE: u32 = 35;
24 pub const SENDER_COMP_ID: u32 = 49;
25 pub const TARGET_COMP_ID: u32 = 56;
26 pub const MSG_SEQ_NUM: u32 = 34;
27 pub const SENDING_TIME: u32 = 52;
28 pub const CHECK_SUM: u32 = 10;
29
30 pub const HEART_BT_INT: u32 = 108;
32 pub const TEST_REQ_ID: u32 = 112;
33 pub const BEGIN_SEQ_NO: u32 = 7;
34 pub const END_SEQ_NO: u32 = 16;
35 pub const RESET_SEQ_NUM_FLAG: u32 = 141;
36 pub const GAP_FILL_FLAG: u32 = 123;
37 pub const NEW_SEQ_NO: u32 = 36;
38 pub const POSS_DUP_FLAG: u32 = 43;
39 pub const ENCRYPT_METHOD: u32 = 98;
40 pub const TEXT: u32 = 58;
41 pub const DEFAULT_APPL_VER_ID: u32 = 1137;
42
43 pub const CL_ORD_ID: u32 = 11;
46 pub const SYMBOL: u32 = 55;
47 pub const SIDE: u32 = 54;
48 pub const ORDER_QTY: u32 = 38;
49 pub const ORD_STATUS: u32 = 39;
50 pub const EXEC_TYPE: u32 = 150;
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct FixField {
56 pub tag: u32,
57 pub value: String,
58}
59
60impl FixField {
61 pub fn new(tag: u32, value: impl Into<String>) -> Self {
62 FixField { tag, value: value.into() }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum MsgType {
69 Logon,
70 Heartbeat,
71 TestRequest,
72 ResendRequest,
73 SequenceReset,
74 Logout,
75 NewOrderSingle,
76 ExecutionReport,
77 Other(String),
78}
79
80impl MsgType {
81 pub fn code(&self) -> &str {
83 match self {
84 MsgType::Logon => "A",
85 MsgType::Heartbeat => "0",
86 MsgType::TestRequest => "1",
87 MsgType::ResendRequest => "2",
88 MsgType::SequenceReset => "4",
89 MsgType::Logout => "5",
90 MsgType::NewOrderSingle => "D",
91 MsgType::ExecutionReport => "8",
92 MsgType::Other(s) => s,
93 }
94 }
95
96 pub fn from_code(code: &str) -> MsgType {
98 match code {
99 "A" => MsgType::Logon,
100 "0" => MsgType::Heartbeat,
101 "1" => MsgType::TestRequest,
102 "2" => MsgType::ResendRequest,
103 "4" => MsgType::SequenceReset,
104 "5" => MsgType::Logout,
105 "D" => MsgType::NewOrderSingle,
106 "8" => MsgType::ExecutionReport,
107 other => MsgType::Other(other.to_string()),
108 }
109 }
110
111 pub fn is_admin(&self) -> bool {
115 matches!(
116 self,
117 MsgType::Logon
118 | MsgType::Heartbeat
119 | MsgType::TestRequest
120 | MsgType::ResendRequest
121 | MsgType::SequenceReset
122 | MsgType::Logout
123 )
124 }
125}
126
127#[derive(Debug, Error, PartialEq, Eq)]
129#[non_exhaustive]
130pub enum FixParseError {
131 #[error("empty frame")]
132 Empty,
133 #[error("field {0:?} is not valid `tag=value`")]
134 MalformedField(String),
135 #[error("tag {0:?} is not a valid integer")]
136 InvalidTag(String),
137 #[error("field bytes are not valid UTF-8")]
138 NotUtf8,
139 #[error("missing required field, tag {0}")]
140 MissingField(u32),
141 #[error("checksum mismatch: computed {computed:03}, frame carried {found:03}")]
142 ChecksumMismatch { computed: u8, found: u8 },
143 #[error("CheckSum(10) value {0:?} is not a 3-digit number")]
144 InvalidCheckSum(String),
145}
146
147#[derive(Debug, Clone, Default, PartialEq, Eq)]
149pub struct FixMessage {
150 fields: Vec<FixField>,
151}
152
153impl FixMessage {
154 pub fn new() -> Self {
156 FixMessage { fields: Vec::new() }
157 }
158
159 pub fn of_type(msg_type: MsgType) -> Self {
162 let mut m = FixMessage::new();
163 m.set(tags::MSG_TYPE, msg_type.code());
164 m
165 }
166
167 pub fn get(&self, tag: u32) -> Option<&str> {
169 self.fields.iter().find(|f| f.tag == tag).map(|f| f.value.as_str())
170 }
171
172 pub fn get_u64(&self, tag: u32) -> Option<u64> {
174 self.get(tag).and_then(|v| v.parse().ok())
175 }
176
177 pub fn set(&mut self, tag: u32, value: impl Into<String>) -> &mut Self {
180 let value = value.into();
181 if let Some(f) = self.fields.iter_mut().find(|f| f.tag == tag) {
182 f.value = value;
183 } else {
184 self.fields.push(FixField { tag, value });
185 }
186 self
187 }
188
189 pub fn fields(&self) -> &[FixField] {
192 &self.fields
193 }
194
195 pub fn msg_type(&self) -> Option<MsgType> {
197 self.get(tags::MSG_TYPE).map(MsgType::from_code)
198 }
199
200 pub fn seq_num(&self) -> Option<u64> {
202 self.get_u64(tags::MSG_SEQ_NUM)
203 }
204
205 pub fn to_wire(&self) -> Bytes {
214 let begin_string = self.get(tags::BEGIN_STRING).unwrap_or("FIX.4.4").to_string();
215 let msg_type = self.get(tags::MSG_TYPE).unwrap_or("").to_string();
216
217 let mut body = Vec::new();
220 push_field(&mut body, tags::MSG_TYPE, &msg_type);
221 for f in &self.fields {
222 if f.tag == tags::BEGIN_STRING
223 || f.tag == tags::BODY_LENGTH
224 || f.tag == tags::MSG_TYPE
225 || f.tag == tags::CHECK_SUM
226 {
227 continue;
228 }
229 push_field(&mut body, f.tag, &f.value);
230 }
231
232 let mut out = Vec::with_capacity(body.len() + 32);
233 push_field(&mut out, tags::BEGIN_STRING, &begin_string);
234 push_field(&mut out, tags::BODY_LENGTH, &body.len().to_string());
235 out.extend_from_slice(&body);
236
237 let sum: u32 = out.iter().map(|b| *b as u32).sum();
240 let checksum = (sum % 256) as u8;
241 push_field(&mut out, tags::CHECK_SUM, &format!("{checksum:03}"));
242
243 Bytes::from(out)
244 }
245
246 pub fn parse(input: &[u8]) -> Result<FixMessage, FixParseError> {
252 if input.is_empty() {
253 return Err(FixParseError::Empty);
254 }
255
256 let mut fields = Vec::new();
259 let mut checksum_boundary: Option<usize> = None; let mut found_checksum: Option<u8> = None;
261
262 let mut offset = 0usize;
263 for raw in input.split(|b| *b == SOH) {
264 if raw.is_empty() {
265 offset += 1;
267 continue;
268 }
269 let s = std::str::from_utf8(raw).map_err(|_| FixParseError::NotUtf8)?;
270 let eq = s.find('=').ok_or_else(|| FixParseError::MalformedField(s.to_string()))?;
271 let (tag_str, val_str) = s.split_at(eq);
272 let value = &val_str[1..]; let tag: u32 = tag_str.parse().map_err(|_| FixParseError::InvalidTag(tag_str.to_string()))?;
274
275 if tag == tags::CHECK_SUM {
276 checksum_boundary = Some(offset);
277 if value.len() != 3 || !value.bytes().all(|b| b.is_ascii_digit()) {
278 return Err(FixParseError::InvalidCheckSum(value.to_string()));
279 }
280 let parsed =
283 value.parse::<u32>().map_err(|_| FixParseError::InvalidCheckSum(value.to_string()))?;
284 found_checksum = Some(parsed as u8);
285 }
286
287 fields.push(FixField { tag, value: value.to_string() });
288 offset += raw.len() + 1; }
290
291 if fields.is_empty() {
292 return Err(FixParseError::Empty);
293 }
294
295 if let (Some(boundary), Some(found)) = (checksum_boundary, found_checksum) {
297 let sum: u32 = input[..boundary].iter().map(|b| *b as u32).sum();
298 let computed = (sum % 256) as u8;
299 if computed != found {
300 return Err(FixParseError::ChecksumMismatch { computed, found });
301 }
302 }
303
304 Ok(FixMessage { fields })
305 }
306}
307
308fn push_field(buf: &mut Vec<u8>, tag: u32, value: &str) {
309 buf.extend_from_slice(tag.to_string().as_bytes());
310 buf.push(b'=');
311 buf.extend_from_slice(value.as_bytes());
312 buf.push(SOH);
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn msg_type_codes_round_trip() {
321 for mt in [
322 MsgType::Logon,
323 MsgType::Heartbeat,
324 MsgType::TestRequest,
325 MsgType::ResendRequest,
326 MsgType::SequenceReset,
327 MsgType::Logout,
328 MsgType::NewOrderSingle,
329 MsgType::ExecutionReport,
330 ] {
331 assert_eq!(MsgType::from_code(mt.code()), mt);
332 }
333 assert_eq!(MsgType::from_code("XY"), MsgType::Other("XY".to_string()));
334 }
335
336 #[test]
337 fn to_wire_then_parse_round_trips() {
338 let mut m = FixMessage::of_type(MsgType::Heartbeat);
339 m.set(tags::BEGIN_STRING, "FIX.4.4");
340 m.set(tags::SENDER_COMP_ID, "CLIENT");
341 m.set(tags::TARGET_COMP_ID, "SERVER");
342 m.set(tags::MSG_SEQ_NUM, "1");
343 m.set(tags::TEST_REQ_ID, "ABC");
344
345 let wire = m.to_wire();
346 let parsed = FixMessage::parse(&wire).expect("parse");
347
348 assert_eq!(parsed.msg_type(), Some(MsgType::Heartbeat));
349 assert_eq!(parsed.get(tags::SENDER_COMP_ID), Some("CLIENT"));
350 assert_eq!(parsed.get(tags::TARGET_COMP_ID), Some("SERVER"));
351 assert_eq!(parsed.get(tags::TEST_REQ_ID), Some("ABC"));
352 assert_eq!(parsed.seq_num(), Some(1));
353 assert_eq!(parsed.get(tags::BEGIN_STRING), Some("FIX.4.4"));
355 assert!(parsed.get(tags::BODY_LENGTH).is_some());
356 assert!(parsed.get(tags::CHECK_SUM).is_some());
357 }
358
359 #[test]
360 fn checksum_matches_known_vector() {
361 let mut m = FixMessage::of_type(MsgType::Logon);
365 m.set(tags::BEGIN_STRING, "FIX.4.2");
366 m.set(tags::SENDER_COMP_ID, "SERVER");
367 m.set(tags::TARGET_COMP_ID, "CLIENT");
368 m.set(tags::MSG_SEQ_NUM, "177");
369 m.set(tags::SENDING_TIME, "20090107-18:15:16");
370 m.set(tags::ENCRYPT_METHOD, "0");
371 m.set(tags::HEART_BT_INT, "30");
372
373 let wire = m.to_wire();
374 let s = String::from_utf8(wire.to_vec()).unwrap().replace(SOH as char, "|");
375 assert_eq!(
376 s,
377 "8=FIX.4.2|9=65|35=A|49=SERVER|56=CLIENT|34=177|52=20090107-18:15:16|98=0|108=30|10=062|"
378 );
379
380 let parsed = FixMessage::parse(&wire).expect("parse");
382 assert_eq!(parsed.get(tags::CHECK_SUM), Some("062"));
383 }
384
385 #[test]
386 fn parse_rejects_bad_checksum() {
387 let mut m = FixMessage::of_type(MsgType::Heartbeat);
389 m.set(tags::BEGIN_STRING, "FIX.4.4");
390 m.set(tags::MSG_SEQ_NUM, "1");
391 let wire = m.to_wire();
392 let mut tampered = wire.to_vec();
393 let pos = tampered.len() - 2; tampered[pos] = if tampered[pos] == b'0' { b'1' } else { b'0' };
396 let err = FixMessage::parse(&tampered).unwrap_err();
397 assert!(matches!(err, FixParseError::ChecksumMismatch { .. }));
398 }
399
400 #[test]
401 fn parse_rejects_malformed_field() {
402 let bad = b"8=FIX.4.4\x01nonsense\x0135=0\x01";
403 let err = FixMessage::parse(bad).unwrap_err();
404 assert!(matches!(err, FixParseError::MalformedField(_)));
405 }
406}