truefix_core/codec/encode.rs
1//! Encode a [`Message`] to wire bytes, computing BodyLength (tag 9) and CheckSum (tag 10).
2
3use crate::field_map::{FieldMap, Member};
4use crate::message::Message;
5use crate::tags::{BEGIN_STRING, BODY_LENGTH, CHECK_SUM, MSG_TYPE, SOH};
6
7/// Encode `msg` to SOH-delimited wire bytes.
8///
9/// Emits the canonical order `8, 9, 35, <rest of header>, <body>, <trailer except 10>, 10`.
10/// BodyLength is the byte count of everything after the `9=..<SOH>` up to and including the
11/// `<SOH>` before `10=`; CheckSum is the sum of all preceding bytes modulo 256.
12pub fn encode(msg: &Message) -> Vec<u8> {
13 encode_with_order(msg, None)
14}
15
16/// As [`encode`], but when `field_order` is `Some`, the message body's top-level fields are
17/// emitted in that tag order instead of insertion order (US9, feature 005, FR-027) — fields
18/// present in `field_order` come first (in that order), then any body field not listed in it
19/// (e.g. a UDF), in its original insertion-relative order, matching QFJ's own
20/// `FieldOrderComparator` "unspecified fields last" semantics. Repeating-group entries are
21/// unaffected (each entry's own field order is unconditionally preserved) — `field_order` only
22/// reorders the message body's direct top-level members.
23pub fn encode_with_order(msg: &Message, field_order: Option<&[u32]>) -> Vec<u8> {
24 let begin = msg
25 .header
26 .get(BEGIN_STRING)
27 .map(|f| f.value_bytes().to_vec())
28 .unwrap_or_default();
29 let msg_type = msg
30 .header
31 .get(MSG_TYPE)
32 .map(|f| f.value_bytes().to_vec())
33 .unwrap_or_default();
34
35 // Everything counted by BodyLength: MsgType, the rest of the header, the body, and the
36 // trailer (excluding CheckSum).
37 let mut middle = Vec::new();
38 render_raw(MSG_TYPE, &msg_type, &mut middle);
39 render_members(
40 &msg.header,
41 &[BEGIN_STRING, BODY_LENGTH, MSG_TYPE],
42 &mut middle,
43 );
44 match field_order {
45 Some(order) => render_members_ordered(&msg.body, order, &mut middle),
46 None => render_members(&msg.body, &[], &mut middle),
47 }
48 render_members(&msg.trailer, &[CHECK_SUM], &mut middle);
49
50 let mut out = Vec::new();
51 render_raw(BEGIN_STRING, &begin, &mut out);
52 render_raw(BODY_LENGTH, middle.len().to_string().as_bytes(), &mut out);
53 out.extend_from_slice(&middle);
54
55 // BUG-24/FR-032 (feature 007): a `u64` accumulator, not `u32` — `Iterator::sum::<u32>()` panics
56 // on overflow in debug builds (violating the crate's "no path panics" invariant) once the
57 // summed bytes exceed ~16.8M in a way whose sum surpasses `u32::MAX` (reachable: `encode()` has
58 // no `MAX_BODY_LEN`-style cap of its own, since that limit is enforced only on the *decode*
59 // path in `frame_length`). `u64` can't realistically overflow this sum (would need billions of
60 // bytes), and `& 0xFF` gives the identical mod-256 result either width, since 2^32 and 2^64 are
61 // both multiples of 256.
62 let checksum: u32 = (out.iter().map(|&b| u64::from(b)).sum::<u64>() & 0xFF) as u32;
63 render_raw(CHECK_SUM, format!("{checksum:03}").as_bytes(), &mut out);
64 out
65}
66
67/// Render `map`'s top-level members ordered by `order` (fields listed in `order` first, in that
68/// order; then any remaining top-level member — a field not in `order`, or a group — in its
69/// original relative position among the remaining members). Each group entry's own internal
70/// field order is untouched.
71fn render_members_ordered(map: &FieldMap, order: &[u32], out: &mut Vec<u8>) {
72 let members: &[Member] = map.raw_members();
73 let tag_of = |m: &Member| match m {
74 Member::Field(f) => Some(f.tag()),
75 Member::Group { count_tag, .. } => Some(*count_tag),
76 };
77 // NEW-80 (feature 009): `order` (a dictionary's `field_order`) may itself list the same tag
78 // more than once -- track already-emitted tags so a duplicate entry in `order` doesn't render
79 // its member twice.
80 let mut emitted: std::collections::HashSet<u32> = std::collections::HashSet::new();
81 for &wanted in order {
82 if !emitted.insert(wanted) {
83 continue;
84 }
85 for member in members {
86 if tag_of(member) == Some(wanted) {
87 render_one_member(member, out);
88 }
89 }
90 }
91 for member in members {
92 let tag = tag_of(member).unwrap_or(0);
93 if !emitted.contains(&tag) {
94 render_one_member(member, out);
95 }
96 }
97}
98
99/// NEW-22 (feature 009): the count to emit for a group's `NoXxx` field — its wire-declared count
100/// if one was recorded (preserving fidelity to a decoded message even when that declaration
101/// didn't match the actual entry count), otherwise `entries.len()` (today's behavior, unchanged
102/// for any group not decoded from the wire).
103fn group_count_to_emit(entries: &[FieldMap], declared_count: Option<i64>) -> String {
104 declared_count.map_or_else(|| entries.len().to_string(), |n| n.to_string())
105}
106
107fn render_one_member(member: &Member, out: &mut Vec<u8>) {
108 match member {
109 Member::Field(f) => render_raw(f.tag(), f.value_bytes(), out),
110 Member::Group {
111 count_tag,
112 entries,
113 declared_count,
114 } => {
115 render_raw(
116 *count_tag,
117 group_count_to_emit(entries, *declared_count).as_bytes(),
118 out,
119 );
120 for entry in entries {
121 render_members(entry, &[], out);
122 }
123 }
124 }
125}
126
127fn render_members(map: &FieldMap, skip: &[u32], out: &mut Vec<u8>) {
128 for member in map.raw_members() {
129 match member {
130 Member::Field(f) => {
131 if !skip.contains(&f.tag()) {
132 render_raw(f.tag(), f.value_bytes(), out);
133 }
134 }
135 Member::Group {
136 count_tag,
137 entries,
138 declared_count,
139 } => {
140 render_raw(
141 *count_tag,
142 group_count_to_emit(entries, *declared_count).as_bytes(),
143 out,
144 );
145 for entry in entries {
146 render_members(entry, &[], out);
147 }
148 }
149 }
150 }
151}
152
153fn render_raw(tag: u32, value: &[u8], out: &mut Vec<u8>) {
154 write_tag(tag, out);
155 out.push(b'=');
156 out.extend_from_slice(value);
157 out.push(SOH);
158}
159
160/// T177/T178 (feature 009, NEW-35): write `tag`'s decimal digits directly into `out`, avoiding
161/// the per-field heap allocation `tag.to_string()` made on every rendered field.
162fn write_tag(tag: u32, out: &mut Vec<u8>) {
163 let mut buf = [0u8; 10]; // u32::MAX ("4294967295") is 10 digits
164 let mut i = buf.len();
165 let mut n = tag;
166 loop {
167 i -= 1;
168 if let Some(slot) = buf.get_mut(i) {
169 *slot = b'0' + (n % 10) as u8;
170 }
171 n /= 10;
172 if n == 0 || i == 0 {
173 break;
174 }
175 }
176 if let Some(digits) = buf.get(i..) {
177 out.extend_from_slice(digits);
178 }
179}