1use anyhow::{bail, ensure, Context, Result};
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Value};
11use std::time::Duration;
12use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
13
14pub const START: u8 = 0x0b;
15pub const END: [u8; 2] = [0x1c, 0x0d];
16pub const MAX_MESSAGE_BYTES: usize = 1024 * 1024;
17pub const MAX_SEGMENTS: usize = 4096;
18pub const MAX_FIELDS: usize = 512;
19pub const MAX_FIELD_BYTES: usize = 64 * 1024;
20pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
21
22#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct Segment {
25 pub id: String,
26 #[serde(default)]
27 pub fields: Vec<String>,
28}
29
30fn segment_id(id: &str) -> Result<()> {
31 ensure!(
32 id.len() == 3
33 && id
34 .bytes()
35 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
36 && id.as_bytes()[0].is_ascii_uppercase(),
37 "segment id '{id}' must be three upper-case letters or digits"
38 );
39 Ok(())
40}
41
42pub fn field(text: &str) -> Result<String> {
46 ensure!(
47 text.len() <= MAX_FIELD_BYTES,
48 "HL7 field exceeds {MAX_FIELD_BYTES} bytes"
49 );
50 ensure!(
51 !text.chars().any(|c| c.is_control()),
52 "HL7 field contains a control character (CR would start a forged segment)"
53 );
54 Ok(text.replace('|', "\\F\\"))
55}
56
57#[derive(Clone, Debug, PartialEq)]
59pub struct Message {
60 pub segments: Vec<Segment>,
61 pub latin1: bool,
63}
64
65impl Message {
66 pub fn msh(&self, n: usize) -> &str {
67 self.segments
69 .first()
70 .and_then(|s| s.fields.get(n.wrapping_sub(2)))
71 .map(String::as_str)
72 .unwrap_or("")
73 }
74 pub fn control_id(&self) -> &str {
75 self.msh(10)
76 }
77 pub fn message_type(&self) -> &str {
78 self.msh(9)
79 }
80 pub fn to_event(&self) -> Value {
81 json!({
82 "message_type": self.message_type(),
83 "control_id": self.control_id(),
84 "version": self.msh(12),
85 "processing_id": self.msh(11),
86 "sending_application": self.msh(3),
87 "sending_facility": self.msh(4),
88 "receiving_application": self.msh(5),
89 "receiving_facility": self.msh(6),
90 "segments": self.segments,
91 "charset_assumed": if self.latin1 { Value::from("ISO-8859-1") } else { Value::Null },
92 })
93 }
94}
95
96pub fn parse(bytes: &[u8]) -> Result<Message> {
98 ensure!(
99 bytes.len() <= MAX_MESSAGE_BYTES,
100 "HL7 message exceeds {MAX_MESSAGE_BYTES} bytes"
101 );
102 let (text, latin1) = match std::str::from_utf8(bytes) {
103 Ok(t) => (t.to_owned(), false),
104 Err(_) => (bytes.iter().map(|&b| b as char).collect::<String>(), true),
105 };
106 ensure!(text.starts_with("MSH"), "HL7 message must start with MSH");
107 let separator = text
108 .chars()
109 .nth(3)
110 .context("MSH without a field separator")?;
111 ensure!(
112 separator == '|',
113 "only the standard field separator '|' is supported"
114 );
115 let mut segments = Vec::new();
116 for line in text.split(['\r', '\n']).filter(|l| !l.is_empty()) {
117 ensure!(
118 segments.len() < MAX_SEGMENTS,
119 "HL7 message exceeds {MAX_SEGMENTS} segments"
120 );
121 let mut parts = line.split(separator);
122 let id = parts.next().unwrap_or_default().to_owned();
123 segment_id(&id)?;
124 let fields: Vec<String> = parts.map(str::to_owned).collect();
125 ensure!(
126 fields.len() <= MAX_FIELDS,
127 "segment {id} exceeds {MAX_FIELDS} fields"
128 );
129 ensure!(
130 fields
131 .iter()
132 .all(|f| f.len() <= MAX_FIELD_BYTES && !f.chars().any(|c| c.is_control())),
133 "segment {id} has an over-long field or a control character"
134 );
135 segments.push(Segment { id, fields });
136 }
137 ensure!(segments[0].id == "MSH", "first segment must be MSH");
138 let encoding = segments[0].fields.first().map(String::as_str).unwrap_or("");
139 ensure!(
140 encoding.starts_with("^~\\&"),
141 "MSH-2 must declare the standard encoding characters ^~\\&"
142 );
143 let message = Message { segments, latin1 };
144 ensure!(
145 !message.control_id().is_empty(),
146 "MSH-10 (message control id) is required"
147 );
148 ensure!(
149 !message.message_type().is_empty(),
150 "MSH-9 (message type) is required"
151 );
152 Ok(message)
153}
154
155fn render(segments: &[Segment]) -> Result<Vec<u8>> {
156 let mut out = String::new();
157 for s in segments {
158 segment_id(&s.id)?;
159 out.push_str(&s.id);
160 for (i, f) in s.fields.iter().enumerate() {
161 out.push('|');
162 if s.id == "MSH" && i == 0 {
163 out.push_str(f);
164 } else {
165 out.push_str(&field(f)?);
166 }
167 }
168 out.push('\r');
169 ensure!(
170 out.len() <= MAX_MESSAGE_BYTES,
171 "HL7 message exceeds {MAX_MESSAGE_BYTES} bytes"
172 );
173 }
174 Ok(out.into_bytes())
175}
176
177pub fn frame(message: &[u8]) -> Vec<u8> {
179 let mut out = Vec::with_capacity(message.len() + 3);
180 out.push(START);
181 out.extend_from_slice(message);
182 out.extend_from_slice(&END);
183 out
184}
185
186pub fn timestamp() -> String {
188 let secs = crate::utils::clock::SystemTime::now()
189 .duration_since(crate::utils::clock::UNIX_EPOCH)
190 .map(|d| d.as_secs())
191 .unwrap_or(0);
192 let days = (secs / 86_400) as i64;
193 let rem = secs % 86_400;
194 let z = days + 719_468;
196 let era = z.div_euclid(146_097);
197 let doe = z - era * 146_097;
198 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
199 let y = yoe + era * 400;
200 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
201 let mp = (5 * doy + 2) / 153;
202 let d = doy - (153 * mp + 2) / 5 + 1;
203 let m = if mp < 10 { mp + 3 } else { mp - 9 };
204 let y = if m <= 2 { y + 1 } else { y };
205 format!(
206 "{y:04}{m:02}{d:02}{:02}{:02}{:02}",
207 rem / 3600,
208 rem % 3600 / 60,
209 rem % 60
210 )
211}
212
213pub struct Header<'a> {
215 pub sending_application: &'a str,
216 pub sending_facility: &'a str,
217 pub receiving_application: &'a str,
218 pub receiving_facility: &'a str,
219 pub message_type: &'a str,
220 pub control_id: &'a str,
221 pub processing_id: &'a str,
222 pub version: &'a str,
223}
224
225pub fn msh(h: &Header) -> Result<Segment> {
226 for (name, v) in [
227 ("message_type", h.message_type),
228 ("control_id", h.control_id),
229 ("version", h.version),
230 ] {
231 ensure!(!v.is_empty(), "{name} is required");
232 }
233 ensure!(
234 h.control_id.len() <= 199,
235 "control id exceeds 199 characters"
236 );
237 let fields = [
238 "^~\\&",
239 h.sending_application,
240 h.sending_facility,
241 h.receiving_application,
242 h.receiving_facility,
243 ×tamp(),
244 "",
245 h.message_type,
246 h.control_id,
247 h.processing_id,
248 h.version,
249 ];
250 Ok(Segment {
251 id: "MSH".into(),
252 fields: fields.iter().map(|s| s.to_string()).collect(),
253 })
254}
255
256pub fn build(header: &Header, body: &[Segment]) -> Result<Vec<u8>> {
258 ensure!(
259 body.len() < MAX_SEGMENTS,
260 "HL7 message exceeds {MAX_SEGMENTS} segments"
261 );
262 ensure!(
263 body.iter().all(|s| s.id != "MSH"),
264 "MSH is built by Rust; do not supply it"
265 );
266 let mut segments = vec![msh(header)?];
267 segments.extend(body.iter().cloned());
268 render(&segments)
269}
270
271pub const ACK_CODES: &[&str] = &["AA", "AE", "AR", "CA", "CE", "CR"];
272
273pub fn ack(
276 original: &Message,
277 code: &str,
278 text: &str,
279 control_id: &str,
280 error: Option<&Value>,
281 extra: &[Segment],
282) -> Result<Vec<u8>> {
283 ensure!(
284 ACK_CODES.contains(&code),
285 "acknowledgment code must be one of {ACK_CODES:?}"
286 );
287 let trigger = original.message_type().split('^').nth(1).unwrap_or("");
288 let ack_type = if trigger.is_empty() {
289 "ACK".to_owned()
290 } else {
291 format!("ACK^{trigger}^ACK")
292 };
293 let header = Header {
294 sending_application: original.msh(5),
295 sending_facility: original.msh(6),
296 receiving_application: original.msh(3),
297 receiving_facility: original.msh(4),
298 message_type: &ack_type,
299 control_id,
300 processing_id: original.msh(11),
301 version: if original.msh(12).is_empty() {
302 "2.5"
303 } else {
304 original.msh(12)
305 },
306 };
307 let mut body = vec![Segment {
308 id: "MSA".into(),
309 fields: vec![code.into(), original.control_id().into(), text.into()],
310 }];
311 if let Some(e) = error {
312 let s = |k: &str| e.get(k).and_then(Value::as_str).unwrap_or("").to_owned();
313 body.push(Segment {
315 id: "ERR".into(),
316 fields: vec![
317 String::new(),
318 s("location"),
319 s("code"),
320 s("severity"),
321 String::new(),
322 String::new(),
323 String::new(),
324 s("message"),
325 ],
326 });
327 }
328 body.extend(extra.iter().cloned());
329 build(&header, &body)
330}
331
332pub async fn read_frame<R: AsyncRead + Unpin>(
335 reader: &mut R,
336 idle: Duration,
337) -> Result<Option<Vec<u8>>> {
338 let mut byte = [0u8; 1];
339 let n = tokio::time::timeout(idle, reader.read(&mut byte))
340 .await
341 .context("MLLP idle deadline")??;
342 if n == 0 {
343 return Ok(None);
344 }
345 ensure!(byte[0] == START, "MLLP frame must begin with <VT> (0x0B)");
346 tokio::time::timeout(IO_TIMEOUT, async {
347 let mut message = Vec::new();
348 let mut buf = [0u8; 8192];
349 loop {
350 let n = reader.read(&mut buf).await?;
351 if n == 0 {
352 bail!("peer closed inside an MLLP frame");
353 }
354 message.extend_from_slice(&buf[..n]);
355 if let Some(end) = message.windows(2).position(|w| w == END) {
356 ensure!(
357 end + 2 == message.len(),
358 "bytes after the MLLP end block (one message per frame, one frame in flight)"
359 );
360 message.truncate(end);
361 return Ok(Some(message));
362 }
363 ensure!(
364 !message.contains(&0x1c),
365 "<FS> without <CR> inside an MLLP frame"
366 );
367 ensure!(!message.contains(&START), "<VT> inside an MLLP frame");
368 ensure!(
369 message.len() <= MAX_MESSAGE_BYTES + 2,
370 "MLLP frame exceeds {MAX_MESSAGE_BYTES} bytes"
371 );
372 }
373 })
374 .await
375 .context("MLLP whole-frame deadline")?
376}
377
378pub async fn write_frame<W: AsyncWrite + Unpin>(writer: &mut W, message: &[u8]) -> Result<usize> {
379 let framed = frame(message);
380 tokio::time::timeout(IO_TIMEOUT, async {
381 writer.write_all(&framed).await?;
382 writer.flush().await
383 })
384 .await
385 .context("MLLP write deadline")??;
386 Ok(framed.len())
387}
388
389pub fn segments_from(v: &Value) -> Result<Vec<Segment>> {
390 match v {
391 Value::Null => Ok(Vec::new()),
392 Value::Array(_) => {
393 let segments: Vec<Segment> = serde_json::from_value(v.clone())
394 .context("segments must be [{id, fields: [text]}]")?;
395 ensure!(segments.len() < MAX_SEGMENTS, "too many segments");
396 for s in &segments {
397 segment_id(&s.id)?;
398 ensure!(
399 s.fields.len() <= MAX_FIELDS,
400 "segment {} exceeds {MAX_FIELDS} fields",
401 s.id
402 );
403 for f in &s.fields {
404 field(f)?;
405 }
406 }
407 Ok(segments)
408 }
409 _ => bail!("segments must be an array"),
410 }
411}