Skip to main content

fix_codec_rs/
encoder.rs

1use smallvec::SmallVec;
2
3use crate::checksum::compute_checksum;
4use crate::error::FixError;
5use crate::field::FIELD_SEPARATOR;
6use crate::message::Message;
7use crate::tag;
8
9/// Default inline capacity for the body buffer (bytes).
10/// Covers the body of most FIX messages without spilling to the heap.
11const DEFAULT_CAPACITY: usize = 512;
12
13/// A reusable FIX message encoder.
14///
15/// Owns a body buffer that is allocated once and reused across every `encode`
16/// call — zero allocation per message on the hot path after the first call.
17///
18/// # Example
19/// ```ignore
20/// let mut enc = Encoder::new();
21/// let mut out = Vec::new();
22/// enc.encode(&msg, &mut out)?;
23/// ```
24pub struct Encoder {
25    /// Reusable scratch buffer for building the message body.
26    /// Cleared (not dropped) at the start of each encode call so capacity is preserved.
27    body: SmallVec<[u8; DEFAULT_CAPACITY]>,
28    /// When true, tag 9 (BodyLength) is not auto-computed; the value from the
29    /// message is used as-is if present, otherwise the field is omitted.
30    disable_auto_calculate_body_length: bool,
31    /// When true, tag 10 (CheckSum) is not auto-computed; the value from the
32    /// message is used as-is if present, otherwise the field is omitted.
33    disable_auto_calculate_checksum: bool,
34}
35
36impl Default for Encoder {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl Encoder {
43    /// Create a new encoder with default inline body-buffer capacity.
44    pub fn new() -> Self {
45        Self {
46            body: SmallVec::new(),
47            disable_auto_calculate_body_length: false,
48            disable_auto_calculate_checksum: false,
49        }
50    }
51
52    /// Create a new encoder pre-allocated for `capacity` body bytes.
53    pub fn with_capacity(capacity: usize) -> Self {
54        Self {
55            body: SmallVec::with_capacity(capacity),
56            disable_auto_calculate_body_length: false,
57            disable_auto_calculate_checksum: false,
58        }
59    }
60
61    /// When set to `true`, tag 9 (BodyLength) will not be auto-computed.
62    /// If the message contains tag 9, its value is written as-is; otherwise
63    /// the field is omitted entirely.
64    ///
65    /// When `false` (the default), tag 9 is always computed from the body length.
66    pub fn disable_auto_calculate_body_length(&mut self, disable: bool) -> &mut Self {
67        self.disable_auto_calculate_body_length = disable;
68        self
69    }
70
71    /// When set to `true`, tag 10 (CheckSum) will not be auto-computed.
72    /// If the message contains tag 10, its value is written as-is; otherwise
73    /// the field is omitted entirely.
74    ///
75    /// When `false` (the default), tag 10 is always computed from the message bytes.
76    pub fn disable_auto_calculate_checksum(&mut self, disable: bool) -> &mut Self {
77        self.disable_auto_calculate_checksum = disable;
78        self
79    }
80
81    /// Encode `msg` as a complete FIX wire message into `out`.
82    ///
83    /// `out` is cleared first. By default, tag 9 (BodyLength) and tag 10 (CheckSum)
84    /// are computed automatically and any existing 9 or 10 fields in `msg` are ignored.
85    /// Use `disable_auto_calculate_body_length(true)` or
86    /// `disable_auto_calculate_checksum(true)` to write the message's own values instead.
87    /// If tag 8 (BeginString) is absent, `FIX.4.4` is used as the default version.
88    pub fn encode(&mut self, msg: &Message<'_>, out: &mut Vec<u8>) -> Result<(), FixError> {
89        const DEFAULT_VERSION: &[u8] = b"FIX.4.4";
90        let version = msg
91            .find(tag::BEGIN_STRING)
92            .map(|f| f.value)
93            .unwrap_or(DEFAULT_VERSION);
94
95        // Build body bytes into reusable scratch buffer (all fields except 8, 9, 10).
96        self.body.clear();
97        for field in msg.fields() {
98            if field.tag == tag::BEGIN_STRING
99                || field.tag == tag::BODY_LENGTH
100                || field.tag == tag::CHECK_SUM
101            {
102                continue;
103            }
104            let (digits, pos) = u32_to_ascii(field.tag);
105            self.body.extend_from_slice(&digits[pos..]);
106            self.body.push(b'=');
107            self.body.extend_from_slice(field.value);
108            self.body.push(FIELD_SEPARATOR);
109        }
110
111        // Assemble output: tag 8, tag 9, body, tag 10.
112        out.clear();
113
114        out.extend_from_slice(b"8=");
115        out.extend_from_slice(version);
116        out.push(FIELD_SEPARATOR);
117
118        if self.disable_auto_calculate_body_length {
119            if let Some(f) = msg.find(tag::BODY_LENGTH) {
120                out.extend_from_slice(b"9=");
121                out.extend_from_slice(f.value);
122                out.push(FIELD_SEPARATOR);
123            }
124        } else {
125            out.extend_from_slice(b"9=");
126            let (digits, pos) = u32_to_ascii(self.body.len() as u32);
127            out.extend_from_slice(&digits[pos..]);
128            out.push(FIELD_SEPARATOR);
129        }
130
131        out.extend_from_slice(&self.body);
132
133        if self.disable_auto_calculate_checksum {
134            if let Some(f) = msg.find(tag::CHECK_SUM) {
135                out.extend_from_slice(b"10=");
136                out.extend_from_slice(f.value);
137                out.push(FIELD_SEPARATOR);
138            }
139        } else {
140            let checksum = compute_checksum(out);
141            out.extend_from_slice(b"10=");
142            out.extend_from_slice(&checksum_to_ascii(checksum));
143            out.push(FIELD_SEPARATOR);
144        }
145
146        Ok(())
147    }
148}
149
150/// Write the decimal digits of `n` (no leading zeros) into `buf` as ASCII bytes.
151/// Uses a stack buffer — no heap allocation.
152#[inline]
153fn u32_to_ascii(n: u32) -> ([u8; 10], usize) {
154    let mut buf = [0u8; 10];
155    let mut pos = 10usize;
156    let mut v = n;
157    loop {
158        pos -= 1;
159        buf[pos] = b'0' + (v % 10) as u8;
160        v /= 10;
161        if v == 0 {
162            break;
163        }
164    }
165    (buf, pos)
166}
167
168/// Write `n` as a zero-padded 3-digit ASCII decimal (for FIX CheckSum tag 10).
169/// No heap allocation.
170#[inline]
171fn checksum_to_ascii(n: u8) -> [u8; 3] {
172    [b'0' + n / 100, b'0' + (n / 10) % 10, b'0' + n % 10]
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::decoder::Decoder;
179
180    #[test]
181    fn encode_single_body_field() {
182        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
183        let mut dec = Decoder::new();
184        let msg = dec.decode(raw).unwrap();
185        let mut enc = Encoder::new();
186        let mut out = Vec::new();
187        enc.encode(&msg, &mut out).unwrap();
188        assert_eq!(out.as_slice(), raw.as_ref());
189    }
190
191    #[test]
192    fn encode_validates_body_length_and_checksum() {
193        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
194        let mut dec = Decoder::new();
195        let msg = dec.decode(raw).unwrap();
196        let mut enc = Encoder::new();
197        let mut out = Vec::new();
198        enc.encode(&msg, &mut out).unwrap();
199        let mut dec2 = Decoder::new();
200        let msg2 = dec2.decode(&out).unwrap();
201        assert!(msg2.validate_body_length().is_ok());
202        assert!(msg2.validate_checksum().is_ok());
203    }
204
205    #[test]
206    fn encode_multiple_body_fields() {
207        let raw = b"8=FIX.4.2\x019=20\x0135=D\x0149=SENDER\x0156=TARGET\x0110=100\x01";
208        let mut dec = Decoder::new();
209        let msg = dec.decode(raw).unwrap();
210        let mut enc = Encoder::new();
211        let mut out = Vec::new();
212        enc.encode(&msg, &mut out).unwrap();
213        let mut dec2 = Decoder::new();
214        let msg2 = dec2.decode(&out).unwrap();
215        assert!(msg2.validate_body_length().is_ok());
216        assert!(msg2.validate_checksum().is_ok());
217    }
218
219    #[test]
220    fn encode_missing_tag8_defaults_to_fix44() {
221        let raw = b"35=D\x01";
222        let mut dec = Decoder::new();
223        let msg = dec.decode(raw).unwrap();
224        let mut enc = Encoder::new();
225        let mut out = Vec::new();
226        enc.encode(&msg, &mut out).unwrap();
227        assert!(out.starts_with(b"8=FIX.4.4\x01"));
228        let mut dec2 = Decoder::new();
229        let msg2 = dec2.decode(&out).unwrap();
230        assert!(msg2.validate_body_length().is_ok());
231        assert!(msg2.validate_checksum().is_ok());
232    }
233
234    #[test]
235    fn encode_clears_existing_out_buffer() {
236        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
237        let mut dec = Decoder::new();
238        let msg = dec.decode(raw).unwrap();
239        let mut enc = Encoder::new();
240        let mut out = b"stale_data".to_vec();
241        enc.encode(&msg, &mut out).unwrap();
242        assert!(!out.starts_with(b"stale_data"));
243        assert!(out.starts_with(b"8="));
244    }
245
246    #[test]
247    fn encode_reuse_encode_twice() {
248        // Encoder reuse: body buffer capacity is preserved across calls.
249        let raw1 = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
250        let raw2 = b"8=FIX.4.2\x019=20\x0135=D\x0149=SENDER\x0156=TARGET\x0110=100\x01";
251        let mut dec = Decoder::new();
252        let mut enc = Encoder::new();
253        let mut out = Vec::new();
254
255        let msg1 = dec.decode(raw1).unwrap();
256        enc.encode(&msg1, &mut out).unwrap();
257        let encoded1 = out.clone();
258
259        let msg2 = dec.decode(raw2).unwrap();
260        enc.encode(&msg2, &mut out).unwrap();
261
262        // First result was correct.
263        let mut dec2 = Decoder::new();
264        let m1 = dec2.decode(&encoded1).unwrap();
265        assert!(m1.validate_body_length().is_ok());
266        assert!(m1.validate_checksum().is_ok());
267
268        // Second result is correct.
269        let m2 = dec2.decode(&out).unwrap();
270        assert!(m2.validate_body_length().is_ok());
271        assert!(m2.validate_checksum().is_ok());
272    }
273
274    #[test]
275    fn encode_disable_auto_body_length_uses_message_value() {
276        // Tag 9 value from the message is preserved when auto-calculation is disabled.
277        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
278        let mut dec = Decoder::new();
279        let msg = dec.decode(raw).unwrap();
280        let mut enc = Encoder::new();
281        enc.disable_auto_calculate_body_length(true);
282        let mut out = Vec::new();
283        enc.encode(&msg, &mut out).unwrap();
284        // "9=5\x01" should appear verbatim from the message.
285        assert!(out.windows(4).any(|w| w == b"9=5\x01"));
286    }
287
288    #[test]
289    fn encode_disable_auto_body_length_omits_tag9_when_absent() {
290        // When tag 9 is absent from the message and auto-calculation is disabled,
291        // tag 9 should not appear in the output at all.
292        let raw = b"35=D\x01";
293        let mut dec = Decoder::new();
294        let msg = dec.decode(raw).unwrap();
295        let mut enc = Encoder::new();
296        enc.disable_auto_calculate_body_length(true);
297        let mut out = Vec::new();
298        enc.encode(&msg, &mut out).unwrap();
299        assert!(!out.windows(2).any(|w| w == b"9="));
300    }
301
302    #[test]
303    fn encode_disable_auto_checksum_uses_message_value() {
304        // Tag 10 value from the message is preserved when auto-calculation is disabled.
305        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
306        let mut dec = Decoder::new();
307        let msg = dec.decode(raw).unwrap();
308        let mut enc = Encoder::new();
309        enc.disable_auto_calculate_checksum(true);
310        let mut out = Vec::new();
311        enc.encode(&msg, &mut out).unwrap();
312        // "10=181\x01" should appear verbatim from the message.
313        assert!(out.windows(7).any(|w| w == b"10=181\x01"));
314    }
315
316    #[test]
317    fn encode_disable_auto_checksum_omits_tag10_when_absent() {
318        // When tag 10 is absent from the message and auto-calculation is disabled,
319        // tag 10 should not appear in the output at all.
320        let raw = b"35=D\x01";
321        let mut dec = Decoder::new();
322        let msg = dec.decode(raw).unwrap();
323        let mut enc = Encoder::new();
324        enc.disable_auto_calculate_checksum(true);
325        let mut out = Vec::new();
326        enc.encode(&msg, &mut out).unwrap();
327        assert!(!out.windows(3).any(|w| w == b"10="));
328    }
329
330    #[test]
331    fn encode_disable_both_preserves_original_bytes() {
332        // With both flags set, the output should round-trip the original wire bytes.
333        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
334        let mut dec = Decoder::new();
335        let msg = dec.decode(raw).unwrap();
336        let mut enc = Encoder::new();
337        enc.disable_auto_calculate_body_length(true);
338        enc.disable_auto_calculate_checksum(true);
339        let mut out = Vec::new();
340        enc.encode(&msg, &mut out).unwrap();
341        assert_eq!(out.as_slice(), raw.as_ref());
342    }
343
344    #[test]
345    fn encode_disable_body_length_re_enable_auto_calculates_correctly() {
346        // Toggling disable back to false restores auto-computation.
347        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
348        let mut dec = Decoder::new();
349        let msg = dec.decode(raw).unwrap();
350        let mut enc = Encoder::new();
351        enc.disable_auto_calculate_body_length(true);
352        enc.disable_auto_calculate_body_length(false);
353        let mut out = Vec::new();
354        enc.encode(&msg, &mut out).unwrap();
355        let mut dec2 = Decoder::new();
356        let msg2 = dec2.decode(&out).unwrap();
357        assert!(msg2.validate_body_length().is_ok());
358    }
359
360    #[test]
361    fn encode_disable_checksum_re_enable_auto_calculates_correctly() {
362        // Toggling disable back to false restores auto-computation.
363        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
364        let mut dec = Decoder::new();
365        let msg = dec.decode(raw).unwrap();
366        let mut enc = Encoder::new();
367        enc.disable_auto_calculate_checksum(true);
368        enc.disable_auto_calculate_checksum(false);
369        let mut out = Vec::new();
370        enc.encode(&msg, &mut out).unwrap();
371        let mut dec2 = Decoder::new();
372        let msg2 = dec2.decode(&out).unwrap();
373        assert!(msg2.validate_checksum().is_ok());
374    }
375
376    #[test]
377    fn encode_disable_body_length_passes_wrong_value_verbatim() {
378        // When auto-calculation is disabled, a wrong tag 9 value is written as-is
379        // and validate_body_length should fail on the output.
380        let raw = b"8=FIX.4.2\x019=999\x0135=D\x0110=181\x01";
381        let mut dec = Decoder::new();
382        let msg = dec.decode(raw).unwrap();
383        let mut enc = Encoder::new();
384        enc.disable_auto_calculate_body_length(true);
385        let mut out = Vec::new();
386        enc.encode(&msg, &mut out).unwrap();
387        assert!(out.windows(6).any(|w| w == b"9=999\x01"));
388        let mut dec2 = Decoder::new();
389        let msg2 = dec2.decode(&out).unwrap();
390        assert!(msg2.validate_body_length().is_err());
391    }
392
393    #[test]
394    fn encode_disable_checksum_passes_wrong_value_verbatim() {
395        // When auto-calculation is disabled, a wrong tag 10 value is written as-is
396        // and validate_checksum should fail on the output.
397        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=000\x01";
398        let mut dec = Decoder::new();
399        let msg = dec.decode(raw).unwrap();
400        let mut enc = Encoder::new();
401        enc.disable_auto_calculate_checksum(true);
402        let mut out = Vec::new();
403        enc.encode(&msg, &mut out).unwrap();
404        assert!(out.windows(7).any(|w| w == b"10=000\x01"));
405        let mut dec2 = Decoder::new();
406        let msg2 = dec2.decode(&out).unwrap();
407        assert!(msg2.validate_checksum().is_err());
408    }
409
410    #[test]
411    fn encode_auto_body_length_ignores_wrong_value_in_message() {
412        // Auto-computation overwrites a wrong tag 9 in the message; output validates.
413        let raw = b"8=FIX.4.2\x019=999\x0135=D\x0110=181\x01";
414        let mut dec = Decoder::new();
415        let msg = dec.decode(raw).unwrap();
416        let mut enc = Encoder::new();
417        // disable flag is false by default — auto-compute should kick in.
418        let mut out = Vec::new();
419        enc.encode(&msg, &mut out).unwrap();
420        let mut dec2 = Decoder::new();
421        let msg2 = dec2.decode(&out).unwrap();
422        assert!(msg2.validate_body_length().is_ok());
423    }
424
425    #[test]
426    fn encode_auto_checksum_ignores_wrong_value_in_message() {
427        // Auto-computation overwrites a wrong tag 10 in the message; output validates.
428        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=000\x01";
429        let mut dec = Decoder::new();
430        let msg = dec.decode(raw).unwrap();
431        let mut enc = Encoder::new();
432        // disable flag is false by default — auto-compute should kick in.
433        let mut out = Vec::new();
434        enc.encode(&msg, &mut out).unwrap();
435        let mut dec2 = Decoder::new();
436        let msg2 = dec2.decode(&out).unwrap();
437        assert!(msg2.validate_checksum().is_ok());
438    }
439
440    #[test]
441    fn encode_disable_body_length_only_does_not_affect_checksum() {
442        // Disabling body length auto-calc leaves checksum auto-computed correctly.
443        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
444        let mut dec = Decoder::new();
445        let msg = dec.decode(raw).unwrap();
446        let mut enc = Encoder::new();
447        enc.disable_auto_calculate_body_length(true);
448        let mut out = Vec::new();
449        enc.encode(&msg, &mut out).unwrap();
450        // tag 9 came from the message verbatim
451        assert!(out.windows(4).any(|w| w == b"9=5\x01"));
452        // tag 10 was auto-computed over the actual output bytes
453        let mut dec2 = Decoder::new();
454        let msg2 = dec2.decode(&out).unwrap();
455        assert!(msg2.validate_checksum().is_ok());
456    }
457
458    #[test]
459    fn encode_disable_checksum_only_does_not_affect_body_length() {
460        // Disabling checksum auto-calc leaves body length auto-computed correctly.
461        let raw = b"8=FIX.4.2\x019=5\x0135=D\x0110=181\x01";
462        let mut dec = Decoder::new();
463        let msg = dec.decode(raw).unwrap();
464        let mut enc = Encoder::new();
465        enc.disable_auto_calculate_checksum(true);
466        let mut out = Vec::new();
467        enc.encode(&msg, &mut out).unwrap();
468        // tag 10 came from the message verbatim
469        assert!(out.windows(7).any(|w| w == b"10=181\x01"));
470        // tag 9 was auto-computed correctly
471        let mut dec2 = Decoder::new();
472        let msg2 = dec2.decode(&out).unwrap();
473        assert!(msg2.validate_body_length().is_ok());
474    }
475}