ironfix_core/message.rs
1/******************************************************************************
2 Author: Joaquín Béjar García
3 Email: jb@taunais.com
4 Date: 27/1/26
5******************************************************************************/
6
7//! Message types and traits for FIX protocol.
8//!
9//! This module provides:
10//! - [`RawMessage`]: Zero-copy view into a FIX message buffer
11//! - [`OwnedMessage`]: Owned message for storage and cross-thread transfer
12//! - [`MsgType`]: Enumeration of FIX message types
13//! - [`FixMessage`]: Trait for typed message access
14
15use crate::error::{DecodeError, MsgTypeError};
16use crate::field::FieldRef;
17use arrayvec::ArrayString;
18use bytes::Bytes;
19use serde::{Deserialize, Serialize};
20use smallvec::SmallVec;
21use std::fmt;
22use std::ops::Range;
23
24/// Maximum length of a MsgType (tag 35) value in bytes.
25///
26/// # Why 8
27///
28/// Every MsgType the FIX specification defines is one or two bytes: in the
29/// vendored `spec/FIX44.xml` all 93 `msgtype` attributes and all 119 enum
30/// values of field 35 are at most two bytes long, the longest being the
31/// two-letter codes `AA`..`BH`. The remaining headroom is for bilaterally
32/// agreed codes, which counterparties conventionally prefix with `U` and
33/// number (`U1`, `U9999`), and for the two-letter codes later FIX versions
34/// keep adding.
35///
36/// Like [`crate::types::COMP_ID_MAX_LEN`], this bound is an IronFix
37/// engineering choice rather than a specification limit — the FIX
38/// specification types MsgType as an unbounded `String`. It is what keeps
39/// [`MsgType::Custom`] in inline [`ArrayString`] storage, so decoding a
40/// message whose MsgType this enum does not name allocates nothing. A value
41/// longer than this is [`MsgTypeError::TooLong`], never a truncation.
42pub const MSG_TYPE_MAX_LEN: usize = 8;
43
44/// Inline storage for a MsgType this enum does not name.
45///
46/// A validated newtype over an [`ArrayString`] bounded by [`MSG_TYPE_MAX_LEN`],
47/// so it lives in the enum itself and never on the heap. The inner buffer is
48/// **private** and reachable only through [`CustomMsgType::new`] (or the
49/// [`Deserialize`] impl, which routes the same constructor), so a
50/// [`MsgType::Custom`] can never be built around an empty code, an over-long
51/// code, or one carrying a byte that cannot appear in tag 35.
52#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
53pub struct CustomMsgType(ArrayString<MSG_TYPE_MAX_LEN>);
54
55impl CustomMsgType {
56 /// Copies an unrecognised MsgType code into inline storage, validating it.
57 ///
58 /// This is the **only** way to obtain a `CustomMsgType`: the inner buffer
59 /// is private, so a [`MsgType::Custom`] cannot wrap an unvalidated code,
60 /// and the [`Deserialize`] impl routes the same check. The bytes typically
61 /// reach here straight off the wire — every decoded MsgType is built
62 /// through [`MsgType::new`], which sends an unnamed code here — so this is
63 /// where an untrusted tag 35 value is either bounded, non-empty, and safe
64 /// to write back into a frame, or an error.
65 ///
66 /// SOH and any non-printable or non-ASCII byte are rejected because a
67 /// MsgType is echoed verbatim into tag 35 of an outbound message, where SOH
68 /// would terminate the field early and a non-printable byte would corrupt
69 /// the frame. `=` and space are **not** rejected: both are legal inside a
70 /// FIX field value — only SOH ends a field, and only the *first* `=` splits
71 /// a tag from its value — so a bilaterally agreed code carrying either must
72 /// round-trip unchanged. The accepted set is therefore printable ASCII,
73 /// `0x20..=0x7e`. The length bound is additionally enforced by
74 /// [`MSG_TYPE_MAX_LEN`], so it holds even for a code built by hand.
75 ///
76 /// # Errors
77 /// Returns [`MsgTypeError::Empty`] for an empty code,
78 /// [`MsgTypeError::TooLong`] if it exceeds [`MSG_TYPE_MAX_LEN`] bytes, or
79 /// [`MsgTypeError::IllegalByte`] if it carries a byte that cannot appear in
80 /// tag 35.
81 pub fn new(code: &str) -> Result<Self, MsgTypeError> {
82 if code.is_empty() {
83 return Err(MsgTypeError::Empty);
84 }
85 if code.len() > MSG_TYPE_MAX_LEN {
86 return Err(MsgTypeError::TooLong {
87 len: code.len(),
88 max_len: MSG_TYPE_MAX_LEN,
89 });
90 }
91 for (position, &byte) in code.as_bytes().iter().enumerate() {
92 if !(0x20..=0x7e).contains(&byte) {
93 return Err(MsgTypeError::IllegalByte { byte, position });
94 }
95 }
96 match ArrayString::<MSG_TYPE_MAX_LEN>::from(code) {
97 Ok(inner) => Ok(Self(inner)),
98 // Unreachable: the length was checked above.
99 Err(_) => Err(MsgTypeError::TooLong {
100 len: code.len(),
101 max_len: MSG_TYPE_MAX_LEN,
102 }),
103 }
104 }
105
106 /// Returns the code as a string slice, exactly as it appears in tag 35.
107 #[inline]
108 #[must_use]
109 pub fn as_str(&self) -> &str {
110 self.0.as_str()
111 }
112}
113
114impl<'de> Deserialize<'de> for CustomMsgType {
115 /// Deserializes a MsgType code, validating it through
116 /// [`CustomMsgType::new`] so a deserialized [`MsgType::Custom`] carries the
117 /// same guarantees as one built off the wire.
118 ///
119 /// # Errors
120 /// Fails with a deserialization error carrying the [`MsgTypeError`] message
121 /// when the code is empty, longer than [`MSG_TYPE_MAX_LEN`], or carries a
122 /// byte that cannot appear in tag 35.
123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124 where
125 D: serde::Deserializer<'de>,
126 {
127 struct CodeVisitor;
128
129 impl serde::de::Visitor<'_> for CodeVisitor {
130 type Value = CustomMsgType;
131
132 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(
134 f,
135 "a FIX MsgType code of 1..={MSG_TYPE_MAX_LEN} printable ASCII bytes"
136 )
137 }
138
139 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
140 where
141 E: serde::de::Error,
142 {
143 CustomMsgType::new(value).map_err(E::custom)
144 }
145 }
146
147 deserializer.deserialize_str(CodeVisitor)
148 }
149}
150
151/// Declares [`MsgType`] and the three mappings that must agree about it.
152///
153/// The variant, its wire code, and its admin/app category are stated **once**
154/// per row; `as_str`, the `from_str` reverse lookup, and `is_admin` are all
155/// generated from that single table, so the three cannot drift. A row's
156/// category must match the `msgcat` the dictionary gives that MsgType — see
157/// [`MsgType::is_admin`].
158macro_rules! define_msg_types {
159 (
160 $(
161 $(#[$variant_meta:meta])*
162 $variant:ident = $code:literal, admin = $admin:literal;
163 )*
164 ) => {
165 /// Standard FIX message types.
166 ///
167 /// This enum covers the most common administrative and application
168 /// messages. Custom or less common message types are represented as
169 /// [`MsgType::Custom`], whose payload is inline storage bounded by
170 /// [`MSG_TYPE_MAX_LEN`] — decoding a message with an unrecognised
171 /// MsgType therefore allocates nothing.
172 ///
173 /// # Equality follows the wire form
174 ///
175 /// [`PartialEq`] and [`Hash`] compare [`MsgType::as_str`], not the
176 /// variant: a `Custom` holding `D` equals [`MsgType::NewOrderSingle`],
177 /// because both put `35=D` on the wire. Without that, a `Custom` value
178 /// reaching a `HashMap` keyed by `MsgType` would silently miss the
179 /// entry its own wire form should have hit. Prefer [`MsgType::new`],
180 /// which folds a known code into its named variant.
181 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
182 pub enum MsgType {
183 $(
184 $(#[$variant_meta])*
185 $variant,
186 )*
187 /// Custom or unknown message type.
188 ///
189 /// The payload is a [`CustomMsgType`], so an over-long code is
190 /// unrepresentable rather than truncated. Build it with
191 /// [`MsgType::new`], which also rejects an empty code and one
192 /// carrying a byte that cannot appear in tag 35.
193 Custom(CustomMsgType),
194 }
195
196 impl MsgType {
197 /// Returns the string representation of this message type.
198 #[must_use]
199 pub fn as_str(&self) -> &str {
200 match self {
201 $( Self::$variant => $code, )*
202 Self::Custom(s) => s.as_str(),
203 }
204 }
205
206 /// Creates a MsgType from its on-the-wire code.
207 ///
208 /// A code this enum names folds into that named variant, so the
209 /// result is always in normal form; anything else becomes
210 /// [`MsgType::Custom`], copied into inline storage without
211 /// allocating.
212 ///
213 /// # Arguments
214 /// * `code` - The message type string (e.g., "D" for NewOrderSingle)
215 ///
216 /// # Errors
217 /// A named code never fails. Anything else returns
218 /// [`MsgTypeError::Empty`] for an empty code,
219 /// [`MsgTypeError::TooLong`] if it exceeds [`MSG_TYPE_MAX_LEN`]
220 /// bytes, or [`MsgTypeError::IllegalByte`] if it carries a byte
221 /// that cannot appear in tag 35. An over-long code is **never**
222 /// truncated: a truncated MsgType is a different, valid MsgType,
223 /// so it would route the message to the wrong handler instead of
224 /// failing.
225 pub fn new(code: &str) -> Result<Self, MsgTypeError> {
226 match code {
227 $( $code => Ok(Self::$variant), )*
228 other => Ok(Self::Custom(CustomMsgType::new(other)?)),
229 }
230 }
231
232 /// Returns true if this is an administrative (session-level)
233 /// message.
234 ///
235 /// The classification is the `msgcat` attribute the FIX dictionary
236 /// gives each MsgType, and this list must agree with it. Note that
237 /// XMLnonFIX (`n`) is `msgcat='admin'` in `FIX44.xml`, so it is
238 /// administrative despite not being one of the seven classic
239 /// session messages.
240 ///
241 /// A [`MsgType::Custom`] carrying a code this table names is
242 /// classified by that code, so two values that compare equal
243 /// always agree here. A genuinely unrecognised code is treated as
244 /// an application message: nothing here knows a private code's
245 /// category, and routing an unknown message through the session
246 /// path would let a counterparty reach session handling with a
247 /// type this engine cannot interpret.
248 #[must_use]
249 pub fn is_admin(&self) -> bool {
250 match self {
251 $( Self::$variant => $admin, )*
252 // Same table, reached by wire code rather than by
253 // discriminant; borrows the code, never allocates.
254 Self::Custom(code) => match code.as_str() {
255 $( $code => $admin, )*
256 _ => false,
257 },
258 }
259 }
260 }
261
262 #[cfg(test)]
263 impl MsgType {
264 /// Every named variant, for exhaustive round-trip tests.
265 fn all_named() -> Vec<Self> {
266 vec![ $( Self::$variant, )* ]
267 }
268 }
269 };
270}
271
272define_msg_types! {
273 /// Heartbeat (0) - Session level.
274 #[default]
275 Heartbeat = "0", admin = true;
276 /// Test Request (1) - Session level.
277 TestRequest = "1", admin = true;
278 /// Resend Request (2) - Session level.
279 ResendRequest = "2", admin = true;
280 /// Reject (3) - Session level.
281 Reject = "3", admin = true;
282 /// Sequence Reset (4) - Session level.
283 SequenceReset = "4", admin = true;
284 /// Logout (5) - Session level.
285 Logout = "5", admin = true;
286 /// Indication of Interest (6).
287 IndicationOfInterest = "6", admin = false;
288 /// Advertisement (7).
289 Advertisement = "7", admin = false;
290 /// Execution Report (8).
291 ExecutionReport = "8", admin = false;
292 /// Order Cancel Reject (9).
293 OrderCancelReject = "9", admin = false;
294 /// Logon (A) - Session level.
295 Logon = "A", admin = true;
296 /// News (B).
297 News = "B", admin = false;
298 /// Email (C).
299 Email = "C", admin = false;
300 /// New Order Single (D).
301 NewOrderSingle = "D", admin = false;
302 /// New Order List (E).
303 NewOrderList = "E", admin = false;
304 /// Order Cancel Request (F).
305 OrderCancelRequest = "F", admin = false;
306 /// Order Cancel/Replace Request (G).
307 OrderCancelReplaceRequest = "G", admin = false;
308 /// Order Status Request (H).
309 OrderStatusRequest = "H", admin = false;
310 /// Allocation Instruction (J).
311 AllocationInstruction = "J", admin = false;
312 /// List Cancel Request (K).
313 ListCancelRequest = "K", admin = false;
314 /// List Execute (L).
315 ListExecute = "L", admin = false;
316 /// List Status Request (M).
317 ListStatusRequest = "M", admin = false;
318 /// List Status (N).
319 ListStatus = "N", admin = false;
320 /// Allocation Instruction Ack (P).
321 AllocationInstructionAck = "P", admin = false;
322 /// Don't Know Trade (Q).
323 DontKnowTrade = "Q", admin = false;
324 /// Quote Request (R).
325 QuoteRequest = "R", admin = false;
326 /// Quote (S).
327 Quote = "S", admin = false;
328 /// Settlement Instructions (T).
329 SettlementInstructions = "T", admin = false;
330 /// Market Data Request (V).
331 MarketDataRequest = "V", admin = false;
332 /// Market Data Snapshot/Full Refresh (W).
333 MarketDataSnapshotFullRefresh = "W", admin = false;
334 /// Market Data Incremental Refresh (X).
335 MarketDataIncrementalRefresh = "X", admin = false;
336 /// Market Data Request Reject (Y).
337 MarketDataRequestReject = "Y", admin = false;
338 /// Quote Cancel (Z).
339 QuoteCancel = "Z", admin = false;
340 /// Quote Status Request (a).
341 QuoteStatusRequest = "a", admin = false;
342 /// Mass Quote Acknowledgement (b).
343 MassQuoteAcknowledgement = "b", admin = false;
344 /// Security Definition Request (c).
345 SecurityDefinitionRequest = "c", admin = false;
346 /// Security Definition (d).
347 SecurityDefinition = "d", admin = false;
348 /// Security Status Request (e).
349 SecurityStatusRequest = "e", admin = false;
350 /// Security Status (f).
351 SecurityStatus = "f", admin = false;
352 /// Trading Session Status Request (g).
353 TradingSessionStatusRequest = "g", admin = false;
354 /// Trading Session Status (h).
355 TradingSessionStatus = "h", admin = false;
356 /// Mass Quote (i).
357 MassQuote = "i", admin = false;
358 /// Business Message Reject (j).
359 BusinessMessageReject = "j", admin = false;
360 /// Bid Request (k).
361 BidRequest = "k", admin = false;
362 /// Bid Response (l).
363 BidResponse = "l", admin = false;
364 /// List Strike Price (m).
365 ListStrikePrice = "m", admin = false;
366 /// XML Message, XMLnonFIX (n) - `msgcat='admin'` in the FIX dictionary.
367 XmlMessage = "n", admin = true;
368 /// Registration Instructions (o).
369 RegistrationInstructions = "o", admin = false;
370 /// Registration Instructions Response (p).
371 RegistrationInstructionsResponse = "p", admin = false;
372 /// Order Mass Cancel Request (q).
373 OrderMassCancelRequest = "q", admin = false;
374 /// Order Mass Cancel Report (r).
375 OrderMassCancelReport = "r", admin = false;
376 /// New Order Cross (s).
377 NewOrderCross = "s", admin = false;
378 /// Cross Order Cancel/Replace Request (t).
379 CrossOrderCancelReplaceRequest = "t", admin = false;
380 /// Cross Order Cancel Request (u).
381 CrossOrderCancelRequest = "u", admin = false;
382 /// Security Type Request (v).
383 SecurityTypeRequest = "v", admin = false;
384 /// Security Types (w).
385 SecurityTypes = "w", admin = false;
386 /// Security List Request (x).
387 SecurityListRequest = "x", admin = false;
388 /// Security List (y).
389 SecurityList = "y", admin = false;
390 /// Derivative Security List Request (z).
391 DerivativeSecurityListRequest = "z", admin = false;
392}
393
394impl std::str::FromStr for MsgType {
395 type Err = MsgTypeError;
396
397 /// Creates a MsgType from a string value.
398 ///
399 /// An unrecognised but representable code becomes [`MsgType::Custom`],
400 /// which is how a private or newer message type reaches the application
401 /// layer without the decoder having to know it. Delegates to
402 /// [`MsgType::new`], so the result is always in normal form.
403 ///
404 /// # Arguments
405 /// * `s` - The message type string (e.g., "D" for NewOrderSingle)
406 ///
407 /// # Errors
408 /// Returns [`MsgTypeError::Empty`], [`MsgTypeError::TooLong`], or
409 /// [`MsgTypeError::IllegalByte`]. This was `Infallible` while `Custom`
410 /// held a heap `String`; bounding that storage makes an over-long code
411 /// unrepresentable, and rejecting it is the only alternative to a
412 /// truncation that would reroute the message.
413 fn from_str(s: &str) -> Result<Self, Self::Err> {
414 Self::new(s)
415 }
416}
417
418impl MsgType {
419 /// Returns true if this is an application message.
420 #[must_use]
421 pub fn is_app(&self) -> bool {
422 !self.is_admin()
423 }
424}
425
426/// Compares the on-the-wire form, not the variant.
427///
428/// `Custom("D")` and `NewOrderSingle` both encode `35=D`, so they compare
429/// equal; anything else would make a `Custom` value miss its own entry in a
430/// `MsgType`-keyed map.
431impl PartialEq for MsgType {
432 fn eq(&self, other: &Self) -> bool {
433 self.as_str() == other.as_str()
434 }
435}
436
437impl Eq for MsgType {}
438
439/// Hashes the on-the-wire form, keeping `Hash` consistent with [`PartialEq`].
440impl std::hash::Hash for MsgType {
441 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
442 self.as_str().hash(state);
443 }
444}
445
446impl fmt::Display for MsgType {
447 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448 write!(f, "{}", self.as_str())
449 }
450}
451
452/// Zero-copy view into a FIX message buffer.
453///
454/// This struct holds references to the original message buffer,
455/// avoiding allocation during parsing. Fields are stored as
456/// offset ranges into the buffer.
457///
458/// # Range convention
459///
460/// **Every stored range is relative to `buffer`, not to whatever larger buffer
461/// the message may have been decoded from.** A decoder that reads several
462/// messages out of one input must rebase its offsets by the start of the
463/// current message before constructing a `RawMessage`; [`RawMessage::new`]
464/// rejects any range that does not lie within `buffer`.
465#[derive(Debug, Clone)]
466pub struct RawMessage<'a> {
467 /// The complete message buffer.
468 buffer: &'a [u8],
469 /// Range of the BeginString field value, relative to `buffer`.
470 begin_string: Range<usize>,
471 /// Range of the message body (after BodyLength, before checksum),
472 /// relative to `buffer`.
473 body: Range<usize>,
474 /// The parsed message type.
475 msg_type: MsgType,
476 /// Parsed field references (tag and value ranges).
477 fields: SmallVec<[FieldRef<'a>; 32]>,
478}
479
480/// Validates that `range` is well ordered and lies within `buffer_len` bytes.
481#[inline]
482fn check_range(range: &Range<usize>, buffer_len: usize) -> Result<(), DecodeError> {
483 if range.start > range.end || range.end > buffer_len {
484 return Err(DecodeError::RangeOutOfBounds {
485 start: range.start,
486 end: range.end,
487 buffer_len,
488 });
489 }
490 Ok(())
491}
492
493impl<'a> RawMessage<'a> {
494 /// Creates a new RawMessage from parsed components.
495 ///
496 /// # Arguments
497 /// * `buffer` - The complete message buffer
498 /// * `begin_string` - Range of the BeginString value, relative to `buffer`
499 /// * `body` - Range of the message body, relative to `buffer`
500 /// * `msg_type` - The parsed message type
501 /// * `fields` - Parsed field references
502 ///
503 /// # Errors
504 /// Returns [`DecodeError::RangeOutOfBounds`] if `begin_string` or `body`
505 /// is inverted or does not lie within `buffer`. Every offset here derives
506 /// from attacker-supplied bytes, so the bounds are checked once at
507 /// construction rather than at every accessor.
508 pub fn new(
509 buffer: &'a [u8],
510 begin_string: Range<usize>,
511 body: Range<usize>,
512 msg_type: MsgType,
513 fields: SmallVec<[FieldRef<'a>; 32]>,
514 ) -> Result<Self, DecodeError> {
515 check_range(&begin_string, buffer.len())?;
516 check_range(&body, buffer.len())?;
517
518 Ok(Self {
519 buffer,
520 begin_string,
521 body,
522 msg_type,
523 fields,
524 })
525 }
526
527 /// Returns the complete message buffer.
528 #[inline]
529 #[must_use]
530 pub const fn buffer(&self) -> &'a [u8] {
531 self.buffer
532 }
533
534 /// Returns the BeginString value (e.g., "FIX.4.4").
535 ///
536 /// # Errors
537 /// Returns [`DecodeError::InvalidUtf8`] if the BeginString bytes are not
538 /// valid UTF-8, or [`DecodeError::RangeOutOfBounds`] if the stored range
539 /// escapes the buffer. The value is never silently defaulted to `""`.
540 pub fn begin_string(&self) -> Result<&'a str, DecodeError> {
541 let range = self.begin_string.clone();
542 debug_assert!(
543 range.end <= self.buffer.len(),
544 "RawMessage ranges must be buffer-relative"
545 );
546 let bytes = self
547 .buffer
548 .get(range.clone())
549 .ok_or(DecodeError::RangeOutOfBounds {
550 start: range.start,
551 end: range.end,
552 buffer_len: self.buffer.len(),
553 })?;
554 std::str::from_utf8(bytes).map_err(DecodeError::from)
555 }
556
557 /// Returns the message body bytes (after BodyLength, before the checksum).
558 ///
559 /// # Errors
560 /// Returns [`DecodeError::RangeOutOfBounds`] if the stored range escapes
561 /// the buffer.
562 pub fn body(&self) -> Result<&'a [u8], DecodeError> {
563 let range = self.body.clone();
564 debug_assert!(
565 range.end <= self.buffer.len(),
566 "RawMessage ranges must be buffer-relative"
567 );
568 self.buffer
569 .get(range.clone())
570 .ok_or(DecodeError::RangeOutOfBounds {
571 start: range.start,
572 end: range.end,
573 buffer_len: self.buffer.len(),
574 })
575 }
576
577 /// Returns the message type.
578 #[inline]
579 #[must_use]
580 pub fn msg_type(&self) -> &MsgType {
581 &self.msg_type
582 }
583
584 /// Returns an iterator over all fields.
585 #[inline]
586 pub fn fields(&self) -> impl Iterator<Item = &FieldRef<'a>> {
587 self.fields.iter()
588 }
589
590 /// Returns the number of fields in the message.
591 #[inline]
592 #[must_use]
593 pub fn field_count(&self) -> usize {
594 self.fields.len()
595 }
596
597 /// Gets a field by tag number.
598 ///
599 /// # Arguments
600 /// * `tag` - The field tag number
601 ///
602 /// # Returns
603 /// The first field with the given tag, or `None` if not found.
604 #[must_use]
605 pub fn get_field(&self, tag: u32) -> Option<&FieldRef<'a>> {
606 self.fields.iter().find(|f| f.tag == tag)
607 }
608
609 /// Gets a field value as a string.
610 ///
611 /// # Arguments
612 /// * `tag` - The field tag number
613 ///
614 /// # Returns
615 /// The field value as a string, or `None` if not found or invalid UTF-8.
616 #[must_use]
617 pub fn get_field_str(&self, tag: u32) -> Option<&'a str> {
618 self.get_field(tag).and_then(|f| f.as_str().ok())
619 }
620
621 /// Gets a field value parsed as the specified type.
622 ///
623 /// # Arguments
624 /// * `tag` - The field tag number
625 ///
626 /// # Errors
627 /// Returns `DecodeError` if the field is not found or cannot be parsed.
628 pub fn get_field_as<T: std::str::FromStr>(&self, tag: u32) -> Result<T, DecodeError> {
629 self.get_field(tag)
630 .ok_or(DecodeError::MissingRequiredField { tag })?
631 .parse()
632 }
633
634 /// Returns the message body range.
635 #[inline]
636 #[must_use]
637 pub fn body_range(&self) -> &Range<usize> {
638 &self.body
639 }
640
641 /// Returns the message length in bytes.
642 #[inline]
643 #[must_use]
644 pub fn len(&self) -> usize {
645 self.buffer.len()
646 }
647
648 /// Returns true if the message is empty.
649 #[inline]
650 #[must_use]
651 pub fn is_empty(&self) -> bool {
652 self.buffer.is_empty()
653 }
654
655 /// Converts this borrowed message to an owned message.
656 #[must_use]
657 pub fn to_owned(&self) -> OwnedMessage {
658 OwnedMessage::from_raw(self)
659 }
660}
661
662/// Owned FIX message for storage and cross-thread transfer.
663///
664/// Unlike [`RawMessage`], this struct owns its data and can be
665/// safely sent across threads or stored for later use.
666#[derive(Debug, Clone)]
667pub struct OwnedMessage {
668 /// The complete message buffer.
669 buffer: Bytes,
670 /// The parsed message type.
671 msg_type: MsgType,
672 /// Field offsets: (tag, value_range).
673 field_offsets: Vec<(u32, Range<usize>)>,
674}
675
676impl OwnedMessage {
677 /// Creates an OwnedMessage from a RawMessage.
678 ///
679 /// # Borrow invariant
680 ///
681 /// Field offsets are recovered by subtracting the buffer's address from
682 /// each field value's address, so **every `FieldRef` in `raw` must borrow
683 /// from `raw.buffer()`** — which is what a decoder that produced both
684 /// guarantees. A field pointing anywhere else has no offset in this
685 /// buffer; rather than computing a wrapped or out-of-range one, it is
686 /// **dropped**, and `field_count()` on the result is correspondingly
687 /// lower. Such a `RawMessage` is a construction bug in whatever produced
688 /// it, not an input this type can repair.
689 ///
690 /// # Arguments
691 /// * `raw` - The raw message to copy
692 #[must_use]
693 pub fn from_raw(raw: &RawMessage<'_>) -> Self {
694 let buffer = Bytes::copy_from_slice(raw.buffer);
695 let buffer_start = raw.buffer.as_ptr() as usize;
696 let buffer_len = raw.buffer.len();
697
698 let mut field_offsets = Vec::with_capacity(raw.fields.len());
699 for field in &raw.fields {
700 let offset = (field.value.as_ptr() as usize)
701 .checked_sub(buffer_start)
702 .and_then(|start| {
703 let end = start.checked_add(field.value.len())?;
704 (end <= buffer_len).then_some(start..end)
705 });
706 if let Some(range) = offset {
707 field_offsets.push((field.tag, range));
708 }
709 }
710
711 Self {
712 buffer,
713 msg_type: raw.msg_type.clone(),
714 field_offsets,
715 }
716 }
717
718 /// Creates an OwnedMessage from raw bytes.
719 ///
720 /// # Arguments
721 /// * `buffer` - The message bytes
722 /// * `msg_type` - The message type
723 /// * `field_offsets` - Field tag and value range pairs
724 #[must_use]
725 pub fn new(buffer: Bytes, msg_type: MsgType, field_offsets: Vec<(u32, Range<usize>)>) -> Self {
726 Self {
727 buffer,
728 msg_type,
729 field_offsets,
730 }
731 }
732
733 /// Returns the message type.
734 #[inline]
735 #[must_use]
736 pub fn msg_type(&self) -> &MsgType {
737 &self.msg_type
738 }
739
740 /// Returns the message bytes.
741 #[inline]
742 #[must_use]
743 pub fn as_bytes(&self) -> &[u8] {
744 &self.buffer
745 }
746
747 /// Returns the message length in bytes.
748 #[inline]
749 #[must_use]
750 pub fn len(&self) -> usize {
751 self.buffer.len()
752 }
753
754 /// Returns true if the message is empty.
755 #[inline]
756 #[must_use]
757 pub fn is_empty(&self) -> bool {
758 self.buffer.is_empty()
759 }
760
761 /// Gets a field value by tag.
762 ///
763 /// # Arguments
764 /// * `tag` - The field tag number
765 ///
766 /// # Returns
767 /// The field value bytes, or `None` if not found or if the stored offset
768 /// escapes the buffer.
769 #[must_use]
770 pub fn get_field(&self, tag: u32) -> Option<&[u8]> {
771 self.field_offsets
772 .iter()
773 .find(|(t, _)| *t == tag)
774 .and_then(|(_, range)| self.buffer.get(range.clone()))
775 }
776
777 /// Gets a field value as a string.
778 ///
779 /// # Arguments
780 /// * `tag` - The field tag number
781 ///
782 /// # Returns
783 /// The field value as a string, or `None` if not found or invalid UTF-8.
784 #[must_use]
785 pub fn get_field_str(&self, tag: u32) -> Option<&str> {
786 self.get_field(tag)
787 .and_then(|b| std::str::from_utf8(b).ok())
788 }
789
790 /// Returns the number of fields.
791 #[inline]
792 #[must_use]
793 pub fn field_count(&self) -> usize {
794 self.field_offsets.len()
795 }
796
797 /// Consumes the message and returns the underlying buffer.
798 #[must_use]
799 pub fn into_bytes(self) -> Bytes {
800 self.buffer
801 }
802}
803
804/// Trait for typed FIX message access.
805///
806/// This trait is implemented by generated message types to provide
807/// type-safe encoding and decoding.
808pub trait FixMessage: Sized {
809 /// The message type string (e.g., "D" for NewOrderSingle).
810 const MSG_TYPE: &'static str;
811
812 /// Decodes a message from a raw message.
813 ///
814 /// # Arguments
815 /// * `raw` - The raw message to decode
816 ///
817 /// # Errors
818 /// Returns `DecodeError` if the message cannot be decoded.
819 fn from_raw(raw: &RawMessage<'_>) -> Result<Self, DecodeError>;
820
821 /// Encodes the message to a buffer.
822 ///
823 /// # Arguments
824 /// * `buf` - The buffer to write to
825 ///
826 /// # Errors
827 /// Returns `EncodeError` if the message cannot be encoded.
828 fn encode(&self, buf: &mut Vec<u8>) -> Result<(), crate::error::EncodeError>;
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834
835 use serde::de::IntoDeserializer;
836 use serde::de::value::{Error as ValueError, StrDeserializer};
837 use std::collections::hash_map::DefaultHasher;
838 use std::collections::{HashMap, HashSet};
839 use std::hash::{Hash, Hasher};
840
841 /// Builds a `Custom` variant directly, bypassing the normal-form folding
842 /// [`MsgType::new`] applies, so a `Custom` holding a named code can be
843 /// tested. Still routes the validating [`CustomMsgType::new`], so a test
844 /// code that could not appear in tag 35 is a test bug, not a fixture.
845 fn custom_variant(code: &str) -> MsgType {
846 match CustomMsgType::new(code) {
847 Ok(inner) => MsgType::Custom(inner),
848 Err(err) => panic!("test code {code:?} must be a valid custom MsgType: {err}"),
849 }
850 }
851
852 #[test]
853 fn test_msg_type_from_str() {
854 assert_eq!("0".parse::<MsgType>(), Ok(MsgType::Heartbeat));
855 assert_eq!("A".parse::<MsgType>(), Ok(MsgType::Logon));
856 assert_eq!("D".parse::<MsgType>(), Ok(MsgType::NewOrderSingle));
857 assert_eq!("8".parse::<MsgType>(), Ok(MsgType::ExecutionReport));
858 }
859
860 #[test]
861 fn test_msg_type_as_str() {
862 assert_eq!(MsgType::Heartbeat.as_str(), "0");
863 assert_eq!(MsgType::Logon.as_str(), "A");
864 assert_eq!(MsgType::NewOrderSingle.as_str(), "D");
865 }
866
867 #[test]
868 fn test_msg_type_every_variant_round_trips_through_its_code() {
869 for variant in MsgType::all_named() {
870 let code = variant.as_str().to_owned();
871 assert_eq!(
872 MsgType::new(&code),
873 Ok(variant.clone()),
874 "MsgType::new({code:?}) must recover {variant:?}"
875 );
876 // A named code must never fall through to Custom.
877 assert!(
878 !matches!(MsgType::new(&code), Ok(MsgType::Custom(_))),
879 "{code:?} must map to a named variant, not Custom"
880 );
881 }
882 }
883
884 #[test]
885 fn test_msg_type_codes_are_unique() {
886 let named = MsgType::all_named();
887 let codes: HashSet<&str> = named.iter().map(MsgType::as_str).collect();
888 assert_eq!(
889 codes.len(),
890 named.len(),
891 "two variants share one wire code, so the reverse lookup is ambiguous"
892 );
893 }
894
895 #[test]
896 fn test_msg_type_is_admin() {
897 assert!(MsgType::Heartbeat.is_admin());
898 assert!(MsgType::Logon.is_admin());
899 assert!(MsgType::Logout.is_admin());
900 assert!(!MsgType::NewOrderSingle.is_admin());
901 assert!(!MsgType::ExecutionReport.is_admin());
902 }
903
904 #[test]
905 fn test_msg_type_is_admin_matches_dictionary_msgcat() {
906 // The eight msgtype values marked msgcat='admin' in FIX44.xml.
907 const ADMIN_CODES: [&str; 8] = ["0", "1", "2", "3", "4", "5", "A", "n"];
908 for variant in MsgType::all_named() {
909 let expected = ADMIN_CODES.contains(&variant.as_str());
910 assert_eq!(
911 variant.is_admin(),
912 expected,
913 "{variant:?} ({}) disagrees with the dictionary msgcat",
914 variant.as_str()
915 );
916 assert_eq!(variant.is_app(), !expected);
917 }
918 }
919
920 #[test]
921 fn test_msg_type_xml_message_is_admin() {
922 assert_eq!(MsgType::new("n"), Ok(MsgType::XmlMessage));
923 assert!(MsgType::XmlMessage.is_admin());
924 assert!(!MsgType::XmlMessage.is_app());
925 }
926
927 #[test]
928 fn test_msg_type_custom() {
929 let custom = custom_variant("XX");
930 assert_eq!("XX".parse::<MsgType>(), Ok(custom.clone()));
931 assert_eq!(custom.as_str(), "XX");
932 assert!(!custom.is_admin());
933 assert!(custom.is_app());
934 }
935
936 #[test]
937 fn test_msg_type_new_normalises_known_code_out_of_custom() {
938 assert_eq!(MsgType::new("D"), Ok(MsgType::NewOrderSingle));
939 assert!(matches!(MsgType::new("D"), Ok(MsgType::NewOrderSingle)));
940 assert!(matches!(MsgType::new("ZZ"), Ok(MsgType::Custom(_))));
941 }
942
943 #[test]
944 fn test_msg_type_custom_at_the_bound_round_trips() {
945 // MSG_TYPE_MAX_LEN bytes exactly: accepted, stored inline, and
946 // recovered byte for byte.
947 let code = "U9999999";
948 assert_eq!(code.len(), MSG_TYPE_MAX_LEN);
949 let parsed = code.parse::<MsgType>();
950 assert_eq!(parsed, Ok(custom_variant(code)));
951 match parsed {
952 Ok(msg_type) => assert_eq!(msg_type.as_str(), code),
953 Err(err) => panic!("a {MSG_TYPE_MAX_LEN}-byte code must be accepted, got {err}"),
954 }
955 }
956
957 #[test]
958 fn test_msg_type_one_byte_over_the_bound_is_too_long_not_truncated() {
959 // One byte over: rejected outright. Truncating to "U9999999" would
960 // silently route this message to a different, valid MsgType.
961 let code = "U99999999";
962 assert_eq!(code.len(), MSG_TYPE_MAX_LEN + 1);
963 assert_eq!(
964 code.parse::<MsgType>(),
965 Err(MsgTypeError::TooLong {
966 len: code.len(),
967 max_len: MSG_TYPE_MAX_LEN,
968 })
969 );
970 }
971
972 #[test]
973 fn test_msg_type_far_over_the_bound_is_too_long() {
974 let code = "A".repeat(4096);
975 assert_eq!(
976 MsgType::new(&code),
977 Err(MsgTypeError::TooLong {
978 len: 4096,
979 max_len: MSG_TYPE_MAX_LEN,
980 })
981 );
982 }
983
984 #[test]
985 fn test_msg_type_custom_owns_no_heap_allocation() {
986 // The regression guard for this whole change: a type that needs
987 // dropping owns something on the heap, and `MsgType` is built once per
988 // decoded message. With `Custom(String)` this held an allocation the
989 // decode success path paid for on every frame with an unrecognised
990 // MsgType.
991 assert!(
992 !std::mem::needs_drop::<MsgType>(),
993 "MsgType must own no heap allocation"
994 );
995 assert!(
996 !std::mem::needs_drop::<CustomMsgType>(),
997 "a custom MsgType code must live inline, not behind a pointer"
998 );
999 }
1000
1001 #[test]
1002 fn test_msg_type_empty_code_is_rejected() {
1003 assert_eq!("".parse::<MsgType>(), Err(MsgTypeError::Empty));
1004 }
1005
1006 #[test]
1007 fn test_msg_type_soh_byte_is_rejected() {
1008 // SOH is the one byte that terminates a field: `35=A\x01B` would put
1009 // `35=A` on the wire and then a bogus field starting `B`. It is
1010 // rejected wherever it appears in the code.
1011 assert_eq!(
1012 MsgType::new("A\x01B"),
1013 Err(MsgTypeError::IllegalByte {
1014 byte: 0x01,
1015 position: 1,
1016 })
1017 );
1018 }
1019
1020 #[test]
1021 fn test_msg_type_control_or_non_ascii_byte_is_rejected() {
1022 // DEL (0x7f) is a control byte just past the printable range; a
1023 // non-ASCII byte lies entirely outside it. Neither can appear in a
1024 // MsgType echoed into tag 35.
1025 assert_eq!(
1026 MsgType::new("A\x7fB"),
1027 Err(MsgTypeError::IllegalByte {
1028 byte: 0x7f,
1029 position: 1,
1030 })
1031 );
1032 // "é" is 0xC3 0xA9 in UTF-8; its first byte trips the check at offset 0.
1033 assert_eq!(
1034 MsgType::new("é"),
1035 Err(MsgTypeError::IllegalByte {
1036 byte: 0xc3,
1037 position: 0,
1038 })
1039 );
1040 }
1041
1042 #[test]
1043 fn test_msg_type_equals_and_space_are_accepted() {
1044 // `=` and space are legal inside a FIX field value — only SOH ends a
1045 // field and only the first `=` splits tag from value — so a bilateral
1046 // code carrying either must round-trip unchanged, not be refused.
1047 let with_equals = MsgType::new("U=1");
1048 assert_eq!(with_equals, Ok(custom_variant("U=1")));
1049 match with_equals {
1050 Ok(msg_type) => assert_eq!(msg_type.as_str(), "U=1"),
1051 Err(err) => panic!("`=` must be accepted in a custom code, got {err}"),
1052 }
1053
1054 let with_space = MsgType::new("U 1");
1055 assert_eq!(with_space, Ok(custom_variant("U 1")));
1056 match with_space {
1057 Ok(msg_type) => assert_eq!(msg_type.as_str(), "U 1"),
1058 Err(err) => panic!("space must be accepted in a custom code, got {err}"),
1059 }
1060 }
1061
1062 #[test]
1063 fn test_custom_msg_type_new_validates_like_msg_type_new() {
1064 // The public constructor is the single validation chokepoint the
1065 // `Custom` variant and `Deserialize` both route through.
1066 assert_eq!(CustomMsgType::new("").err(), Some(MsgTypeError::Empty));
1067 assert_eq!(
1068 CustomMsgType::new("U99999999").err(),
1069 Some(MsgTypeError::TooLong {
1070 len: 9,
1071 max_len: MSG_TYPE_MAX_LEN,
1072 })
1073 );
1074 assert_eq!(
1075 CustomMsgType::new("A\x01B").err(),
1076 Some(MsgTypeError::IllegalByte {
1077 byte: 0x01,
1078 position: 1,
1079 })
1080 );
1081 match CustomMsgType::new("U=1") {
1082 Ok(code) => assert_eq!(code.as_str(), "U=1"),
1083 Err(err) => panic!("`U=1` must be a valid custom code, got {err}"),
1084 }
1085 }
1086
1087 #[test]
1088 fn test_custom_msg_type_deserialize_rejects_illegal_code() {
1089 // The derived `MsgType` deserialization reaches the `Custom` field
1090 // through this impl, so validation cannot be bypassed by deserializing.
1091 let de: StrDeserializer<'_, ValueError> = "A\x01B".into_deserializer();
1092 assert!(CustomMsgType::deserialize(de).is_err());
1093 }
1094
1095 #[test]
1096 fn test_custom_msg_type_deserialize_accepts_valid_code() {
1097 let de: StrDeserializer<'_, ValueError> = "U=1".into_deserializer();
1098 match CustomMsgType::deserialize(de) {
1099 Ok(code) => assert_eq!(code.as_str(), "U=1"),
1100 Err(err) => panic!("a valid code must deserialize, got {err}"),
1101 }
1102 }
1103
1104 #[test]
1105 fn test_msg_type_error_converts_into_decode_error() {
1106 let err: DecodeError = MsgTypeError::Empty.into();
1107 assert_eq!(err, DecodeError::InvalidMsgType(MsgTypeError::Empty));
1108 }
1109
1110 #[test]
1111 fn test_msg_type_custom_equals_named_variant_with_same_wire_form() {
1112 let custom = custom_variant("D");
1113 assert_eq!(custom, MsgType::NewOrderSingle);
1114 assert_eq!(MsgType::NewOrderSingle, custom);
1115 assert_ne!(custom, MsgType::ExecutionReport);
1116 }
1117
1118 #[test]
1119 fn test_msg_type_equal_values_agree_on_is_admin() {
1120 // Equality follows the wire form, so classification must too — a
1121 // Custom("A") that routed as an application message while an equal
1122 // MsgType::Logon routed as admin would be a session-layer split brain.
1123 for variant in MsgType::all_named() {
1124 let as_custom = custom_variant(variant.as_str());
1125 assert_eq!(as_custom, variant);
1126 assert_eq!(
1127 as_custom.is_admin(),
1128 variant.is_admin(),
1129 "Custom({:?}) must classify like {variant:?}",
1130 variant.as_str()
1131 );
1132 }
1133 // A code no variant names stays an application message.
1134 assert!(!custom_variant("ZZ").is_admin());
1135 }
1136
1137 #[test]
1138 fn test_msg_type_hash_follows_wire_form() {
1139 fn hash_of(value: &MsgType) -> u64 {
1140 let mut hasher = DefaultHasher::new();
1141 value.hash(&mut hasher);
1142 hasher.finish()
1143 }
1144 let custom = custom_variant("D");
1145 assert_eq!(hash_of(&custom), hash_of(&MsgType::NewOrderSingle));
1146
1147 let mut map: HashMap<MsgType, u32> = HashMap::new();
1148 map.insert(MsgType::NewOrderSingle, 1);
1149 assert_eq!(map.get(&custom), Some(&1));
1150 }
1151
1152 /// Builds the field list for a one-field `RawMessage` over `buffer`.
1153 fn single_field(buffer: &[u8]) -> SmallVec<[FieldRef<'_>; 32]> {
1154 let mut fields = SmallVec::new();
1155 fields.push(FieldRef::new(8, buffer));
1156 fields
1157 }
1158
1159 #[test]
1160 fn test_raw_message_new_rejects_begin_string_range_past_buffer() {
1161 let buffer: &[u8] = b"8=FIX.4.4\x01";
1162 let fields = single_field(buffer);
1163 let result = RawMessage::new(buffer, 2..64, 0..0, MsgType::Heartbeat, fields);
1164 assert_eq!(
1165 result.err(),
1166 Some(DecodeError::RangeOutOfBounds {
1167 start: 2,
1168 end: 64,
1169 buffer_len: buffer.len(),
1170 })
1171 );
1172 }
1173
1174 #[test]
1175 fn test_raw_message_new_rejects_body_range_past_buffer() {
1176 let buffer: &[u8] = b"8=FIX.4.4\x01";
1177 let fields = single_field(buffer);
1178 let result = RawMessage::new(buffer, 2..9, 0..11, MsgType::Heartbeat, fields);
1179 assert!(matches!(
1180 result.err(),
1181 Some(DecodeError::RangeOutOfBounds { .. })
1182 ));
1183 }
1184
1185 #[test]
1186 fn test_raw_message_new_rejects_inverted_range() {
1187 let buffer: &[u8] = b"8=FIX.4.4\x01";
1188 let fields = single_field(buffer);
1189 // Built via the struct literal: `9..2` as a literal range trips
1190 // `clippy::reversed_empty_ranges` before it can reach the check.
1191 let inverted = Range { start: 9, end: 2 };
1192 let result = RawMessage::new(buffer, inverted, 0..0, MsgType::Heartbeat, fields);
1193 assert!(matches!(
1194 result.err(),
1195 Some(DecodeError::RangeOutOfBounds { .. })
1196 ));
1197 }
1198
1199 #[test]
1200 fn test_raw_message_begin_string_and_body_are_buffer_relative() {
1201 let buffer: &[u8] = b"8=FIX.4.4\x0135=0\x01";
1202 let fields = single_field(buffer);
1203 let Ok(msg) = RawMessage::new(buffer, 2..9, 10..15, MsgType::Heartbeat, fields) else {
1204 panic!("in-bounds ranges must be accepted");
1205 };
1206 assert_eq!(msg.begin_string(), Ok("FIX.4.4"));
1207 assert_eq!(msg.body(), Ok(&b"35=0\x01"[..]));
1208 assert_eq!(msg.len(), buffer.len());
1209 }
1210
1211 #[test]
1212 fn test_raw_message_begin_string_invalid_utf8_is_typed_error() {
1213 let buffer: &[u8] = b"8=\xff\xfe\x01";
1214 let fields = single_field(buffer);
1215 let Ok(msg) = RawMessage::new(buffer, 2..4, 0..0, MsgType::Heartbeat, fields) else {
1216 panic!("in-bounds ranges must be accepted");
1217 };
1218 assert!(matches!(
1219 msg.begin_string(),
1220 Err(DecodeError::InvalidUtf8(_))
1221 ));
1222 }
1223
1224 #[test]
1225 fn test_owned_message_get_field_out_of_bounds_offset_returns_none() {
1226 let buffer = Bytes::from_static(b"8=FIX.4.4\x01");
1227 let msg = OwnedMessage::new(buffer, MsgType::Heartbeat, vec![(8, 2..64)]);
1228 assert_eq!(msg.get_field(8), None);
1229 }
1230
1231 #[test]
1232 fn test_owned_message_from_raw_recovers_buffer_offsets() {
1233 let buffer: &[u8] = b"8=FIX.4.4\x0135=D\x0149=SENDER\x01";
1234 let mut fields: SmallVec<[FieldRef<'_>; 32]> = SmallVec::new();
1235 let Some(begin) = buffer.get(2..9) else {
1236 panic!("fixture buffer is long enough")
1237 };
1238 let Some(sender) = buffer.get(18..24) else {
1239 panic!("fixture buffer is long enough")
1240 };
1241 fields.push(FieldRef::new(8, begin));
1242 fields.push(FieldRef::new(49, sender));
1243 let Ok(raw) = RawMessage::new(buffer, 2..9, 10..24, MsgType::NewOrderSingle, fields) else {
1244 panic!("in-bounds ranges must be accepted");
1245 };
1246
1247 let owned = OwnedMessage::from_raw(&raw);
1248 assert_eq!(owned.field_count(), 2);
1249 assert_eq!(owned.get_field_str(8), Some("FIX.4.4"));
1250 assert_eq!(owned.get_field_str(49), Some("SENDER"));
1251 }
1252
1253 #[test]
1254 fn test_owned_message_from_raw_drops_field_not_borrowing_from_buffer() {
1255 let buffer: &[u8] = b"8=FIX.4.4\x0135=D\x01";
1256 // Deliberately borrowed from a different allocation: the offset of
1257 // this value inside `buffer` does not exist, and the old pointer
1258 // subtraction would have wrapped into a huge range.
1259 let foreign: &[u8] = b"FOREIGN";
1260 let mut fields: SmallVec<[FieldRef<'_>; 32]> = SmallVec::new();
1261 let Some(begin) = buffer.get(2..9) else {
1262 panic!("fixture buffer is long enough")
1263 };
1264 fields.push(FieldRef::new(8, begin));
1265 fields.push(FieldRef::new(49, foreign));
1266 let Ok(raw) = RawMessage::new(buffer, 2..9, 10..15, MsgType::NewOrderSingle, fields) else {
1267 panic!("in-bounds ranges must be accepted");
1268 };
1269
1270 let owned = OwnedMessage::from_raw(&raw);
1271 assert_eq!(owned.field_count(), 1);
1272 assert_eq!(owned.get_field_str(8), Some("FIX.4.4"));
1273 assert_eq!(owned.get_field(49), None);
1274 }
1275
1276 #[test]
1277 fn test_owned_message_field_access() {
1278 // Buffer: "8=FIX.4.4\x0135=D\x0149=SENDER\x01"
1279 // Offsets: 8=FIX.4.4 (0-9), \x01 (9), 35=D (10-13), \x01 (14), 49=SENDER (15-23), \x01 (24)
1280 // FIX.4.4 is at 2..9, D is at 13..14, SENDER is at 18..24
1281 let buffer = Bytes::from_static(b"8=FIX.4.4\x0135=D\x0149=SENDER\x01");
1282 let field_offsets = vec![(8, 2..9), (35, 13..14), (49, 18..24)];
1283 let msg = OwnedMessage::new(buffer, MsgType::NewOrderSingle, field_offsets);
1284
1285 assert_eq!(msg.get_field_str(8), Some("FIX.4.4"));
1286 assert_eq!(msg.get_field_str(35), Some("D"));
1287 assert_eq!(msg.get_field_str(49), Some("SENDER"));
1288 assert_eq!(msg.get_field_str(999), None);
1289 }
1290}