Skip to main content

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}