fix_codec_rs/message.rs
1use std::cell::OnceCell;
2
3use smallvec::SmallVec;
4
5use crate::body_length::parse_body_length;
6use crate::checksum::{compute_checksum, parse_checksum};
7use crate::error::FixError;
8use crate::field::Field;
9use crate::group::{FIX42_GROUPS, FIX44_GROUPS, GroupIter, GroupSpec, parse_count};
10use crate::tag::{self, Tag};
11
12/// Default inline capacity for the sorted index — matches the decoder's field capacity.
13const SORTED_CAPACITY: usize = 32;
14
15/// A decoded FIX message.
16///
17/// Zero-copy: field values are sub-slices of the original input buffer — no
18/// bytes are copied when accessing fields.
19///
20/// The sorted tag index for [`find`] is built lazily on the first call and
21/// cached for the lifetime of the message. This means `decode()` pays no sort
22/// cost when you never call `find()`, and pays it at most once when you do.
23#[derive(Debug)]
24pub struct Message<'a> {
25 /// The raw bytes of the complete FIX message as received (e.g. the network
26 /// buffer passed to `Decoder::decode`). Every field value is a sub-slice of
27 /// this buffer — no bytes are copied when accessing fields.
28 pub(crate) buf: &'a [u8],
29
30 /// Index of parsed fields. Each entry is `(tag, start, end)` where:
31 /// - `tag` — the numeric FIX tag (e.g. `8`, `35`, `49`).
32 /// - `start` — byte offset in `buf` where the field *value* begins
33 /// (the byte immediately after `=`).
34 /// - `end` — byte offset in `buf` where the field value ends
35 /// (the SOH byte `\x01`, exclusive).
36 ///
37 /// A field value is recovered as `&buf[start as usize..end as usize]`.
38 /// The slice is borrowed from the `Decoder`'s internal `SmallVec`, so it
39 /// lives as long as `'a`.
40 pub(crate) offsets: &'a [(Tag, u32, u32)],
41
42 /// Sorted (tag, offsets_index) pairs for O(log n) binary search in find().
43 ///
44 /// Built lazily on the first call to `find()` and cached for the lifetime
45 /// of the message via `OnceCell`. Never allocated if `find()` is never
46 /// called, and built at most once regardless of how many times `find()` is
47 /// called.
48 sorted: OnceCell<SmallVec<[(Tag, u16); SORTED_CAPACITY]>>,
49}
50
51impl<'a> Message<'a> {
52 /// Create a new message from a buffer and an offset slice.
53 /// The sorted index starts uninitialized and is built lazily on first find().
54 pub(crate) fn new(buf: &'a [u8], offsets: &'a [(Tag, u32, u32)]) -> Self {
55 Self {
56 buf,
57 offsets,
58 sorted: OnceCell::new(),
59 }
60 }
61
62 /// Number of fields in the message.
63 #[inline]
64 pub fn len(&self) -> usize {
65 self.offsets.len()
66 }
67
68 /// Returns true if the message contains no fields.
69 #[inline]
70 pub fn is_empty(&self) -> bool {
71 self.offsets.is_empty()
72 }
73
74 /// Returns the field at `index`, reconstructing it zero-copy from the
75 /// stored byte offsets. Panics if `index >= self.len()`.
76 #[inline]
77 pub fn field(&self, index: usize) -> Field<'a> {
78 let (tag, start, end) = self.offsets[index];
79 Field {
80 tag,
81 value: &self.buf[start as usize..end as usize],
82 }
83 }
84
85 /// Returns an iterator over all fields, reconstructing each `Field<'a>`
86 /// zero-copy on demand.
87 #[inline]
88 pub fn fields(&self) -> impl Iterator<Item = Field<'a>> + '_ {
89 self.offsets.iter().map(move |&(tag, start, end)| Field {
90 tag,
91 value: &self.buf[start as usize..end as usize],
92 })
93 }
94
95 /// Return the value of tag 8 (`BEGIN_STRING`) as a byte slice, or `None`
96 /// if the field is absent.
97 ///
98 /// Common values are `b"FIX.4.2"`, `b"FIX.4.4"`, `b"FIXT.1.1"`, etc.
99 #[inline]
100 pub fn fix_version(&self) -> Option<&'a [u8]> {
101 self.find(tag::BEGIN_STRING).map(|f| f.value)
102 }
103
104 /// Find the first field with the given tag, or `None` if not present.
105 ///
106 /// The sorted index is built lazily on the first call (O(n log n)) and
107 /// cached for subsequent calls (O(log n) binary search). If `find()` is
108 /// never called, the sort never happens.
109 #[inline]
110 pub fn find(&self, tag: Tag) -> Option<Field<'a>> {
111 let sorted = self.sorted.get_or_init(|| {
112 let mut v: SmallVec<[(Tag, u16); SORTED_CAPACITY]> =
113 SmallVec::with_capacity(self.offsets.len());
114 for (i, &(t, _, _)) in self.offsets.iter().enumerate() {
115 v.push((t, i as u16));
116 }
117 v.sort_unstable_by_key(|&(t, _)| t);
118 v
119 });
120
121 let idx = sorted.partition_point(|&(t, _)| t < tag);
122 let &(found_tag, offset_idx) = sorted.get(idx)?;
123 if found_tag != tag {
124 return None;
125 }
126 let (t, start, end) = self.offsets[offset_idx as usize];
127 Some(Field {
128 tag: t,
129 value: &self.buf[start as usize..end as usize],
130 })
131 }
132
133 /// Return an iterator over the instances of the repeating group described
134 /// by `spec`.
135 ///
136 /// The iterator is zero-copy: each `Group` borrows directly into this
137 /// message's offset slice and raw buffer. If the count tag is absent or
138 /// its value is `0`, the iterator yields nothing.
139 ///
140 /// # Example
141 /// ```ignore
142 /// for entry in msg.groups(&group::MD_ENTRIES) {
143 /// let ty = entry.find(tag::MD_ENTRY_TYPE);
144 /// let px = entry.find(tag::MD_ENTRY_PX);
145 /// }
146 /// ```
147 #[inline]
148 pub fn groups(&self, spec: &GroupSpec) -> GroupIter<'a> {
149 // Find the NO_* count tag position.
150 let pos = self
151 .offsets
152 .iter()
153 .position(|&(t, _, _)| t == spec.count_tag);
154
155 let (count, remaining) = match pos {
156 None => (0, &[][..]),
157 Some(i) => {
158 let (_, start, end) = self.offsets[i];
159 let count = parse_count(&self.buf[start as usize..end as usize]);
160 let after = &self.offsets[i + 1..];
161 (count, after)
162 }
163 };
164
165 GroupIter {
166 buf: self.buf,
167 remaining,
168 delimiter_tag: spec.delimiter_tag,
169 count,
170 emitted: 0,
171 }
172 }
173
174 /// Return an iterator over every repeating group present in this message.
175 ///
176 /// Scans the appropriate group spec array based on the FIX version detected
177 /// from tag 8 (`BEGIN_STRING`): `FIX42_GROUPS` for FIX 4.2 messages, and
178 /// both `FIX42_GROUPS` + `FIX44_GROUPS` for FIX 4.4 messages (which is a
179 /// superset). Yields `(&'static GroupSpec, GroupIter<'a>)` for each spec
180 /// whose count tag is found in the message with a non-zero count. Groups
181 /// whose count tag is absent or zero are skipped.
182 ///
183 /// The order follows the order of the spec arrays, not the order fields
184 /// appear in the message.
185 ///
186 /// # Example
187 /// ```ignore
188 /// for (spec, instances) in msg.all_groups() {
189 /// for g in instances {
190 /// // process each group instance
191 /// }
192 /// }
193 /// ```
194 /// Validate the BodyLength field (tag 9).
195 ///
196 /// A FIX message body spans from the first byte after the `9=…\x01` field
197 /// up to and including the SOH that terminates the last field before `10=`.
198 /// This method computes that byte count from the raw buffer and compares it
199 /// to the value declared in tag 9.
200 ///
201 /// # Errors
202 /// Returns `FixError::InvalidBodyLength` when:
203 /// - The message has fewer than 3 fields (no room for tags 8, 9, and 10).
204 /// - Tag 9 is not at position 1 or its value cannot be parsed as an integer.
205 /// - Tag 10 is not the last field.
206 /// - The computed byte count does not match the declared value.
207 pub fn validate_body_length(&self) -> Result<(), FixError> {
208 let n = self.offsets.len();
209 if n < 3 {
210 return Err(FixError::InvalidBodyLength);
211 }
212
213 // Tag 9 must be the second field.
214 let (tag9, _, body_length_value_end) = self.offsets[1];
215 if tag9 != tag::BODY_LENGTH {
216 return Err(FixError::InvalidBodyLength);
217 }
218
219 // Tag 10 must be the last field.
220 let (tag10, checksum_value_start, _) = self.offsets[n - 1];
221 if tag10 != tag::CHECK_SUM {
222 return Err(FixError::InvalidBodyLength);
223 }
224
225 // Parse the declared body length from the raw buffer.
226 let declared = parse_body_length(
227 &self.buf[self.offsets[1].1 as usize..body_length_value_end as usize],
228 )
229 .ok_or(FixError::InvalidBodyLength)?;
230
231 // Body bytes: from (SOH of tag-9 field + 1) to (start of "10=" tag bytes).
232 // "10=" is 3 bytes, so the tag-10 field starts at checksum_value_start - 3.
233 let body_start = body_length_value_end as usize + 1;
234 let checksum_tag_start = checksum_value_start as usize - 3; // len("10=") == 3
235 let computed = checksum_tag_start.saturating_sub(body_start);
236
237 if computed == declared {
238 Ok(())
239 } else {
240 Err(FixError::InvalidBodyLength)
241 }
242 }
243
244 /// Validate the CheckSum field (tag 10).
245 ///
246 /// The FIX checksum is the sum of every byte from the start of the buffer
247 /// up to (but not including) the `10=` tag bytes, taken mod 256. This
248 /// method computes that value and compares it to the 3-digit decimal string
249 /// stored in tag 10.
250 ///
251 /// # Errors
252 /// Returns `FixError::InvalidCheckSum` when:
253 /// - The message has fewer than 1 field.
254 /// - Tag 10 is not the last field or its value cannot be parsed.
255 /// - The computed checksum does not match the declared value.
256 pub fn validate_checksum(&self) -> Result<(), FixError> {
257 let n = self.offsets.len();
258 if n == 0 {
259 return Err(FixError::InvalidCheckSum);
260 }
261
262 // Tag 10 must be the last field.
263 let (tag10, checksum_value_start, checksum_value_end) = self.offsets[n - 1];
264 if tag10 != tag::CHECK_SUM {
265 return Err(FixError::InvalidCheckSum);
266 }
267
268 // Parse the declared checksum from the raw buffer.
269 let declared =
270 parse_checksum(&self.buf[checksum_value_start as usize..checksum_value_end as usize])
271 .ok_or(FixError::InvalidCheckSum)?;
272
273 // Checksum covers all bytes before the "10=" tag bytes.
274 let checksum_tag_start = checksum_value_start as usize - 3; // len("10=") == 3
275 let computed = compute_checksum(&self.buf[..checksum_tag_start]);
276
277 if computed == declared {
278 Ok(())
279 } else {
280 Err(FixError::InvalidCheckSum)
281 }
282 }
283
284 #[inline]
285 pub fn all_groups(&self) -> impl Iterator<Item = (&'static GroupSpec, GroupIter<'a>)> + '_ {
286 let specs: &[&GroupSpec] = match self.fix_version() {
287 Some(b"FIX.4.4") => FIX44_GROUPS,
288 _ => FIX42_GROUPS,
289 };
290
291 specs.iter().copied().filter_map(|spec| {
292 // Check if the count tag is present with a non-zero count.
293 let found = self.offsets.iter().find(|&&(t, _, _)| t == spec.count_tag);
294 let &(_, start, end) = found?;
295 let count = parse_count(&self.buf[start as usize..end as usize]);
296 if count == 0 {
297 return None;
298 }
299 Some((spec, self.groups(spec)))
300 })
301 }
302}