Skip to main content

hl7_net/
message.rs

1use std::collections::HashMap;
2use std::sync::LazyLock;
3
4use regex::Regex;
5
6use crate::encoding::HL7Encoding;
7use crate::error::Hl7Error;
8use crate::field::Field;
9use crate::helper;
10use crate::segment::Segment;
11
12static SEGMENT_REGEX: LazyLock<Regex> =
13    LazyLock::new(|| Regex::new(r"^([A-Z][A-Z][A-Z1-9])([\(\[]([0-9]+)[\)\]]){0,1}$").unwrap());
14static FIELD_REGEX: LazyLock<Regex> =
15    LazyLock::new(|| Regex::new(r"^([0-9]+)([\(\[]([0-9]+)[\)\]]){0,1}$").unwrap());
16static OTHER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[1-9]([0-9]{1,2})?$").unwrap());
17
18/// A parsed HL7 v2 message: a collection of [`Segment`]s keyed by name, plus the
19/// message-level metadata extracted from the MSH segment.
20#[derive(Debug, Clone, Default)]
21pub struct Message {
22    /// The raw message text (normalized after parsing).
23    pub hl7_message: String,
24    /// HL7 version from MSH-12.
25    pub version: String,
26    /// Message structure from MSH-9.3 (or a derived value).
27    pub message_structure: String,
28    /// Message control ID from MSH-10.
29    pub message_control_id: String,
30    /// Processing ID from MSH-11.
31    pub processing_id: String,
32    /// Number of segments in the message.
33    pub segment_count: usize,
34    /// The encoding (delimiters) used by the message.
35    pub encoding: HL7Encoding,
36
37    segments: HashMap<String, Vec<Segment>>,
38    all_segments: Vec<String>,
39}
40
41impl Message {
42    /// Creates an empty message with default encoding.
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Creates a message wrapping the given raw HL7 text (not yet parsed).
48    pub fn with_message(text: impl Into<String>) -> Self {
49        Self { hl7_message: text.into(), ..Self::default() }
50    }
51
52    /// Convenience: wraps and parses `text` in one step.
53    pub fn parse_str(text: impl Into<String>, bypass_validation: bool) -> Result<Self, Hl7Error> {
54        let mut msg = Self::with_message(text);
55        msg.parse(bypass_validation)?;
56        Ok(msg)
57    }
58
59    // ----- Parsing & serialization ---------------------------------------
60
61    /// Parses [`Self::hl7_message`] into segments/fields/components.
62    ///
63    /// Returns `true` when the message round-trips (re-serializing yields the same
64    /// text). Pass `bypass_validation = true` to skip structural validation (do not
65    /// use for newly constructed messages).
66    pub fn parse(&mut self, bypass_validation: bool) -> Result<bool, Hl7Error> {
67        let is_valid = if bypass_validation { true } else { self.validate_message()? };
68
69        if !is_valid {
70            return Ok(false);
71        }
72
73        if self.all_segments.is_empty() {
74            self.all_segments = helper::split_message(&self.hl7_message);
75        }
76
77        let enc = self.encoding.clone();
78        self.segments.clear();
79        self.segment_count = 0;
80
81        let lines = self.all_segments.clone();
82        for line in lines {
83            if line.trim().is_empty() {
84                continue;
85            }
86
87            let segment = Segment::parse(&line, &enc).map_err(|e| {
88                Hl7Error::with_code(
89                    format!("Failed to parse the message with error - {}", e.message),
90                    Hl7Error::PARSING_ERROR,
91                )
92            })?;
93            self.add_new_segment(segment);
94        }
95
96        let serialized = self.serialize().map_err(|e| {
97            Hl7Error::with_code(
98                format!("Failed to serialize parsed message with error - {}", e.message),
99                Hl7Error::PARSING_ERROR,
100            )
101        })?;
102
103        if serialized.is_empty() {
104            return Err(Hl7Error::with_code(
105                "Unable to serialize to original message - ",
106                Hl7Error::PARSING_ERROR,
107            ));
108        }
109
110        self.encoding.evaluate_segment_delimiter(&self.hl7_message)?;
111
112        Ok(self.equals(&serialized))
113    }
114
115    /// Serializes the message back to HL7 text.
116    pub fn serialize(&self) -> Result<String, Hl7Error> {
117        let mut out = String::new();
118        for seg in self.segments_in_order() {
119            seg.serialize(&mut out, &self.encoding);
120        }
121        Ok(out)
122    }
123
124    // ----- Value access --------------------------------------------------
125
126    /// Gets the value at a path such as `PID.5.2` (segment.field.component.subcomponent).
127    ///
128    /// A segment occurrence may be supplied as `PID(2).5`. Returns the decoded value;
129    /// a "present but null" value is returned as an empty string.
130    pub fn get_value(&self, path: &str) -> Result<String, Hl7Error> {
131        let parts: Vec<&str> = path.split('.').collect();
132        let com_count = parts.len();
133
134        if !validate_value_format(&parts) {
135            return Err(Hl7Error::new(format!("Request format is not valid: {path}")));
136        }
137
138        let caps = SEGMENT_REGEX
139            .captures(parts[0])
140            .ok_or_else(|| Hl7Error::new(format!("Request format is not valid: {path}")))?;
141        let seg_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
142        let mut occurrence = 0usize;
143        if let Some(m) = caps.get(3)
144            && let Ok(v) = m.as_str().parse::<usize>()
145        {
146            occurrence = v.saturating_sub(1);
147        }
148
149        let segment = self
150            .segments
151            .get(seg_name)
152            .and_then(|list| list.get(occurrence))
153            .ok_or_else(|| Hl7Error::new(format!("Segment name not available: {path}")))?;
154
155        let enc = &self.encoding;
156        let str_value: Option<String> = match com_count {
157            4 => {
158                let field = get_field(segment, parts[1]).map_err(|e| {
159                    Hl7Error::new(format!("SubComponent not available - {path} Error: {}", e.message))
160                })?;
161                let ci = parse_index(parts[2])?;
162                let sci = parse_index(parts[3])?;
163                let comp = field.components.get(ci).ok_or_else(|| {
164                    Hl7Error::new(format!("SubComponent not available - {path}"))
165                })?;
166                let sub = comp.sub_components.get(sci).ok_or_else(|| {
167                    Hl7Error::new(format!("SubComponent not available - {path}"))
168                })?;
169                sub.value(enc)
170            }
171            3 => {
172                let field = get_field(segment, parts[1]).map_err(|e| {
173                    Hl7Error::new(format!("Component not available - {path} Error: {}", e.message))
174                })?;
175                let ci = parse_index(parts[2])?;
176                let comp = field
177                    .components
178                    .get(ci)
179                    .ok_or_else(|| Hl7Error::new(format!("Component not available - {path}")))?;
180                comp.value(enc)
181            }
182            2 => {
183                let field = get_field(segment, parts[1]).map_err(|e| {
184                    Hl7Error::new(format!("Field not available - {path} Error: {}", e.message))
185                })?;
186                field.value(enc)
187            }
188            _ => segment.value(enc),
189        };
190
191        Ok(enc.decode(str_value.as_deref().unwrap_or("")))
192    }
193
194    /// Sets the value at a path such as `PID.5.2` in every matching segment.
195    pub fn set_value(&mut self, path: &str, value: &str) -> Result<bool, Hl7Error> {
196        let parts: Vec<&str> = path.split('.').collect();
197        let com_count = parts.len();
198
199        if !validate_value_format(&parts) {
200            return Err(Hl7Error::new("Request format is not valid"));
201        }
202
203        let seg_name = parts[0];
204        let enc = self.encoding.clone();
205
206        let list = self
207            .segments
208            .get_mut(seg_name)
209            .ok_or_else(|| Hl7Error::new("Segment name not available"))?;
210
211        let mut is_set = false;
212
213        for segment in list.iter_mut() {
214            match com_count {
215                4 => {
216                    let ci = parse_index(parts[2])?;
217                    let sci = parse_index(parts[3])?;
218                    let field = get_field_mut(segment, parts[1]).map_err(|e| {
219                        Hl7Error::new(format!(
220                            "SubComponent not available - {path} Error: {}",
221                            e.message
222                        ))
223                    })?;
224                    let comp = field.components.get_mut(ci).ok_or_else(|| {
225                        Hl7Error::new(format!("SubComponent not available - {path}"))
226                    })?;
227                    let sub = comp.sub_components.get_mut(sci).ok_or_else(|| {
228                        Hl7Error::new(format!("SubComponent not available - {path}"))
229                    })?;
230                    sub.set_value(value);
231                    is_set = true;
232                }
233                3 => {
234                    let ci = parse_index(parts[2])?;
235                    let field = get_field_mut(segment, parts[1]).map_err(|e| {
236                        Hl7Error::new(format!("Component not available - {path} Error: {}", e.message))
237                    })?;
238                    let comp = field
239                        .components
240                        .get_mut(ci)
241                        .ok_or_else(|| Hl7Error::new(format!("Component not available - {path}")))?;
242                    comp.set_value(value, &enc);
243                    is_set = true;
244                }
245                2 => {
246                    let field = get_field_mut(segment, parts[1]).map_err(|e| {
247                        Hl7Error::new(format!("Field not available - {path} Error: {}", e.message))
248                    })?;
249                    field.set_value(value, &enc);
250                    is_set = true;
251                }
252                _ => return Err(Hl7Error::new("Cannot overwrite a segment value")),
253            }
254        }
255
256        Ok(is_set)
257    }
258
259    /// Whether the field at `path` is componentized.
260    pub fn is_componentized(&self, path: &str) -> Result<bool, Hl7Error> {
261        let parts: Vec<&str> = path.split('.').collect();
262
263        if !validate_value_format(&parts) {
264            return Err(Hl7Error::new("Request format is not valid"));
265        }
266        if parts.len() < 2 {
267            return Err(Hl7Error::new("Field not identified in request"));
268        }
269
270        let segment = self
271            .segments
272            .get(parts[0])
273            .and_then(|list| list.first())
274            .ok_or_else(|| Hl7Error::new(format!("Field not available - {path}")))?;
275        let field = get_field(segment, parts[1])
276            .map_err(|e| Hl7Error::new(format!("Field not available - {path} Error: {}", e.message)))?;
277
278        Ok(field.is_componentized)
279    }
280
281    /// Whether the field at `path` has repetitions.
282    pub fn has_repetitions(&self, path: &str) -> Result<bool, Hl7Error> {
283        let parts: Vec<&str> = path.split('.').collect();
284
285        if !validate_value_format(&parts) {
286            return Err(Hl7Error::new("Request format is not valid"));
287        }
288        if parts.len() < 2 {
289            return Err(Hl7Error::new("Field not identified in request"));
290        }
291
292        let segment = self
293            .segments
294            .get(parts[0])
295            .and_then(|list| list.first())
296            .ok_or_else(|| Hl7Error::new(format!("Field not available - {path}")))?;
297        let count = get_field_repetitions(segment, parts[1])
298            .map_err(|e| Hl7Error::new(format!("Field not available - {path} Error: {}", e.message)))?;
299
300        Ok(count > 1)
301    }
302
303    /// Whether the component at `path` is subcomponentized.
304    pub fn is_subcomponentized(&self, path: &str) -> Result<bool, Hl7Error> {
305        let parts: Vec<&str> = path.split('.').collect();
306
307        if !validate_value_format(&parts) {
308            return Err(Hl7Error::new("Request format is not valid"));
309        }
310        if parts.len() < 3 {
311            return Err(Hl7Error::new("Component not identified in request"));
312        }
313
314        let segment = self
315            .segments
316            .get(parts[0])
317            .and_then(|list| list.first())
318            .ok_or_else(|| Hl7Error::new(format!("Component not available - {path}")))?;
319        let field = get_field(segment, parts[1]).map_err(|e| {
320            Hl7Error::new(format!("Component not available - {path} Error: {}", e.message))
321        })?;
322        let ci = parse_index(parts[2])?;
323        let comp = field
324            .components
325            .get(ci)
326            .ok_or_else(|| Hl7Error::new(format!("Component not available - {path}")))?;
327
328        Ok(comp.is_subcomponentized)
329    }
330
331    // ----- Segment management --------------------------------------------
332
333    /// Appends a segment, assigning it the next sequence number.
334    pub fn add_new_segment(&mut self, mut segment: Segment) -> bool {
335        segment.sequence_no = self.segment_count;
336        self.segment_count += 1;
337        self.segments.entry(segment.name.clone()).or_default().push(segment);
338        true
339    }
340
341    /// Removes the `index`-th segment with the given name.
342    pub fn remove_segment(&mut self, segment_name: &str, index: usize) -> bool {
343        if let Some(list) = self.segments.get_mut(segment_name)
344            && index < list.len()
345        {
346            list.remove(index);
347            self.segment_count = self.segment_count.saturating_sub(1);
348            return true;
349        }
350        false
351    }
352
353    /// All segments in original order.
354    pub fn segments(&self) -> Vec<&Segment> {
355        self.segments_in_order()
356    }
357
358    /// All segments with the given name, in original order.
359    pub fn segments_named(&self, segment_name: &str) -> Vec<&Segment> {
360        self.segments_in_order()
361            .into_iter()
362            .filter(|s| s.name == segment_name)
363            .collect()
364    }
365
366    /// The first segment with the given name.
367    pub fn default_segment(&self, segment_name: &str) -> Option<&Segment> {
368        self.segments_in_order().into_iter().find(|s| s.name == segment_name)
369    }
370
371    /// Mutable access to all segments with the given name, in original order.
372    pub fn segments_named_mut(&mut self, segment_name: &str) -> Option<&mut Vec<Segment>> {
373        self.segments.get_mut(segment_name)
374    }
375
376    /// Builds and appends an MSH header segment.
377    #[allow(clippy::too_many_arguments)]
378    pub fn add_segment_msh(
379        &mut self,
380        sending_application: &str,
381        sending_facility: &str,
382        receiving_application: &str,
383        receiving_facility: &str,
384        security: Option<&str>,
385        message_type: &str,
386        message_control_id: &str,
387        processing_id: &str,
388        version: &str,
389    ) -> Result<(), Hl7Error> {
390        let date_string = helper::now_long_date();
391        let delim = self.encoding.field_delimiter;
392        let all = self.encoding.all_delimiters();
393        let seg_delim = self.encoding.segment_delimiter.clone();
394
395        let mut response = String::new();
396        response.push_str("MSH");
397        response.push_str(&all);
398        response.push(delim);
399        response.push_str(sending_application);
400        response.push(delim);
401        response.push_str(sending_facility);
402        response.push(delim);
403        response.push_str(receiving_application);
404        response.push(delim);
405        response.push_str(receiving_facility);
406        response.push(delim);
407        response.push_str(&self.encoding.encode(&date_string));
408        response.push(delim);
409        response.push_str(security.unwrap_or(""));
410        response.push(delim);
411        response.push_str(message_type);
412        response.push(delim);
413        response.push_str(message_control_id);
414        response.push(delim);
415        response.push_str(processing_id);
416        response.push(delim);
417        response.push_str(version);
418        response.push_str(&seg_delim);
419
420        let message = Message::parse_str(response, false)?;
421        let msh = message
422            .default_segment("MSH")
423            .ok_or_else(|| Hl7Error::new("MSH segment not found"))?
424            .clone();
425        self.add_new_segment(msh);
426
427        Ok(())
428    }
429
430    // ----- Acknowledgements & framing ------------------------------------
431
432    /// Builds the positive acknowledgement (ACK, code `AA`) for this message.
433    pub fn get_ack(&self, bypass_validation: bool) -> Option<Message> {
434        self.create_ack_message("AA", false, None, bypass_validation)
435    }
436
437    /// Builds a negative acknowledgement (NACK) with the given code and message.
438    pub fn get_nack(&self, code: &str, err_msg: &str, bypass_validation: bool) -> Option<Message> {
439        self.create_ack_message(code, true, Some(err_msg), bypass_validation)
440    }
441
442    /// Serializes the message into an MLLP-framed byte buffer.
443    pub fn get_mllp(&self) -> Result<Vec<u8>, Hl7Error> {
444        Ok(helper::get_mllp(&self.serialize()?))
445    }
446
447    fn create_ack_message(
448        &self,
449        code: &str,
450        is_nack: bool,
451        err_msg: Option<&str>,
452        bypass_validation: bool,
453    ) -> Option<Message> {
454        if self.message_structure == "ACK" {
455            return None;
456        }
457
458        let date_string = helper::now_long_date();
459        let msh = self.segments.get("MSH")?.first()?;
460        let delim = self.encoding.field_delimiter;
461        let all = self.encoding.all_delimiters();
462        let seg_delim = &self.encoding.segment_delimiter;
463
464        let field = |i: usize| -> String {
465            msh.fields.get(i).and_then(|f| f.value(&self.encoding)).unwrap_or_default()
466        };
467
468        let mut response = String::new();
469        response.push_str("MSH");
470        response.push_str(&all);
471        response.push(delim);
472        response.push_str(&field(4)); // receiving application -> sending
473        response.push(delim);
474        response.push_str(&field(5)); // receiving facility -> sending
475        response.push(delim);
476        response.push_str(&field(2)); // sending application -> receiving
477        response.push(delim);
478        response.push_str(&field(3)); // sending facility -> receiving
479        response.push(delim);
480        response.push_str(&date_string);
481        response.push(delim);
482        response.push(delim); // empty security
483        response.push_str("ACK");
484        response.push(delim);
485        response.push_str(&self.message_control_id);
486        response.push(delim);
487        response.push_str(&self.processing_id);
488        response.push(delim);
489        response.push_str(&self.version);
490        response.push_str(seg_delim);
491
492        response.push_str("MSA");
493        response.push(delim);
494        response.push_str(code);
495        response.push(delim);
496        response.push_str(&self.message_control_id);
497        if is_nack {
498            response.push(delim);
499            response.push_str(err_msg.unwrap_or(""));
500        }
501        response.push_str(seg_delim);
502
503        Message::parse_str(response, bypass_validation).ok()
504    }
505
506    // ----- Internals -----------------------------------------------------
507
508    fn segments_in_order(&self) -> Vec<&Segment> {
509        let mut all: Vec<&Segment> = self.segments.values().flatten().collect();
510        all.sort_by_key(|s| s.sequence_no);
511        all
512    }
513
514    /// Structural validation of the message; also extracts MSH metadata and
515    /// normalizes [`Self::hl7_message`]. Mirrors the .NET `validateMessage`,
516    /// wrapping every failure as a `BAD_MESSAGE`.
517    fn validate_message(&mut self) -> Result<bool, Hl7Error> {
518        self.validate_message_inner().map_err(|e| {
519            Hl7Error::with_code(
520                format!("Failed to validate the message with error - {}", e.message),
521                Hl7Error::BAD_MESSAGE,
522            )
523        })?;
524        Ok(true)
525    }
526
527    fn validate_message_inner(&mut self) -> Result<(), Hl7Error> {
528        if self.hl7_message.is_empty() {
529            return Err(Hl7Error::with_code("No Message Found", Hl7Error::BAD_MESSAGE));
530        }
531
532        // MSH + delimiters + 12 fields in MSH.
533        if self.hl7_message.len() < 20 {
534            return Err(Hl7Error::with_code(
535                format!("Message Length too short: {} chars.", self.hl7_message.len()),
536                Hl7Error::BAD_MESSAGE,
537            ));
538        }
539
540        if !self.hl7_message.starts_with("MSH") {
541            return Err(Hl7Error::with_code(
542                "MSH segment not found at the beginning of the message",
543                Hl7Error::BAD_MESSAGE,
544            ));
545        }
546
547        self.encoding.evaluate_segment_delimiter(&self.hl7_message)?;
548        self.all_segments = helper::split_message(&self.hl7_message);
549        self.hl7_message =
550            self.all_segments.join(&self.encoding.segment_delimiter) + &self.encoding.segment_delimiter;
551
552        let first = &self.all_segments[0];
553        let field_delimiters: String = first.chars().skip(3).take(5).collect();
554        self.encoding.evaluate_delimiters(&field_delimiters)?;
555        self.decompose_multibyte_hex_sequences();
556
557        let fourth_char = self.hl7_message.chars().nth(3);
558
559        for segment in &self.all_segments {
560            if segment.trim().is_empty() {
561                continue;
562            }
563
564            let name: String = segment.chars().take(3).collect();
565            if !SEGMENT_REGEX.is_match(&name) {
566                return Err(Hl7Error::with_code(
567                    format!("Invalid segment name found: {segment}"),
568                    Hl7Error::BAD_MESSAGE,
569                ));
570            }
571
572            if segment.chars().count() > 3 && segment.chars().nth(3) != fourth_char {
573                return Err(Hl7Error::with_code(
574                    format!("Invalid segment found: {segment}"),
575                    Hl7Error::BAD_MESSAGE,
576                ));
577            }
578        }
579
580        let msh_line = &self.all_segments[0];
581        let field_sep_count = msh_line.chars().filter(|c| *c == self.encoding.field_delimiter).count();
582        if field_sep_count < 11 {
583            return Err(Hl7Error::with_code(
584                "MSH segment doesn't contain all the required fields",
585                Hl7Error::BAD_MESSAGE,
586            ));
587        }
588
589        let msh_fields: Vec<&str> = msh_line.split(self.encoding.field_delimiter).collect();
590
591        // MSH-12: version
592        if msh_fields.len() >= 12 {
593            let decoded = self.encoding.decode(msh_fields[11]);
594            self.version =
595                decoded.split(self.encoding.component_delimiter).next().unwrap_or("").to_string();
596        } else {
597            return Err(Hl7Error::with_code(
598                "HL7 version not found in the MSH segment",
599                Hl7Error::REQUIRED_FIELD_MISSING,
600            ));
601        }
602
603        // MSH-9: message type & trigger event
604        let msh_9 = self.encoding.decode(msh_fields[8]);
605        if msh_9.is_empty() {
606            return Err(Hl7Error::with_code(
607                "MSH.9 not available",
608                Hl7Error::UNSUPPORTED_MESSAGE_TYPE,
609            ));
610        }
611
612        let comps: Vec<&str> = msh_9.split(self.encoding.component_delimiter).collect();
613        if comps.len() >= 3 {
614            self.message_structure = comps[2].to_string();
615        } else if !comps.is_empty() && comps[0] == "ACK" {
616            self.message_structure = "ACK".to_string();
617        } else if comps.len() == 2 {
618            self.message_structure = format!("{}_{}", comps[0], comps[1]);
619        } else {
620            return Err(Hl7Error::with_code(
621                "Message Type & Trigger Event value not found in message",
622                Hl7Error::UNSUPPORTED_MESSAGE_TYPE,
623            ));
624        }
625
626        // MSH-10: message control ID
627        self.message_control_id = self.encoding.decode(msh_fields[9]);
628        if self.message_control_id.is_empty() {
629            return Err(Hl7Error::with_code(
630                "MSH.10 - Message Control ID not found",
631                Hl7Error::REQUIRED_FIELD_MISSING,
632            ));
633        }
634
635        // MSH-11: processing ID
636        self.processing_id = self.encoding.decode(msh_fields[10]);
637        if self.processing_id.is_empty() {
638            return Err(Hl7Error::with_code(
639                "MSH.11 - Processing ID not found",
640                Hl7Error::REQUIRED_FIELD_MISSING,
641            ));
642        }
643
644        Ok(())
645    }
646
647    /// Round-trip self-check: compares the (hex-normalized) original message with
648    /// the freshly serialized text, segment by segment.
649    fn equals(&self, other: &str) -> bool {
650        let seg_chars: Vec<char> = self.encoding.segment_delimiter.chars().collect();
651        let split = |s: &str| -> Vec<String> {
652            s.split(|c| seg_chars.contains(&c))
653                .filter(|p| !p.is_empty())
654                .map(str::to_string)
655                .collect()
656        };
657
658        let mut arr1 = split(&self.hl7_message);
659        let arr2 = split(other);
660
661        self.decode_hexa_sequences(&mut arr1, false);
662
663        arr1 == arr2
664    }
665
666    fn decompose_multibyte_hex_sequences(&mut self) {
667        if self.encoding.escape_character == '\0' || self.all_segments.is_empty() {
668            return;
669        }
670
671        let mut lines = std::mem::take(&mut self.all_segments);
672        let changed = self.decode_hexa_sequences(&mut lines, true);
673        self.all_segments = lines;
674
675        if changed {
676            self.hl7_message = self.all_segments.join(&self.encoding.segment_delimiter)
677                + &self.encoding.segment_delimiter;
678        }
679    }
680
681    /// Decodes (or decomposes) `\X..\` hex escapes across message lines. Returns
682    /// whether anything changed. CR/LF bytes are preserved so they survive the
683    /// serialization round-trip.
684    fn decode_hexa_sequences(&self, lines: &mut [String], decompose: bool) -> bool {
685        let esc = self.encoding.escape_character;
686        let pattern = format!(r"\x{{{0:X}}}X([0-9A-Fa-f]*)\x{{{0:X}}}", esc as u32);
687        let re = match Regex::new(&pattern) {
688            Ok(r) => r,
689            Err(_) => return false,
690        };
691
692        let mut changed = false;
693
694        for line in lines.iter_mut() {
695            if !line.contains(esc) {
696                continue;
697            }
698
699            let replaced = re
700                .replace_all(line, |caps: &regex::Captures| {
701                    let whole = &caps[0];
702                    let hex = caps.get(1).map(|m| m.as_str()).unwrap_or("");
703
704                    if decompose {
705                        decompose_multibyte_hex(whole, hex, esc)
706                    } else if !is_encoded_linebreak_byte(hex) {
707                        HL7Encoding::decode_hex_string(hex)
708                    } else {
709                        whole.to_string()
710                    }
711                })
712                .into_owned();
713
714            if &replaced != line {
715                *line = replaced;
716                changed = true;
717            }
718        }
719
720        changed
721    }
722}
723
724// ----- Free helpers ------------------------------------------------------
725
726/// Parses a 1-based numeric index into a 0-based one.
727fn parse_index(s: &str) -> Result<usize, Hl7Error> {
728    s.parse::<usize>()
729        .ok()
730        .and_then(|n| n.checked_sub(1))
731        .ok_or_else(|| Hl7Error::new(format!("Invalid index: {s}")))
732}
733
734/// Parses a field index (with optional repetition), returning 0-based values.
735fn parse_field_index(index: &str) -> Result<(usize, usize), Hl7Error> {
736    let caps = FIELD_REGEX
737        .captures(index)
738        .ok_or_else(|| Hl7Error::new("Invalid field index"))?;
739
740    let field_index = caps[1]
741        .parse::<usize>()
742        .ok()
743        .and_then(|n| n.checked_sub(1))
744        .ok_or_else(|| Hl7Error::new("Invalid field index"))?;
745
746    let repetition = match caps.get(3) {
747        Some(m) => m
748            .as_str()
749            .parse::<usize>()
750            .ok()
751            .and_then(|n| n.checked_sub(1))
752            .ok_or_else(|| Hl7Error::new("Invalid field index"))?,
753        None => 0,
754    };
755
756    Ok((field_index, repetition))
757}
758
759/// Resolves a field for read access, honoring an optional repetition suffix
760/// (e.g. `3(2)` selects the second repetition of field 3).
761fn get_field<'a>(segment: &'a Segment, index: &str) -> Result<&'a Field, Hl7Error> {
762    let (field_index, repetition) = parse_field_index(index)?;
763    let field = segment
764        .fields
765        .get(field_index)
766        .ok_or_else(|| Hl7Error::new("Field not available"))?;
767
768    if field.has_repetitions {
769        field
770            .repetitions
771            .get(repetition)
772            .ok_or_else(|| Hl7Error::new("Field repetition not available"))
773    } else if repetition == 0 {
774        Ok(field)
775    } else {
776        Err(Hl7Error::new("Field repetition not available"))
777    }
778}
779
780/// Mutable counterpart of [`get_field`].
781fn get_field_mut<'a>(segment: &'a mut Segment, index: &str) -> Result<&'a mut Field, Hl7Error> {
782    let (field_index, repetition) = parse_field_index(index)?;
783    let field = segment
784        .fields
785        .get_mut(field_index)
786        .ok_or_else(|| Hl7Error::new("Field not available"))?;
787
788    if field.has_repetitions {
789        field
790            .repetitions
791            .get_mut(repetition)
792            .ok_or_else(|| Hl7Error::new("Field repetition not available"))
793    } else if repetition == 0 {
794        Ok(field)
795    } else {
796        Err(Hl7Error::new("Field repetition not available"))
797    }
798}
799
800/// Returns the number of repetitions for the field at `index` (`1` when the
801/// field does not repeat, `0` when the index is not a valid field reference).
802fn get_field_repetitions(segment: &Segment, index: &str) -> Result<usize, Hl7Error> {
803    let caps = match FIELD_REGEX.captures(index) {
804        Some(c) => c,
805        None => return Ok(0),
806    };
807
808    let field_index = caps[1]
809        .parse::<usize>()
810        .ok()
811        .and_then(|n| n.checked_sub(1))
812        .ok_or_else(|| Hl7Error::new("Invalid field index"))?;
813
814    let field = segment
815        .fields
816        .get(field_index)
817        .ok_or_else(|| Hl7Error::new("Field not available"))?;
818
819    if field.has_repetitions {
820        Ok(field.repetitions.len())
821    } else {
822        Ok(1)
823    }
824}
825
826/// Validates that a path split into `parts` is well-formed: a valid segment
827/// name, then a field index, then numeric component/subcomponent indices.
828fn validate_value_format(parts: &[&str]) -> bool {
829    if parts.is_empty() || !SEGMENT_REGEX.is_match(parts[0]) {
830        return false;
831    }
832
833    let mut is_valid = false;
834    for (i, part) in parts.iter().enumerate().skip(1) {
835        let matches = (i == 1 && FIELD_REGEX.is_match(part))
836            || (i > 1 && OTHER_REGEX.is_match(part));
837
838        if matches {
839            is_valid = true;
840        } else {
841            return false;
842        }
843    }
844
845    is_valid
846}
847
848/// Splits a multibyte `\X..\` hex escape that encodes a line break into one
849/// single-byte escape per pair, so the CR/LF bytes survive the round-trip.
850/// Sequences that don't encode a line break are returned unchanged.
851fn decompose_multibyte_hex(match_value: &str, hex: &str, esc: char) -> String {
852    if hex.len() <= 2 || !hex.len().is_multiple_of(2) || !contains_encoded_linebreak(hex) {
853        return match_value.to_string();
854    }
855
856    let mut result = String::with_capacity(match_value.len());
857    let mut i = 0;
858    while i + 2 <= hex.len() {
859        result.push(esc);
860        result.push('X');
861        result.push_str(&hex[i..i + 2]);
862        result.push(esc);
863        i += 2;
864    }
865
866    result
867}
868
869/// Whether any byte in the hex payload is an encoded CR or LF.
870fn contains_encoded_linebreak(hex: &str) -> bool {
871    (0..hex.len() / 2).any(|i| is_encoded_linebreak_byte(&hex[i * 2..i * 2 + 2]))
872}
873
874/// Whether a two-character hex byte is an encoded CR (`0D`) or LF (`0A`).
875fn is_encoded_linebreak_byte(hex: &str) -> bool {
876    matches!(hex, "0D" | "0A" | "0d" | "0a")
877}