1use anyhow::{bail, ensure, Context, Result};
5
6pub const SOH: u8 = 0x01;
7pub const MAX_BODY: usize = 256 * 1024;
9pub const MAX_FIELDS: usize = 2000;
10
11const DATA_PAIRS: &[(u32, u32)] = &[
13 (90, 91),
14 (93, 89),
15 (95, 96),
16 (212, 213),
17 (348, 349),
18 (350, 351),
19 (352, 353),
20 (354, 355),
21 (356, 357),
22 (358, 359),
23 (360, 361),
24 (362, 363),
25 (364, 365),
26 (445, 446),
27 (618, 619),
28 (621, 622),
29];
30
31#[derive(Debug, Clone, PartialEq)]
32pub struct Message {
33 pub fields: Vec<(u32, String)>,
35}
36
37impl Message {
38 pub fn get(&self, tag: u32) -> Option<&str> {
39 self.fields
40 .iter()
41 .find(|(t, _)| *t == tag)
42 .map(|(_, v)| v.as_str())
43 }
44 pub fn msg_type(&self) -> &str {
45 self.get(35).unwrap_or_default()
46 }
47 pub fn seq(&self) -> Option<u32> {
48 self.get(34).and_then(|s| s.parse().ok())
49 }
50}
51
52pub enum Frame {
54 Incomplete,
56 Message {
58 begin: String,
59 message: Message,
60 consumed: usize,
61 },
62 Garbled { consumed: usize, reason: String },
64}
65
66fn find(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
67 hay.get(from..)?
68 .windows(needle.len())
69 .position(|w| w == needle)
70 .map(|p| p + from)
71}
72
73pub fn checksum(bytes: &[u8]) -> u8 {
74 bytes.iter().fold(0u8, |a, b| a.wrapping_add(*b))
75}
76
77pub fn frame(buf: &[u8]) -> Frame {
79 if buf.is_empty() {
80 return Frame::Incomplete;
81 }
82 if !buf.starts_with(b"8=") {
83 return match find(buf, b"\x018=", 0) {
85 Some(p) => Frame::Garbled {
86 consumed: p + 1,
87 reason: "bytes before BeginString".into(),
88 },
89 None if buf.len() > 1 => Frame::Garbled {
90 consumed: buf.len() - 1,
91 reason: "no BeginString".into(),
92 },
93 None => Frame::Incomplete,
94 };
95 }
96 let Some(e1) = buf.iter().position(|b| *b == SOH) else {
97 return if buf.len() > 32 {
98 Frame::Garbled {
99 consumed: 2,
100 reason: "BeginString too long".into(),
101 }
102 } else {
103 Frame::Incomplete
104 };
105 };
106 let begin = String::from_utf8_lossy(&buf[2..e1]).into_owned();
107 let rest = &buf[e1 + 1..];
108 if rest.len() < 2 {
109 return Frame::Incomplete;
110 }
111 if !rest.starts_with(b"9=") {
112 return Frame::Garbled {
113 consumed: e1 + 1,
114 reason: "BodyLength is not the second field".into(),
115 };
116 }
117 let Some(e2) = rest.iter().position(|b| *b == SOH) else {
118 return if rest.len() > 12 {
119 Frame::Garbled {
120 consumed: e1 + 1,
121 reason: "BodyLength too long".into(),
122 }
123 } else {
124 Frame::Incomplete
125 };
126 };
127 let len: usize = match std::str::from_utf8(&rest[2..e2])
128 .ok()
129 .filter(|s| !s.is_empty() && s.len() <= 7 && s.bytes().all(|b| b.is_ascii_digit()))
130 .and_then(|s| s.parse().ok())
131 {
132 Some(n) if n <= MAX_BODY => n,
133 _ => {
134 return Frame::Garbled {
135 consumed: e1 + 1 + e2 + 1,
136 reason: "BodyLength is not a number within bounds".into(),
137 }
138 }
139 };
140 let body_start = e1 + 1 + e2 + 1;
141 let total = body_start + len + 7;
142 if buf.len() < total {
143 return Frame::Incomplete;
144 }
145 let trailer = &buf[body_start + len..total];
146 if !trailer.starts_with(b"10=")
147 || trailer[6] != SOH
148 || !trailer[3..6].iter().all(u8::is_ascii_digit)
149 {
150 return Frame::Garbled {
151 consumed: body_start,
152 reason: "BodyLength does not end at CheckSum".into(),
153 };
154 }
155 let declared: u32 = std::str::from_utf8(&trailer[3..6])
156 .ok()
157 .and_then(|s| s.parse().ok())
158 .unwrap_or(1000);
159 if declared != checksum(&buf[..body_start + len]) as u32 {
160 return Frame::Garbled {
161 consumed: total,
162 reason: "CheckSum mismatch".into(),
163 };
164 }
165 match parse_fields(&buf[body_start..body_start + len]) {
166 Ok(fields) => Frame::Message {
167 begin,
168 message: Message { fields },
169 consumed: total,
170 },
171 Err(e) => Frame::Garbled {
172 consumed: total,
173 reason: format!("{e:#}"),
174 },
175 }
176}
177
178fn parse_fields(body: &[u8]) -> Result<Vec<(u32, String)>> {
179 let mut out: Vec<(u32, String)> = Vec::new();
180 let mut i = 0;
181 let mut data_len: Option<(u32, usize)> = None;
182 while i < body.len() {
183 ensure!(out.len() < MAX_FIELDS, "more than {MAX_FIELDS} fields");
184 let eq = body[i..]
185 .iter()
186 .position(|b| *b == b'=')
187 .context("field without =")?
188 + i;
189 let tag_s = std::str::from_utf8(&body[i..eq])
190 .ok()
191 .filter(|s| {
192 !s.is_empty()
193 && s.len() <= 9
194 && !s.starts_with('0')
195 && s.bytes().all(|b| b.is_ascii_digit())
196 })
197 .context("invalid tag")?;
198 let tag: u32 = tag_s.parse()?;
199 let value = match data_len.take() {
200 Some((data_tag, n)) if data_tag == tag => {
201 let v = body
202 .get(eq + 1..eq + 1 + n)
203 .context("data field shorter than its length")?;
204 ensure!(
205 body.get(eq + 1 + n) == Some(&SOH),
206 "data field not followed by SOH"
207 );
208 i = eq + 1 + n + 1;
209 String::from_utf8_lossy(v).into_owned()
210 }
211 _ => {
212 let end = body[eq + 1..]
213 .iter()
214 .position(|b| *b == SOH)
215 .context("field not terminated by SOH")?
216 + eq
217 + 1;
218 let v = &body[eq + 1..end];
219 ensure!(!v.is_empty(), "tag {tag} has an empty value");
220 i = end + 1;
221 String::from_utf8_lossy(v).into_owned()
222 }
223 };
224 if let Some((_, data_tag)) = DATA_PAIRS.iter().find(|(l, _)| *l == tag) {
225 let n: usize = value
226 .parse()
227 .ok()
228 .filter(|n| *n <= MAX_BODY)
229 .context("invalid data length")?;
230 data_len = Some((*data_tag, n));
231 }
232 out.push((tag, value));
233 }
234 ensure!(
235 !out.is_empty() && out[0].0 == 35,
236 "MsgType is not the third field"
237 );
238 Ok(out)
239}
240
241pub fn encode(begin: &str, fields: &[(u32, String)]) -> Result<Vec<u8>> {
243 let mut body = Vec::new();
244 for (tag, value) in fields {
245 if value.as_bytes().contains(&SOH) && !DATA_PAIRS.iter().any(|(_, d)| d == tag) {
246 bail!("tag {tag} contains SOH");
247 }
248 body.extend_from_slice(format!("{tag}=").as_bytes());
249 body.extend_from_slice(value.as_bytes());
250 body.push(SOH);
251 }
252 ensure!(body.len() <= MAX_BODY, "message body over {MAX_BODY} bytes");
253 let mut out = format!("8={begin}\x019={}\x01", body.len()).into_bytes();
254 out.extend(body);
255 let sum = checksum(&out);
256 out.extend_from_slice(format!("10={sum:03}\x01").as_bytes());
257 Ok(out)
258}
259
260pub fn timestamp() -> String {
262 let now = time::OffsetDateTime::now_utc();
263 format!(
264 "{:04}{:02}{:02}-{:02}:{:02}:{:02}.{:03}",
265 now.year(),
266 now.month() as u8,
267 now.day(),
268 now.hour(),
269 now.minute(),
270 now.second(),
271 now.millisecond()
272 )
273}