Skip to main content

hl7kit/
message.rs

1//! The parsed message and its node views.
2
3use crate::escape::unescape;
4use crate::{Encoding, ParseError, Path};
5use std::borrow::Cow;
6use std::ops::Range;
7
8/// A byte range into [`Message::raw`].
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
10pub struct Span {
11    /// Inclusive start offset.
12    pub start: usize,
13    /// Exclusive end offset.
14    pub end: usize,
15}
16
17impl Span {
18    /// Create a span.
19    pub const fn new(start: usize, end: usize) -> Span {
20        Span { start, end }
21    }
22    /// Length in bytes.
23    pub const fn len(&self) -> usize {
24        self.end - self.start
25    }
26    /// Whether the span is empty.
27    pub const fn is_empty(&self) -> bool {
28        self.start == self.end
29    }
30    /// The span as a range, for slicing.
31    pub const fn range(&self) -> Range<usize> {
32        self.start..self.end
33    }
34    /// Slice `text` with this span. Returns an empty string when the span is
35    /// out of bounds or not on a character boundary rather than panicking.
36    pub fn slice<'a>(&self, text: &'a str) -> &'a str {
37        text.get(self.range()).unwrap_or("")
38    }
39}
40
41impl From<Span> for Range<usize> {
42    fn from(s: Span) -> Range<usize> {
43        s.range()
44    }
45}
46
47/// Something the parser tolerated but a reviewer may want to know about.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum Warning {
50    /// MSH-2 was malformed; the standard encoding characters were used instead.
51    EncodingFallback {
52        /// The MSH-2 text that was found.
53        found: String,
54    },
55    /// MLLP framing bytes (`0x0B`, `0x1C`) were present and ignored.
56    MllpFraming,
57    /// A UTF-8 byte order mark preceded MSH and was ignored.
58    ByteOrderMark,
59    /// Segment terminators were not `\r` (the standard) but `\n` or `\r\n`.
60    NonStandardTerminator,
61    /// The message carries more than one Study Instance UID and they
62    /// differ: several `IPC` segments, or `IPC`/`ZDS` against an `OBX`.
63    /// The first in precedence was used. Recorded on an
64    /// [`Order`](crate::order::Order), not on the message.
65    ConflictingStudyUid {
66        /// Span of the value that was taken.
67        first: Span,
68        /// Span of the first value that disagrees with it.
69        other: Span,
70    },
71}
72
73impl std::fmt::Display for Warning {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            Warning::EncodingFallback { found } => write!(
77                f,
78                "MSH-2 {found:?} is not a valid set of encoding characters; using ^~\\&"
79            ),
80            Warning::MllpFraming => write!(f, "MLLP framing bytes present and ignored"),
81            Warning::ByteOrderMark => write!(f, "UTF-8 byte order mark ignored"),
82            Warning::NonStandardTerminator => {
83                write!(f, "segments terminated by LF or CRLF instead of CR")
84            }
85            Warning::ConflictingStudyUid { first, other } => write!(
86                f,
87                "the message carries differing study instance UIDs (bytes {}..{} used, \
88                 bytes {}..{} differ)",
89                first.start, first.end, other.start, other.end
90            ),
91        }
92    }
93}
94
95#[derive(Debug, Clone)]
96struct SegmentNode {
97    name: Span,
98    span: Span,
99    /// Index 0 is the segment name; index `n` is field `n` (HL7 numbering).
100    fields: Vec<FieldNode>,
101}
102
103#[derive(Debug, Clone)]
104struct FieldNode {
105    span: Span,
106    repetitions: Vec<RepetitionNode>,
107}
108
109#[derive(Debug, Clone)]
110struct RepetitionNode {
111    span: Span,
112    components: Vec<ComponentNode>,
113}
114
115#[derive(Debug, Clone)]
116struct ComponentNode {
117    span: Span,
118    subcomponents: Vec<Span>,
119}
120
121/// A parsed HL7 v2 message.
122///
123/// The message owns a copy of the input text; all views borrow from it.
124#[derive(Debug, Clone)]
125pub struct Message {
126    raw: String,
127    encoding: Encoding,
128    segments: Vec<SegmentNode>,
129    warnings: Vec<Warning>,
130}
131
132/// MSH-9, the message type.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct MessageType<'m> {
135    /// MSH-9.1, e.g. `ORM`.
136    pub code: &'m str,
137    /// MSH-9.2, e.g. `O01`.
138    pub trigger: &'m str,
139    /// MSH-9.3, e.g. `ORM_O01`; empty when absent.
140    pub structure: &'m str,
141}
142
143impl Message {
144    /// Parse a message from text.
145    pub fn parse(text: impl Into<String>) -> Result<Message, ParseError> {
146        parse(text.into())
147    }
148
149    /// Parse a message from bytes that must be valid UTF-8.
150    pub fn parse_bytes(bytes: &[u8]) -> Result<Message, ParseError> {
151        match std::str::from_utf8(bytes) {
152            Ok(text) => parse(text.to_string()),
153            Err(e) => Err(ParseError::InvalidUtf8 {
154                valid_up_to: e.valid_up_to(),
155            }),
156        }
157    }
158
159    /// Parse a message from bytes, replacing invalid UTF-8 sequences. Spans
160    /// refer to the converted text available through [`Message::raw`], not to
161    /// the original bytes.
162    pub fn parse_lossy(bytes: &[u8]) -> Result<Message, ParseError> {
163        parse(String::from_utf8_lossy(bytes).into_owned())
164    }
165
166    /// The message text that all spans index into.
167    pub fn raw(&self) -> &str {
168        &self.raw
169    }
170
171    /// The encoding characters in effect.
172    pub fn encoding(&self) -> Encoding {
173        self.encoding
174    }
175
176    /// Tolerated irregularities in the input.
177    pub fn warnings(&self) -> &[Warning] {
178        &self.warnings
179    }
180
181    /// Number of segments.
182    pub fn segment_count(&self) -> usize {
183        self.segments.len()
184    }
185
186    /// All segments in order.
187    pub fn segments(&self) -> impl Iterator<Item = Segment<'_>> + '_ {
188        self.segments
189            .iter()
190            .enumerate()
191            .map(move |(index, node)| Segment {
192                msg: self,
193                node,
194                index,
195            })
196    }
197
198    /// The segment at position `index` (zero-based).
199    pub fn segment_at(&self, index: usize) -> Option<Segment<'_>> {
200        self.segments.get(index).map(|node| Segment {
201            msg: self,
202            node,
203            index,
204        })
205    }
206
207    /// The first segment with the given name (case-insensitive).
208    pub fn segment(&self, name: &str) -> Option<Segment<'_>> {
209        self.segments_named(name).next()
210    }
211
212    /// All segments with the given name (case-insensitive).
213    pub fn segments_named(&self, name: &str) -> impl Iterator<Item = Segment<'_>> + '_ {
214        let name = name.to_ascii_uppercase();
215        self.segments()
216            .filter(move |s| s.name().eq_ignore_ascii_case(&name))
217    }
218
219    /// Look up a value by path, e.g. `PID-3.1`. See [`Path`] for the syntax.
220    ///
221    /// Returns `None` when the path does not parse or the addressed element is
222    /// absent. Use [`Path::parse`] first to distinguish the two.
223    pub fn get(&self, path: &str) -> Option<&str> {
224        self.get_span(path).map(|s| s.slice(&self.raw))
225    }
226
227    /// Like [`Message::get`] but decodes escape sequences.
228    pub fn get_decoded(&self, path: &str) -> Option<Cow<'_, str>> {
229        self.get(path).map(|v| unescape(v, self.encoding))
230    }
231
232    /// The span of the element addressed by `path`.
233    pub fn get_span(&self, path: &str) -> Option<Span> {
234        Path::parse(path).ok().and_then(|p| self.resolve(&p))
235    }
236
237    /// Resolve a parsed path to a span.
238    pub fn resolve(&self, path: &Path) -> Option<Span> {
239        let seg = self
240            .segments_named(&path.segment)
241            .nth(path.occurrence.checked_sub(1)?)?;
242        let Some(field_no) = path.field else {
243            return Some(seg.span());
244        };
245        let field = seg.field(field_no)?;
246        let rep_no = path.repetition;
247        let Some(comp_no) = path.component else {
248            return match rep_no {
249                None => Some(field.span()),
250                Some(r) => field.repetition(r).map(|r| r.span()),
251            };
252        };
253        let rep = field.repetition(rep_no.unwrap_or(1))?;
254        let comp = rep.component(comp_no)?;
255        match path.subcomponent {
256            None => Some(comp.span()),
257            Some(s) => comp.subcomponent(s).map(|s| s.span()),
258        }
259    }
260
261    /// Decode escape sequences in `text` with this message's encoding.
262    pub fn decode<'a>(&self, text: &'a str) -> Cow<'a, str> {
263        unescape(text, self.encoding)
264    }
265
266    /// MSH-9, the message type.
267    pub fn message_type(&self) -> Option<MessageType<'_>> {
268        let field = self.segment("MSH")?.field(9)?;
269        Some(MessageType {
270            code: field.component(1).map(|c| c.value()).unwrap_or(""),
271            trigger: field.component(2).map(|c| c.value()).unwrap_or(""),
272            structure: field.component(3).map(|c| c.value()).unwrap_or(""),
273        })
274    }
275
276    /// MSH-10, the message control ID.
277    pub fn control_id(&self) -> Option<&str> {
278        self.get("MSH-10").filter(|v| !v.is_empty())
279    }
280
281    /// MSH-12.1, the HL7 version.
282    pub fn version(&self) -> Option<&str> {
283        self.get("MSH-12.1").filter(|v| !v.is_empty())
284    }
285}
286
287/// A segment view.
288#[derive(Clone, Copy)]
289pub struct Segment<'m> {
290    msg: &'m Message,
291    node: &'m SegmentNode,
292    index: usize,
293}
294
295impl<'m> Segment<'m> {
296    /// Segment name, e.g. `PID`.
297    pub fn name(&self) -> &'m str {
298        self.node.name.slice(&self.msg.raw)
299    }
300    /// Zero-based position within the message.
301    pub fn index(&self) -> usize {
302        self.index
303    }
304    /// Span of the whole segment line, without its terminator.
305    pub fn span(&self) -> Span {
306        self.node.span
307    }
308    /// The segment text.
309    pub fn text(&self) -> &'m str {
310        self.node.span.slice(&self.msg.raw)
311    }
312    /// Highest field number present.
313    pub fn field_count(&self) -> usize {
314        self.node.fields.len().saturating_sub(1)
315    }
316    /// Field `n` in HL7 numbering (one-based). For `MSH`, field 1 is the field
317    /// separator and field 2 the encoding characters, as the standard says.
318    pub fn field(&self, n: usize) -> Option<Field<'m>> {
319        if n == 0 {
320            return None;
321        }
322        self.node.fields.get(n).map(|node| Field {
323            msg: self.msg,
324            node,
325            number: n,
326        })
327    }
328    /// All fields, numbered from 1.
329    pub fn fields(&self) -> impl Iterator<Item = Field<'m>> + 'm {
330        let msg = self.msg;
331        self.node
332            .fields
333            .iter()
334            .enumerate()
335            .skip(1)
336            .map(move |(number, node)| Field { msg, node, number })
337    }
338}
339
340impl std::fmt::Debug for Segment<'_> {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        f.debug_struct("Segment")
343            .field("name", &self.name())
344            .field("index", &self.index)
345            .field("span", &self.node.span)
346            .finish()
347    }
348}
349
350/// A field view.
351#[derive(Clone, Copy)]
352pub struct Field<'m> {
353    msg: &'m Message,
354    node: &'m FieldNode,
355    number: usize,
356}
357
358impl<'m> Field<'m> {
359    /// One-based field number.
360    pub fn number(&self) -> usize {
361        self.number
362    }
363    /// Span of the whole field including all repetitions.
364    pub fn span(&self) -> Span {
365        self.node.span
366    }
367    /// Raw field text including separators of nested levels.
368    pub fn value(&self) -> &'m str {
369        self.node.span.slice(&self.msg.raw)
370    }
371    /// Field text with escape sequences decoded.
372    pub fn decoded(&self) -> Cow<'m, str> {
373        unescape(self.value(), self.msg.encoding)
374    }
375    /// True when the field has no content.
376    pub fn is_empty(&self) -> bool {
377        self.node.span.is_empty()
378    }
379    /// True when the field is the HL7 null value `""`.
380    pub fn is_null(&self) -> bool {
381        self.value() == "\"\""
382    }
383    /// Number of repetitions (at least 1).
384    pub fn repetition_count(&self) -> usize {
385        self.node.repetitions.len()
386    }
387    /// Repetition `n` (one-based).
388    pub fn repetition(&self, n: usize) -> Option<Repetition<'m>> {
389        let node = self.node.repetitions.get(n.checked_sub(1)?)?;
390        Some(Repetition {
391            msg: self.msg,
392            node,
393            index: n,
394        })
395    }
396    /// All repetitions.
397    pub fn repetitions(&self) -> impl Iterator<Item = Repetition<'m>> + 'm {
398        let msg = self.msg;
399        self.node
400            .repetitions
401            .iter()
402            .enumerate()
403            .map(move |(i, node)| Repetition {
404                msg,
405                node,
406                index: i + 1,
407            })
408    }
409    /// Component `n` of the first repetition.
410    pub fn component(&self, n: usize) -> Option<Component<'m>> {
411        self.repetition(1)?.component(n)
412    }
413}
414
415impl std::fmt::Debug for Field<'_> {
416    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417        f.debug_struct("Field")
418            .field("number", &self.number)
419            .field("value", &self.value())
420            .field("span", &self.node.span)
421            .finish()
422    }
423}
424
425/// A repetition view.
426#[derive(Clone, Copy)]
427pub struct Repetition<'m> {
428    msg: &'m Message,
429    node: &'m RepetitionNode,
430    index: usize,
431}
432
433impl<'m> Repetition<'m> {
434    /// One-based repetition index.
435    pub fn index(&self) -> usize {
436        self.index
437    }
438    /// Span of this repetition.
439    pub fn span(&self) -> Span {
440        self.node.span
441    }
442    /// Raw text of this repetition.
443    pub fn value(&self) -> &'m str {
444        self.node.span.slice(&self.msg.raw)
445    }
446    /// Text with escape sequences decoded.
447    pub fn decoded(&self) -> Cow<'m, str> {
448        unescape(self.value(), self.msg.encoding)
449    }
450    /// Number of components (at least 1).
451    pub fn component_count(&self) -> usize {
452        self.node.components.len()
453    }
454    /// Component `n` (one-based).
455    pub fn component(&self, n: usize) -> Option<Component<'m>> {
456        let node = self.node.components.get(n.checked_sub(1)?)?;
457        Some(Component {
458            msg: self.msg,
459            node,
460            index: n,
461        })
462    }
463    /// All components.
464    pub fn components(&self) -> impl Iterator<Item = Component<'m>> + 'm {
465        let msg = self.msg;
466        self.node
467            .components
468            .iter()
469            .enumerate()
470            .map(move |(i, node)| Component {
471                msg,
472                node,
473                index: i + 1,
474            })
475    }
476}
477
478impl std::fmt::Debug for Repetition<'_> {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        f.debug_struct("Repetition")
481            .field("index", &self.index)
482            .field("value", &self.value())
483            .finish()
484    }
485}
486
487/// A component view.
488#[derive(Clone, Copy)]
489pub struct Component<'m> {
490    msg: &'m Message,
491    node: &'m ComponentNode,
492    index: usize,
493}
494
495impl<'m> Component<'m> {
496    /// One-based component index.
497    pub fn index(&self) -> usize {
498        self.index
499    }
500    /// Span of this component.
501    pub fn span(&self) -> Span {
502        self.node.span
503    }
504    /// Raw text of this component.
505    pub fn value(&self) -> &'m str {
506        self.node.span.slice(&self.msg.raw)
507    }
508    /// Text with escape sequences decoded.
509    pub fn decoded(&self) -> Cow<'m, str> {
510        unescape(self.value(), self.msg.encoding)
511    }
512    /// Number of subcomponents (at least 1).
513    pub fn subcomponent_count(&self) -> usize {
514        self.node.subcomponents.len()
515    }
516    /// Subcomponent `n` (one-based).
517    pub fn subcomponent(&self, n: usize) -> Option<Subcomponent<'m>> {
518        let span = *self.node.subcomponents.get(n.checked_sub(1)?)?;
519        Some(Subcomponent {
520            msg: self.msg,
521            span,
522            index: n,
523        })
524    }
525    /// All subcomponents.
526    pub fn subcomponents(&self) -> impl Iterator<Item = Subcomponent<'m>> + 'm {
527        let msg = self.msg;
528        self.node
529            .subcomponents
530            .iter()
531            .enumerate()
532            .map(move |(i, &span)| Subcomponent {
533                msg,
534                span,
535                index: i + 1,
536            })
537    }
538}
539
540impl std::fmt::Debug for Component<'_> {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        f.debug_struct("Component")
543            .field("index", &self.index)
544            .field("value", &self.value())
545            .finish()
546    }
547}
548
549/// A subcomponent view.
550#[derive(Clone, Copy)]
551pub struct Subcomponent<'m> {
552    msg: &'m Message,
553    span: Span,
554    index: usize,
555}
556
557impl<'m> Subcomponent<'m> {
558    /// One-based subcomponent index.
559    pub fn index(&self) -> usize {
560        self.index
561    }
562    /// Span of this subcomponent.
563    pub fn span(&self) -> Span {
564        self.span
565    }
566    /// Raw text.
567    pub fn value(&self) -> &'m str {
568        self.span.slice(&self.msg.raw)
569    }
570    /// Text with escape sequences decoded.
571    pub fn decoded(&self) -> Cow<'m, str> {
572        unescape(self.value(), self.msg.encoding)
573    }
574}
575
576impl std::fmt::Debug for Subcomponent<'_> {
577    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578        f.debug_struct("Subcomponent")
579            .field("index", &self.index)
580            .field("value", &self.value())
581            .finish()
582    }
583}
584
585// ---------------------------------------------------------------------------
586// Parser
587// ---------------------------------------------------------------------------
588
589const MLLP_START: u8 = 0x0B;
590const MLLP_END: u8 = 0x1C;
591const BOM: &[u8] = b"\xEF\xBB\xBF";
592
593fn parse(raw: String) -> Result<Message, ParseError> {
594    let bytes = raw.as_bytes();
595    let mut warnings = Vec::new();
596
597    let mut lines = split_lines(bytes, &mut warnings);
598    // Drop empty and whitespace-only lines, strip MLLP framing.
599    lines.retain_mut(|line| {
600        let mut s = line.start;
601        let mut e = line.end;
602        loop {
603            if s < e && (bytes[s] == MLLP_START || bytes[s].is_ascii_whitespace()) {
604                if bytes[s] == MLLP_START {
605                    push_unique(&mut warnings, Warning::MllpFraming);
606                }
607                s += 1;
608            } else if bytes[s..e].starts_with(BOM) {
609                push_unique(&mut warnings, Warning::ByteOrderMark);
610                s += BOM.len();
611            } else {
612                break;
613            }
614        }
615        while e > s && (bytes[e - 1] == MLLP_END || bytes[e - 1].is_ascii_whitespace()) {
616            if bytes[e - 1] == MLLP_END {
617                push_unique(&mut warnings, Warning::MllpFraming);
618            }
619            e -= 1;
620        }
621        *line = Span::new(s, e);
622        s < e
623    });
624
625    let Some(first) = lines.first().copied() else {
626        return Err(ParseError::Empty);
627    };
628    let first_text = first.slice(&raw);
629    if !first_text.starts_with("MSH") {
630        return Err(ParseError::MissingMsh {
631            found: truncate(first_text, 16),
632        });
633    }
634    if first.len() < 4 {
635        return Err(ParseError::MalformedMsh {
636            reason: "no field separator after MSH",
637        });
638    }
639    let fs = bytes[first.start + 3];
640    if !fs.is_ascii() || fs.is_ascii_alphanumeric() || fs.is_ascii_whitespace() {
641        return Err(ParseError::MalformedMsh {
642            reason: "byte after MSH is not a usable field separator",
643        });
644    }
645    let enc_start = first.start + 4;
646    let enc_end = bytes[enc_start..first.end]
647        .iter()
648        .position(|&b| b == fs)
649        .map(|p| enc_start + p)
650        .unwrap_or(first.end);
651    let enc_bytes = &bytes[enc_start..enc_end];
652    let encoding = match Encoding::from_msh2(fs, enc_bytes) {
653        Some(e) => e,
654        None => {
655            warnings.push(Warning::EncodingFallback {
656                found: String::from_utf8_lossy(enc_bytes).into_owned(),
657            });
658            Encoding::with_field(fs)
659        }
660    };
661
662    let mut segments = Vec::with_capacity(lines.len());
663    for (i, line) in lines.iter().enumerate() {
664        let name_len = bytes[line.start..line.end]
665            .iter()
666            .position(|&b| b == fs)
667            .unwrap_or(line.len());
668        let name = Span::new(line.start, line.start + name_len);
669        let name_bytes = &bytes[name.range()];
670        if name_bytes.len() != 3 || !name_bytes.iter().all(u8::is_ascii_alphanumeric) {
671            return Err(ParseError::MalformedSegment {
672                line: i + 1,
673                text: truncate(line.slice(&raw), 40),
674            });
675        }
676        let mut fields = vec![opaque_field(name)];
677        if i == 0 {
678            // MSH: field 1 is the separator, field 2 the encoding characters,
679            // and neither is split on any delimiter.
680            fields.push(opaque_field(Span::new(line.start + 3, line.start + 4)));
681            fields.push(opaque_field(Span::new(enc_start, enc_end)));
682            if enc_end < line.end {
683                for span in split_on(bytes, Span::new(enc_end + 1, line.end), fs) {
684                    fields.push(parse_field(bytes, span, encoding));
685                }
686            }
687        } else if name.end < line.end {
688            for span in split_on(bytes, Span::new(name.end + 1, line.end), fs) {
689                fields.push(parse_field(bytes, span, encoding));
690            }
691        }
692        segments.push(SegmentNode {
693            name,
694            span: *line,
695            fields,
696        });
697    }
698
699    Ok(Message {
700        raw,
701        encoding,
702        segments,
703        warnings,
704    })
705}
706
707fn split_lines(bytes: &[u8], warnings: &mut Vec<Warning>) -> Vec<Span> {
708    let mut lines = Vec::new();
709    let mut start = 0;
710    let mut i = 0;
711    while i < bytes.len() {
712        match bytes[i] {
713            b'\r' => {
714                lines.push(Span::new(start, i));
715                if bytes.get(i + 1) == Some(&b'\n') {
716                    i += 1;
717                    push_unique(warnings, Warning::NonStandardTerminator);
718                }
719                start = i + 1;
720            }
721            b'\n' => {
722                lines.push(Span::new(start, i));
723                push_unique(warnings, Warning::NonStandardTerminator);
724                start = i + 1;
725            }
726            _ => {}
727        }
728        i += 1;
729    }
730    if start < bytes.len() {
731        lines.push(Span::new(start, bytes.len()));
732    }
733    lines
734}
735
736fn split_on(bytes: &[u8], span: Span, sep: u8) -> Vec<Span> {
737    let mut out = Vec::new();
738    let mut start = span.start;
739    for (i, &b) in bytes[span.range()].iter().enumerate() {
740        if b == sep {
741            out.push(Span::new(start, span.start + i));
742            start = span.start + i + 1;
743        }
744    }
745    out.push(Span::new(start, span.end));
746    out
747}
748
749fn opaque_field(span: Span) -> FieldNode {
750    FieldNode {
751        span,
752        repetitions: vec![RepetitionNode {
753            span,
754            components: vec![ComponentNode {
755                span,
756                subcomponents: vec![span],
757            }],
758        }],
759    }
760}
761
762fn parse_field(bytes: &[u8], span: Span, enc: Encoding) -> FieldNode {
763    let repetitions = split_on(bytes, span, enc.repetition)
764        .into_iter()
765        .map(|rep| RepetitionNode {
766            span: rep,
767            components: split_on(bytes, rep, enc.component)
768                .into_iter()
769                .map(|comp| ComponentNode {
770                    span: comp,
771                    subcomponents: split_on(bytes, comp, enc.subcomponent),
772                })
773                .collect(),
774        })
775        .collect();
776    FieldNode { span, repetitions }
777}
778
779fn push_unique(warnings: &mut Vec<Warning>, w: Warning) {
780    if !warnings.contains(&w) {
781        warnings.push(w);
782    }
783}
784
785fn truncate(text: &str, max: usize) -> String {
786    if text.len() <= max {
787        return text.to_string();
788    }
789    let mut end = max;
790    while !text.is_char_boundary(end) {
791        end -= 1;
792    }
793    format!("{}…", &text[..end])
794}