Skip to main content

hl7_net/
encoding.rs

1use crate::error::Hl7Error;
2
3/// Recognized segment delimiters, in priority order (longest first).
4const SEGMENT_DELIMITERS: [&str; 4] = ["\r\n", "\n\r", "\r", "\n"];
5
6/// HL7 encoding/decoding rules: the field, component, repetition, escape and
7/// subcomponent delimiters plus the routines that escape and unescape values.
8///
9/// `escape_character` uses `'\0'` to mean "no escape character" (matching the
10/// `(char)0` sentinel from the original .NET implementation).
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct HL7Encoding {
13    /// Field delimiter (`|`, encoded as `\F\`).
14    pub field_delimiter: char,
15    /// Component delimiter (`^`, encoded as `\S\`).
16    pub component_delimiter: char,
17    /// Repetition delimiter (`~`, encoded as `\R\`).
18    pub repeat_delimiter: char,
19    /// Escape character (`\`, encoded as `\E\`). `'\0'` means "disabled".
20    pub escape_character: char,
21    /// Subcomponent delimiter (`&`, encoded as `\T\`).
22    pub subcomponent_delimiter: char,
23    /// Segment delimiter (defaults to `\r`).
24    pub segment_delimiter: String,
25    /// String representation of a "present but null" value (defaults to `""`).
26    pub present_but_null: String,
27}
28
29impl Default for HL7Encoding {
30    fn default() -> Self {
31        Self {
32            field_delimiter: '|',
33            component_delimiter: '^',
34            repeat_delimiter: '~',
35            escape_character: '\\',
36            subcomponent_delimiter: '&',
37            segment_delimiter: "\r".to_string(),
38            present_but_null: "\"\"".to_string(),
39        }
40    }
41}
42
43impl HL7Encoding {
44    /// Creates an encoding with the default HL7 delimiters.
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// All delimiter characters concatenated as they appear in MSH-2
50    /// (`^~\&` for the defaults). The escape character is omitted when disabled.
51    pub fn all_delimiters(&self) -> String {
52        let mut s = String::new();
53        s.push(self.field_delimiter);
54        s.push(self.component_delimiter);
55        s.push(self.repeat_delimiter);
56        if self.escape_character != '\0' {
57            s.push(self.escape_character);
58        }
59        s.push(self.subcomponent_delimiter);
60        s
61    }
62
63    /// Sets the delimiter characters from the MSH delimiter string in the order
64    /// field, component, repetition, escape, subcomponent.
65    ///
66    /// When the 5th character equals the field delimiter, the escape character is
67    /// treated as disabled and the 4th character becomes the subcomponent delimiter.
68    pub fn evaluate_delimiters(&mut self, delimiters: &str) -> Result<(), Hl7Error> {
69        let chars: Vec<char> = delimiters.chars().collect();
70
71        if chars.len() < 5 {
72            return Err(Hl7Error::with_code(
73                "Not enough delimiter characters in MSH segment",
74                Self::bad(),
75            ));
76        }
77
78        self.field_delimiter = chars[0];
79        self.component_delimiter = chars[1];
80        self.repeat_delimiter = chars[2];
81
82        if chars[4] == self.field_delimiter {
83            self.escape_character = '\0';
84            self.subcomponent_delimiter = chars[3];
85        } else {
86            self.escape_character = chars[3];
87            self.subcomponent_delimiter = chars[4];
88        }
89
90        Ok(())
91    }
92
93    /// Detects and stores the segment delimiter used in `message`.
94    pub fn evaluate_segment_delimiter(&mut self, message: &str) -> Result<(), Hl7Error> {
95        for delim in SEGMENT_DELIMITERS {
96            if message.contains(delim) {
97                self.segment_delimiter = delim.to_string();
98                return Ok(());
99            }
100        }
101
102        Err(Hl7Error::with_code(
103            "Segment delimiter not found in message",
104            Self::bad(),
105        ))
106    }
107
108    /// Escapes HL7 special characters in `val` according to the current delimiters.
109    pub fn encode(&self, val: &str) -> String {
110        if val.is_empty() {
111            return String::new();
112        }
113
114        let chars: Vec<char> = val.chars().collect();
115        let esc = self.escape_character;
116        let mut sb = String::with_capacity(val.len());
117        let mut i = 0;
118
119        while i < chars.len() {
120            let c = chars[i];
121            let mut continue_encoding = true;
122
123            if c == '<' {
124                continue_encoding = false;
125
126                if i + 2 < chars.len() && chars[i + 1] == 'B' && chars[i + 2] == '>' {
127                    // <B> -> highlight on
128                    sb.push(esc);
129                    sb.push('H');
130                    sb.push(esc);
131                    i += 2;
132                } else if i + 3 < chars.len()
133                    && chars[i + 1] == '/'
134                    && chars[i + 2] == 'B'
135                    && chars[i + 3] == '>'
136                {
137                    // </B> -> highlight off
138                    sb.push(esc);
139                    sb.push('N');
140                    sb.push(esc);
141                    i += 3;
142                } else if i + 3 < chars.len()
143                    && chars[i + 1] == 'B'
144                    && chars[i + 2] == 'R'
145                    && chars[i + 3] == '>'
146                {
147                    // <BR> -> line break
148                    sb.push(esc);
149                    sb.push_str(".br");
150                    sb.push(esc);
151                    i += 3;
152                } else {
153                    continue_encoding = true;
154                }
155            }
156
157            if continue_encoding {
158                if c == self.component_delimiter {
159                    sb.push(esc);
160                    sb.push('S');
161                    sb.push(esc);
162                } else if c == esc {
163                    sb.push(esc);
164                    sb.push('E');
165                    sb.push(esc);
166                } else if c == self.field_delimiter {
167                    sb.push(esc);
168                    sb.push('F');
169                    sb.push(esc);
170                } else if c == self.repeat_delimiter {
171                    sb.push(esc);
172                    sb.push('R');
173                    sb.push(esc);
174                } else if c == self.subcomponent_delimiter {
175                    sb.push(esc);
176                    sb.push('T');
177                    sb.push(esc);
178                } else if c == '\n' || c == '\r' {
179                    // Preserve other non-visible characters as hex escapes.
180                    let mut v = format!("{:X}", c as u32);
181                    if v.len() % 2 != 0 {
182                        v.insert(0, '0');
183                    }
184                    sb.push(esc);
185                    sb.push('X');
186                    sb.push_str(&v);
187                    sb.push(esc);
188                } else {
189                    sb.push(c);
190                }
191            }
192
193            i += 1;
194        }
195
196        sb
197    }
198
199    /// Convenience for serialization: encodes `Some(value)` or returns the
200    /// "present but null" representation for `None`.
201    pub fn encode_opt(&self, val: Option<&str>) -> String {
202        match val {
203            Some(v) => self.encode(v),
204            None => self.present_but_null.clone(),
205        }
206    }
207
208    /// Decodes an escaped HL7 string back to its original characters.
209    pub fn decode(&self, encoded: &str) -> String {
210        if encoded.trim().is_empty() {
211            return encoded.to_string();
212        }
213
214        if !encoded.contains(self.escape_character) {
215            return encoded.to_string();
216        }
217
218        let chars: Vec<char> = encoded.chars().collect();
219        let esc = self.escape_character;
220        let mut result = String::with_capacity(encoded.len());
221        let mut i = 0;
222
223        while i < chars.len() {
224            let c = chars[i];
225
226            if c != esc {
227                result.push(c);
228                i += 1;
229                continue;
230            }
231
232            // Skip the opening escape character.
233            i += 1;
234
235            // Find the closing escape character.
236            let li = (i..chars.len()).find(|&k| chars[k] == esc);
237
238            match li {
239                None => {
240                    // Unterminated escape sequence: keep it verbatim.
241                    result.push(esc);
242                    if i < chars.len() {
243                        result.push(chars[i]);
244                    }
245                    i += 1;
246                }
247                Some(li) => {
248                    let seq: String = chars[i..li].iter().collect();
249
250                    if seq.is_empty() {
251                        i = li + 1;
252                        continue;
253                    }
254
255                    match seq.as_str() {
256                        "H" => result.push_str("<B>"),
257                        "N" => result.push_str("</B>"),
258                        "F" => result.push(self.field_delimiter),
259                        "S" => result.push(self.component_delimiter),
260                        "T" => result.push(self.subcomponent_delimiter),
261                        "R" => result.push(self.repeat_delimiter),
262                        "E" => result.push(self.escape_character),
263                        ".br" => result.push_str("<BR>"),
264                        _ => {
265                            if let Some(hex) = seq.strip_prefix('X') {
266                                result.push_str(&Self::decode_hex_string(hex));
267                            } else {
268                                result.push_str(&seq);
269                            }
270                        }
271                    }
272
273                    i = li + 1;
274                }
275            }
276        }
277
278        result
279    }
280
281    /// Decodes a hexadecimal string (the payload of an `\X..\` escape) into a
282    /// Unicode string.
283    pub fn decode_hex_string(hex: &str) -> String {
284        let n = hex.len();
285        let mut bytes = Vec::with_capacity(n / 2);
286
287        let mut i = 0;
288        while i + 2 <= n {
289            match u8::from_str_radix(&hex[i..i + 2], 16) {
290                Ok(b) => bytes.push(b),
291                Err(_) => bytes.push(0),
292            }
293            i += 2;
294        }
295
296        if bytes.len() == 1 {
297            char::from_u32(bytes[0] as u32).map(String::from).unwrap_or_default()
298        } else if bytes.len() == 2 && bytes[0] == 0 {
299            char::from_u32(bytes[1] as u32).map(String::from).unwrap_or_default()
300        } else {
301            String::from_utf8_lossy(&bytes).into_owned()
302        }
303    }
304
305    fn bad() -> &'static str {
306        Hl7Error::BAD_MESSAGE
307    }
308}