Skip to main content

edifact_rs/
tokenizer.rs

1//! EDIFACT tokenizer — splits raw bytes into typed tokens.
2//!
3//! Respects UNA service string advice for non-default delimiters.
4//! Uses `memchr` for fast delimiter scanning (no byte-by-byte inner loops).
5
6use crate::{error::EdifactError, model::Span};
7use memchr::{memchr, memchr2, memchr3};
8
9/// EDIFACT service string advice — the six characters of the `UNA`
10/// (ISO 9735-1 Annex B).
11///
12/// The five *active* service characters — component separator, element
13/// separator, release character, repetition separator, and segment terminator —
14/// are what [`is_valid`][Self::is_valid] enforces: printable non-alphanumeric
15/// ASCII, mutually distinct, so a collision between the repetition separator and
16/// any other delimiter, or a delimiter that would clash with segment-tag
17/// characters, is caught at UNA parse time.
18///
19/// The decimal mark is deliberately **not** in that set; see
20/// [`decimal_mark`][Self::decimal_mark].
21///
22/// # Defaults
23///
24/// ISO 9735-1 §5.1 fixes the defaults as `:` (component), `+` (element), `?`
25/// (release), `*` (repetition), `'` (terminator). Syntax version 4 is the
26/// version that defines the repetition separator at all: in versions 1–3 that
27/// UNA position is reserved and carries a space. [`Default`] is therefore the
28/// version-agnostic reading — everything per §5.1 **except** repetition, which
29/// stays inactive until something says the interchange is version 4. See
30/// [`for_syntax_version`][Self::for_syntax_version].
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct ServiceStringAdvice {
33    /// Data element separator (default `+`; `UNA` position 020)
34    pub element_sep: u8,
35    /// Component data element separator (default `:`; `UNA` position 010)
36    pub component_sep: u8,
37    /// Release character (default `?`; `UNA` position 040)
38    pub release_char: u8,
39    /// Decimal mark (`UNA` position 030), default `.`.
40    ///
41    /// **Ignored on receipt.** ISO 9735-1 Annex B keeps this position only for
42    /// upward compatibility with earlier syntax versions and states that the
43    /// character transferred here "shall be ignored by the recipient"; §10
44    /// instead allows the full stop *or* the comma per individual numeric value.
45    /// It is therefore neither validated nor used for splitting — it is
46    /// preserved so a writer can round-trip the `UNA` it was given, and so
47    /// [`DecimalFloat`][crate::ser::DecimalFloat] has a house style to format
48    /// with.
49    pub decimal_mark: u8,
50    /// Repetition separator (`UNA` position 050), introduced by syntax version 4.
51    ///
52    /// A space (`0x20`) means **not used**: that is what versions 1–3 put in this
53    /// reserved position, and version 4 forbids a space here precisely because
54    /// the position now carries a real separator.
55    ///
56    /// When the separator is active the tokenizer splits on it: a data element
57    /// carrying `ON:1*ON:2` becomes one element with two repetitions rather than
58    /// one repetition whose second component is the literal text `1*ON`.  Use
59    /// [`is_repetition_active`][Self::is_repetition_active] to test for this.
60    pub repetition_sep: u8,
61    /// Segment terminator (default `'`; `UNA` position 060)
62    pub segment_term: u8,
63}
64
65impl Default for ServiceStringAdvice {
66    fn default() -> Self {
67        Self {
68            element_sep: b'+',
69            component_sep: b':',
70            release_char: b'?',
71            decimal_mark: b'.',
72            // Inactive until the interchange is known to be syntax version 4 —
73            // see `for_syntax_version`.  Defaulting to the §5.1 asterisk would
74            // split every unescaped `*` in a version 3 interchange, where `*` is
75            // an ordinary level A character and not a service character at all.
76            repetition_sep: b' ',
77            segment_term: b'\'',
78        }
79    }
80}
81
82impl ServiceStringAdvice {
83    /// Read the service characters an interchange actually uses.
84    ///
85    /// Two sources, in priority order:
86    ///
87    /// 1. A leading `UNA`, which states all six characters explicitly.
88    /// 2. Otherwise the ISO 9735-1 §5.1 defaults, with the repetition separator
89    ///    resolved from the syntax version in `UNB` S001 DE 0002 — see
90    ///    [`for_syntax_version`][Self::for_syntax_version].
91    ///
92    /// # Errors
93    ///
94    /// [`EdifactError::InvalidUna`] when a `UNA` is present but its active
95    /// service characters are not mutually distinct printable non-alphanumeric
96    /// ASCII.  See [`is_valid`][Self::is_valid] for the exact rule.
97    ///
98    /// This is the **safe, default constructor** — always use this for input from
99    /// an external source.  For trusted or internal use where delimiter uniqueness
100    /// is already guaranteed, use [`from_bytes_unchecked`](Self::from_bytes_unchecked).
101    pub fn from_bytes(input: &[u8]) -> Result<Self, crate::error::EdifactError> {
102        let ssa = Self::from_bytes_unchecked(input);
103        if !ssa.is_valid() {
104            return Err(crate::error::EdifactError::InvalidUna);
105        }
106        Ok(ssa)
107    }
108
109    /// Parse a UNA header from the beginning of an EDIFACT interchange **without**
110    /// validating delimiter uniqueness or printability.
111    ///
112    /// When no `UNA` is present the §5.1 defaults apply, with the repetition
113    /// separator taken from the syntax version declared in `UNB` S001 DE 0002.
114    ///
115    /// # When to use
116    ///
117    /// Use this only for trusted internal data (e.g. round-tripping data where
118    /// the UNA invariant is already guaranteed) or in fuzz/property tests that
119    /// intentionally explore degenerate delimiter combinations.
120    ///
121    /// For any external or user-provided input, prefer [`from_bytes`](Self::from_bytes)
122    /// which validates delimiter uniqueness and rejects invalid bytes.
123    pub fn from_bytes_unchecked(input: &[u8]) -> Self {
124        // A byte-order mark and stray newlines ahead of the header are common in
125        // files that have been through a Windows editor or an FTP hop; ISO 9735
126        // says nothing about them, and refusing to look past them would reject
127        // interchanges that are otherwise perfectly conformant.
128        let input = &input[prologue_len(input)..];
129        // UNA is 9 bytes: "UNA" + 6 service chars
130        if input.len() >= 9 && &input[..3] == b"UNA" {
131            Self {
132                component_sep: input[3],
133                element_sep: input[4],
134                decimal_mark: input[5],
135                release_char: input[6],
136                repetition_sep: input[7],
137                segment_term: input[8],
138            }
139        } else {
140            Self::for_syntax_version(sniff_syntax_version(input))
141        }
142    }
143
144    /// The ISO 9735-1 §5.1 defaults for a given syntax version.
145    ///
146    /// The repetition separator is the only character the version decides:
147    /// version 4 introduced it as `*`, and versions 1–3 have no such service
148    /// character at all — that `UNA` position is reserved and carries a space.
149    /// Splitting on `*` in a version 3 interchange would corrupt every value
150    /// containing one, since `*` is an ordinary level A character there.
151    ///
152    /// `None` means the version could not be determined (no `UNB`, or an
153    /// unreadable one) and is treated as "not version 4".
154    ///
155    /// # Example
156    ///
157    /// ```
158    /// use edifact_rs::ServiceStringAdvice;
159    ///
160    /// assert!(ServiceStringAdvice::for_syntax_version(Some(4)).is_repetition_active());
161    /// assert!(!ServiceStringAdvice::for_syntax_version(Some(3)).is_repetition_active());
162    /// assert!(!ServiceStringAdvice::for_syntax_version(None).is_repetition_active());
163    /// ```
164    #[must_use]
165    pub const fn for_syntax_version(version: Option<u8>) -> Self {
166        Self {
167            element_sep: b'+',
168            component_sep: b':',
169            release_char: b'?',
170            decimal_mark: b'.',
171            repetition_sep: match version {
172                Some(4) => b'*',
173                _ => b' ',
174            },
175            segment_term: b'\'',
176        }
177    }
178
179    /// Return `true` if all **active** service characters are mutually distinct
180    /// and printable, non-alphanumeric ASCII.
181    ///
182    /// The active set is the component separator, element separator, release
183    /// character, segment terminator, and — when it is not the space "not used"
184    /// sentinel — the repetition separator.  Each must be in `0x21..=0x7E`
185    /// excluding `0-9A-Za-z`, and all must differ pairwise.
186    ///
187    /// Alphanumerics are excluded because segment tags are always three ASCII
188    /// uppercase letters written verbatim (a tag cannot be escaped).  A delimiter
189    /// such as `N` would make `NAD` unrepresentable — the writer would emit a
190    /// premature terminator and the result would not reparse.  High bytes
191    /// (`>= 0x80`) are rejected because they would bisect multi-byte UTF-8
192    /// sequences, and DEL (`0x7F`) is a control character.
193    ///
194    /// The **decimal mark is not checked at all**: ISO 9735-1 Annex B states that
195    /// the character in that position "shall be ignored by the recipient", and is
196    /// the one position where the standard permits a space.  Rejecting a `UNA`
197    /// over a character the standard tells receivers to ignore would fail
198    /// conformant interchanges for nothing.
199    ///
200    /// # Example
201    ///
202    /// ```
203    /// use edifact_rs::ServiceStringAdvice;
204    ///
205    /// // A duplicated *active* character is fatal …
206    /// assert!(ServiceStringAdvice::from_bytes(b"UNA::.? '").is_err());
207    /// // … but the ignored decimal-mark slot may hold anything, even a space.
208    /// assert!(ServiceStringAdvice::from_bytes(b"UNA:+ ? '")?.is_repetition_active() == false);
209    /// # Ok::<(), edifact_rs::EdifactError>(())
210    /// ```
211    pub fn is_valid(&self) -> bool {
212        let printable_ascii = |b: u8| (0x21..=0x7E).contains(&b) && !b.is_ascii_alphanumeric();
213        // The decimal mark is ignored on receipt, so the only requirement is
214        // that it stay a single graphic ASCII byte — Annex B types it `an1`, and
215        // space is explicitly permitted in this one position.
216        if !(0x20..=0x7E).contains(&self.decimal_mark) {
217            return false;
218        }
219        let active: [u8; 5] = [
220            self.component_sep,
221            self.element_sep,
222            self.release_char,
223            self.segment_term,
224            self.repetition_sep,
225        ];
226        // The repetition separator occupies the last slot and drops out of both
227        // checks when it holds the "not used" space.
228        let active = &active[..if self.is_repetition_active() { 5 } else { 4 }];
229        active.iter().all(|&b| printable_ascii(b))
230            && (0..active.len()).all(|i| active[i + 1..].iter().all(|&other| active[i] != other))
231    }
232
233    /// Returns `true` when this interchange declares a usable repetition
234    /// separator (`UNA` position 050, syntax version 4).
235    ///
236    /// A space there means "not used" — the reserved value carried by syntax
237    /// versions 1–3 — so it reports `false` and the tokenizer never splits on it.
238    ///
239    /// # Example
240    ///
241    /// ```
242    /// use edifact_rs::ServiceStringAdvice;
243    ///
244    /// assert!(!ServiceStringAdvice::default().is_repetition_active());
245    /// assert!(ServiceStringAdvice::from_bytes(b"UNA:+.?*'")?.is_repetition_active());
246    /// # Ok::<(), edifact_rs::EdifactError>(())
247    /// ```
248    #[inline]
249    #[must_use]
250    pub const fn is_repetition_active(&self) -> bool {
251        self.repetition_sep != b' '
252    }
253}
254
255/// The UTF-8 byte order mark, which real-world EDIFACT files acquire from
256/// editors and transfer tooling even though ISO 9735 never mentions one.
257pub(crate) const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
258
259/// Length of the non-EDIFACT prologue at the head of an interchange.
260///
261/// Covers a UTF-8 byte order mark and any run of ASCII whitespace before the
262/// first service segment. Nothing in ISO 9735 authorises either, but both turn
263/// up constantly, and the alternative — rejecting the interchange with
264/// `InvalidSegmentTag("\u{feff}UNB")` — points the reader at the wrong problem.
265#[inline]
266pub(crate) fn prologue_len(input: &[u8]) -> usize {
267    let mut pos = usize::from(input.starts_with(&UTF8_BOM)) * 3;
268    while pos < input.len() && matches!(input[pos], b' ' | b'\t' | b'\r' | b'\n') {
269        pos += 1;
270    }
271    pos
272}
273
274/// Whether `tag` is a syntactically valid segment tag.
275///
276/// Exactly three ASCII uppercase letters. Both the tokenizer and the writer
277/// consult this, so what the writer emits is always something the parser
278/// accepts: a tag is written verbatim and cannot be release-escaped, so a
279/// lowercase, mis-sized, or delimiter-bearing one produces bytes that do not
280/// read back as the segment they were written from.
281#[inline]
282pub(crate) fn is_valid_segment_tag(tag: &str) -> bool {
283    tag.len() == 3 && tag.bytes().all(|b| b.is_ascii_uppercase())
284}
285
286/// Read the syntax version number (`UNB` S001 DE 0002) out of raw bytes.
287///
288/// Deliberately byte-level and deliberately tiny: this runs *before* the
289/// delimiters are settled, so it can only assume what ISO 9735-1 §6 guarantees —
290/// that everything up to and including S001 is ISO/IEC 646 — and the §5.1
291/// default separators, which are the only ones in play when no `UNA` said
292/// otherwise.
293///
294/// Returns `None` for input with no readable `UNB` S001.
295fn sniff_syntax_version(input: &[u8]) -> Option<u8> {
296    let pos = prologue_len(input);
297    // `UNB+` — the element separator is the §5.1 default, because a UNA that
298    // changed it would have been used instead of this function.
299    if input.len() < pos + 4 || &input[pos..pos + 3] != b"UNB" || input[pos + 3] != b'+' {
300        return None;
301    }
302    // S001 = `<identifier>:<version>[:…]`; the version is component 2.
303    let s001 = &input[pos + 4..];
304    let end = s001
305        .iter()
306        .position(|&b| b == b'+' || b == b'\'')
307        .unwrap_or(s001.len());
308    let mut components = s001[..end].split(|&b| b == b':');
309    let _identifier = components.next()?;
310    match components.next()? {
311        [digit @ b'1'..=b'9'] => Some(digit - b'0'),
312        _ => None,
313    }
314}
315
316/// Token produced by [`Tokenizer`].
317#[derive(Debug, Clone, PartialEq, Eq)]
318pub enum Token<'a> {
319    /// 3-character segment tag (e.g. `"BGM"`)
320    SegmentTag {
321        /// Raw tag value.
322        value: &'a str,
323        /// Source span of the tag.
324        span: Span,
325    },
326    /// Data element value (between element separators)
327    DataElement {
328        /// Raw element value.
329        value: &'a str,
330        /// Source span of the element value.
331        span: Span,
332    },
333    /// Component within a composite data element (between component separators)
334    ComponentElement {
335        /// Raw component value.
336        value: &'a str,
337        /// Source span of the component value.
338        span: Span,
339    },
340    /// First component of a further repetition of the current data element
341    /// (ISO 9735-1 §8.6).
342    ///
343    /// Only produced when the active [`ServiceStringAdvice`] declares a
344    /// repetition separator — see
345    /// [`is_repetition_active`][ServiceStringAdvice::is_repetition_active].
346    RepeatElement {
347        /// Raw value of the repetition's first component.
348        value: &'a str,
349        /// Source span of the value.
350        span: Span,
351    },
352    /// Segment terminator — signals the end of a segment
353    SegmentTerminator {
354        /// Source span of the segment terminator byte.
355        span: Span,
356    },
357}
358
359#[derive(Debug)]
360pub(crate) struct RawSegment {
361    pub(crate) bytes: Vec<u8>,
362    pub(crate) start_offset: usize,
363}
364
365/// Zero-copy tokenizer over a byte slice.
366///
367/// Yields `Token` values, each borrowing from the original input.
368///
369/// # Segment size guard
370///
371/// The default constructor [`Tokenizer::new`] enforces a **64 KiB** per-segment
372/// limit, which is sufficient for all well-formed EDIFACT interchanges and guards
373/// against adversarially crafted inputs that omit segment terminators.
374/// Use [`Tokenizer::with_limit`] to raise or lower this threshold, or
375/// [`Tokenizer::unlimited`] to remove it entirely (trusted / pre-validated input only).
376pub struct Tokenizer<'a> {
377    input: &'a [u8],
378    pos: usize,
379    ssa: ServiceStringAdvice,
380    state: TokState,
381    /// Maximum allowed segment byte length (tag + elements, **excluding** the
382    /// segment terminator byte itself).  Checked in `read_value` and `read_tag`.
383    /// `usize::MAX` = unlimited.
384    max_segment_bytes: usize,
385    /// Byte position where the current segment started (set in `read_tag`).
386    segment_start: usize,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390enum TokState {
391    /// Expecting a segment tag next
392    ExpectTag,
393    /// Inside a segment; next byte could be element or component sep, release, or terminator
394    InSegment,
395}
396
397impl<'a> Tokenizer<'a> {
398    /// Return the byte offset of the first segment tag in `input`.
399    ///
400    /// Skips the [`prologue_len`] prologue (byte order mark plus leading
401    /// whitespace) and, if a `UNA` service string advice follows it, the nine
402    /// bytes of that header.
403    ///
404    /// Only correct for a slice that starts at the head of an interchange.
405    /// A slice holding a single already-delimited segment must use
406    /// [`Tokenizer::for_segment`], because `UNA` is also a syntactically valid
407    /// segment tag and skipping nine bytes of it corrupts the parse.
408    #[inline]
409    fn una_start_pos(input: &[u8]) -> usize {
410        let start = prologue_len(input);
411        if input.len() >= start + 9 && &input[start..start + 3] == b"UNA" {
412            start + 9
413        } else {
414            start
415        }
416    }
417
418    /// Construct a tokenizer over a slice that holds **one already-delimited
419    /// segment**, with no interchange header to skip.
420    ///
421    /// The whole-interchange constructors treat a leading `b"UNA"` as the
422    /// service string advice and jump nine bytes past it.  The reader paths
423    /// re-tokenize each segment from its own slice, where that heuristic is
424    /// wrong: `UNA` is three ASCII uppercase letters and therefore a legal
425    /// segment tag, so `UNA+XXXXXX'` parsed cleanly from a byte slice but was
426    /// rejected as `InvalidSegmentTag` when the identical bytes arrived through
427    /// a reader.
428    #[must_use]
429    pub fn for_segment(
430        input: &'a [u8],
431        ssa: ServiceStringAdvice,
432        max_segment_bytes: usize,
433    ) -> Self {
434        Self {
435            input,
436            pos: 0,
437            ssa,
438            state: TokState::ExpectTag,
439            max_segment_bytes,
440            segment_start: 0,
441        }
442    }
443
444    /// Construct a tokenizer with the default 64 KiB segment-size limit.
445    ///
446    /// If a single segment's byte length exceeds 65 536 bytes, the iterator
447    /// returns [`EdifactError::SegmentTooLong`].  This guards against
448    /// pathological or adversarially crafted inputs that omit segment
449    /// terminators and would otherwise cause unbounded scanning.
450    ///
451    /// Call [`Tokenizer::unlimited`] if you deliberately need to process
452    /// segments larger than 64 KiB, or [`Tokenizer::with_limit`] to supply a
453    /// custom bound.
454    pub fn new(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
455        Self::with_limit(input, ssa, 65_536)
456    }
457
458    /// Construct a tokenizer with **no** segment-size limit.
459    ///
460    /// # Security warning
461    ///
462    /// This constructor imposes **no upper bound** on how many bytes a single
463    /// segment may consume.  For untrusted or adversarially crafted input a
464    /// missing segment terminator can cause the tokenizer to scan the entire
465    /// input before returning an error.  Prefer [`Tokenizer::new`] (64 KiB
466    /// limit) or [`Tokenizer::with_limit`] for untrusted sources.
467    #[must_use]
468    pub fn unlimited(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
469        Self {
470            input,
471            pos: Self::una_start_pos(input),
472            ssa,
473            state: TokState::ExpectTag,
474            max_segment_bytes: usize::MAX,
475            segment_start: 0,
476        }
477    }
478
479    /// Construct a tokenizer with a segment-size limit.
480    ///
481    /// If a single segment's byte length (from the start of the tag to the end
482    /// of the last value, not including the terminator itself) exceeds `limit`,
483    /// the iterator returns [`EdifactError::SegmentTooLong`].
484    ///
485    /// # Examples
486    ///
487    /// ```
488    /// use edifact_rs::{ServiceStringAdvice, Tokenizer};
489    ///
490    /// let input = b"BGM+220+PO-4711+9'";
491    /// let ssa = ServiceStringAdvice::default();
492    /// let tokens: Vec<_> = Tokenizer::with_limit(input, ssa, 64)
493    ///     .collect::<Result<_, _>>()
494    ///     .unwrap();
495    /// assert!(!tokens.is_empty());
496    /// ```
497    pub fn with_limit(input: &'a [u8], ssa: ServiceStringAdvice, max_segment_bytes: usize) -> Self {
498        Self {
499            input,
500            pos: Self::una_start_pos(input),
501            ssa,
502            state: TokState::ExpectTag,
503            max_segment_bytes,
504            segment_start: 0,
505        }
506    }
507
508    /// Current byte position in the input.
509    #[inline]
510    pub fn position(&self) -> usize {
511        self.pos
512    }
513
514    /// Return the service string advice active for this tokenizer.
515    #[inline]
516    pub fn service_string_advice(&self) -> ServiceStringAdvice {
517        self.ssa
518    }
519
520    /// Consume leading whitespace / CR / LF between segments (not inside data values).
521    fn skip_inter_segment_whitespace(&mut self) {
522        while self.pos < self.input.len() {
523            match self.input[self.pos] {
524                b' ' | b'\t' | b'\r' | b'\n' => self.pos += 1,
525                _ => break,
526            }
527        }
528    }
529
530    /// Read a field value starting at `self.pos`, advancing past the value.
531    ///
532    /// Recognises the release character (`?` by default) and returns the raw
533    /// slice including release sequences. The parser layer resolves them.
534    ///
535    /// Uses `memchr3` to bulk-scan over non-special bytes between hits, only
536    /// falling back to a per-byte step when a release character is encountered.
537    /// Offset of the next segment terminator — or repetition separator, when the
538    /// interchange declares one — at or after `from`, searching within `window`.
539    ///
540    /// `memchr` tops out at three needles and `read_value` already spends those
541    /// on the element separator, component separator, and release character, so
542    /// the remaining one or two needles are searched separately and cached.
543    #[inline]
544    fn find_stop(&self, window: &[u8]) -> Option<usize> {
545        if self.ssa.is_repetition_active() {
546            memchr2(self.ssa.segment_term, self.ssa.repetition_sep, window)
547        } else {
548            memchr(self.ssa.segment_term, window)
549        }
550    }
551
552    fn read_value(&mut self) -> Result<(&'a str, Span), EdifactError> {
553        let start = self.pos;
554        let (elem, comp, release) = (
555            self.ssa.element_sep,
556            self.ssa.component_sep,
557            self.ssa.release_char,
558        );
559        // Absolute cap on how far this value may extend before the per-segment
560        // byte guard trips.  Bounding the scan window here (rather than only
561        // checking the length after the loop) keeps adversarial input that omits
562        // every delimiter from forcing a scan across the whole remaining input.
563        let scan_end = self
564            .segment_start
565            .saturating_add(self.max_segment_bytes)
566            .saturating_add(1)
567            .min(self.input.len());
568
569        // Absolute offset of the next stop byte (segment terminator, plus the
570        // repetition separator when active) at or after the current search
571        // origin.  `memchr3` below rescans only the bytes it actually consumes,
572        // but a naive re-search per iteration would rescan the whole tail on
573        // every release sequence, making a value such as `?a?a?a…` quadratic.
574        // Caching the hit keeps this search amortised linear: each rescan starts
575        // past the previous hit, so the scanned regions are disjoint.
576        let mut stop_hit = self
577            .find_stop(&self.input[self.pos..scan_end])
578            .map(|i| self.pos + i);
579
580        loop {
581            if self.pos >= scan_end {
582                break;
583            }
584            let remaining = &self.input[self.pos..scan_end];
585            // Refresh the cached stop position once the cursor has moved past it
586            // (only happens when a release sequence escaped a stop byte).
587            if stop_hit.is_some_and(|t| t < self.pos) {
588                stop_hit = self.find_stop(remaining).map(|i| self.pos + i);
589            }
590            let hit_ect = memchr3(elem, comp, release, remaining);
591            let hit_stop = stop_hit.map(|t| t - self.pos);
592            let hit = match (hit_ect, hit_stop) {
593                (None, None) => {
594                    self.pos = scan_end;
595                    break;
596                }
597                (Some(a), None) => a,
598                (None, Some(b)) => b,
599                (Some(a), Some(b)) => a.min(b),
600            };
601            let b = remaining[hit];
602            if b == release {
603                // A release char must be followed by exactly one escaped byte.
604                // If it is the last byte in the buffer the sequence is malformed.
605                if self.pos + hit + 1 >= self.input.len() {
606                    return Err(EdifactError::InvalidReleaseSequence {
607                        offset: self.pos + hit,
608                    });
609                }
610                // Skip release char + the escaped byte.
611                self.pos += hit + 2;
612                continue;
613            }
614            // b is elem, comp, rep, or term — end of value.
615            self.pos += hit;
616            break;
617        }
618        // The size guard is checked *before* UTF-8 validation.  `scan_end` can
619        // cut a multi-byte sequence in half, and reporting that as `InvalidText`
620        // blamed the payload for what is really an oversized segment.
621        if self.pos - self.segment_start > self.max_segment_bytes {
622            return Err(EdifactError::SegmentTooLong {
623                offset: self.segment_start,
624                limit: self.max_segment_bytes,
625            });
626        }
627        let span = Span::new(start, self.pos);
628        let value = std::str::from_utf8(&self.input[start..self.pos])
629            .map_err(|_| EdifactError::InvalidText { offset: start })?;
630        Ok((value, span))
631    }
632
633    /// Fast scan for the segment tag (exactly 3 ASCII uppercase letters).
634    fn read_tag(&mut self) -> Result<Option<Token<'a>>, EdifactError> {
635        self.skip_inter_segment_whitespace();
636        if self.pos >= self.input.len() {
637            return Ok(None);
638        }
639        let start = self.pos;
640        // A segment tag is terminated by the element separator or segment terminator.
641        // Bound the scan to max_segment_bytes + 1 so adversarial input with no delimiters
642        // cannot force memchr to scan arbitrarily large buffers before we return an error.
643        let input_remaining = &self.input[self.pos..];
644        let scan_limit = self
645            .max_segment_bytes
646            .saturating_add(1)
647            .min(input_remaining.len());
648        let remaining = &input_remaining[..scan_limit];
649        // Take the *nearest* of the two terminating delimiters.  Searching for
650        // the element separator first and only falling back to the segment
651        // terminator would run straight past the terminator of an element-less
652        // segment (`UNZ'…`) and swallow the following segment's tag.
653        let end = memchr2(self.ssa.element_sep, self.ssa.segment_term, remaining)
654            .unwrap_or(remaining.len());
655
656        if end == 0 {
657            // First byte is already a delimiter — tag is zero-length, which is invalid.
658            let byte = self.input[self.pos];
659            self.pos += 1;
660            return Err(EdifactError::InvalidDelimiter {
661                byte,
662                offset: start,
663            });
664        }
665
666        // Enforce the per-segment byte-length guard in read_tag as well.
667        // Without this check, adversarial input with no delimiters could cause
668        // memchr to scan the entire remaining buffer (potentially hundreds of MB).
669        if end > self.max_segment_bytes {
670            // Advance past the offending bytes so the iterator can continue.
671            self.pos = start + end;
672            return Err(EdifactError::SegmentTooLong {
673                offset: start,
674                limit: self.max_segment_bytes,
675            });
676        }
677        let tag_bytes = &self.input[start..start + end];
678        // Always advance pos so errors cannot cause an infinite retry loop.
679        self.pos = start + end;
680        // Record segment start for the size-limit check in read_value.
681        self.segment_start = start;
682        let tag = std::str::from_utf8(tag_bytes)
683            .map_err(|_| EdifactError::InvalidSegmentTag(format!("{tag_bytes:?}")))?;
684        if !is_valid_segment_tag(tag) {
685            return Err(EdifactError::InvalidSegmentTag(tag.to_owned()));
686        }
687        self.state = TokState::InSegment;
688        Ok(Some(Token::SegmentTag {
689            value: tag,
690            span: Span::new(start, start + end),
691        }))
692    }
693}
694
695impl<'a> Iterator for Tokenizer<'a> {
696    type Item = Result<Token<'a>, EdifactError>;
697
698    fn next(&mut self) -> Option<Self::Item> {
699        loop {
700            if self.pos >= self.input.len() {
701                return None;
702            }
703
704            match self.state {
705                TokState::ExpectTag => {
706                    return match self.read_tag() {
707                        Ok(Some(tok)) => Some(Ok(tok)),
708                        Ok(None) => None,
709                        Err(e) => Some(Err(e)),
710                    };
711                }
712                TokState::InSegment => {
713                    let b = self.input[self.pos];
714                    let (elem, comp, term) = (
715                        self.ssa.element_sep,
716                        self.ssa.component_sep,
717                        self.ssa.segment_term,
718                    );
719
720                    if b == term {
721                        let start = self.pos;
722                        self.pos += 1;
723                        self.state = TokState::ExpectTag;
724                        return Some(Ok(Token::SegmentTerminator {
725                            span: Span::new(start, self.pos),
726                        }));
727                    } else if b == elem {
728                        self.pos += 1;
729                        let (value, span) = match self.read_value() {
730                            Ok(value) => value,
731                            Err(error) => return Some(Err(error)),
732                        };
733                        // Peek: is the *next* byte a component sep?
734                        // We emit DataElement for the leading sub-element regardless;
735                        // subsequent components within the same element are ComponentElement.
736                        return Some(Ok(Token::DataElement { value, span }));
737                    } else if b == comp {
738                        self.pos += 1;
739                        let (value, span) = match self.read_value() {
740                            Ok(value) => value,
741                            Err(error) => return Some(Err(error)),
742                        };
743                        return Some(Ok(Token::ComponentElement { value, span }));
744                    } else if self.ssa.is_repetition_active() && b == self.ssa.repetition_sep {
745                        self.pos += 1;
746                        let (value, span) = match self.read_value() {
747                            Ok(value) => value,
748                            Err(error) => return Some(Err(error)),
749                        };
750                        return Some(Ok(Token::RepeatElement { value, span }));
751                    } else if b == b'\r' || b == b'\n' {
752                        self.pos += 1;
753                        // inter-element whitespace inside a segment — skip
754                        continue;
755                    } else {
756                        // Unexpected byte inside a segment — skip it and report.
757                        let offset = self.pos;
758                        self.pos += 1; // always advance to prevent infinite retry loop
759                        self.state = TokState::ExpectTag;
760                        return Some(Err(EdifactError::InvalidDelimiter { byte: b, offset }));
761                    }
762                }
763            }
764        }
765    }
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771
772    fn tokens(input: &[u8]) -> Vec<Token<'_>> {
773        let ssa = ServiceStringAdvice::from_bytes_unchecked(input);
774        Tokenizer::new(input, ssa)
775            .collect::<Result<Vec<_>, _>>()
776            .expect("tokenize failed")
777    }
778
779    #[test]
780    fn syntax_version_4_activates_the_default_repetition_separator() {
781        // ISO 9735-1 §5.1: `*` is the default repetition separator, and version
782        // 4 is the version that has one.  Without a UNA, the only thing that can
783        // say so is UNB S001 DE 0002.
784        let v4 = ServiceStringAdvice::from_bytes(b"UNB+UNOC:4+S+R+260101:0900+IC1'").unwrap();
785        assert!(v4.is_repetition_active());
786        assert_eq!(v4.repetition_sep, b'*');
787
788        let v3 = ServiceStringAdvice::from_bytes(b"UNB+UNOC:3+S+R+260101:0900+IC1'").unwrap();
789        assert!(!v3.is_repetition_active());
790
791        // No UNB at all — a bare message — stays conservative.
792        let fragment = ServiceStringAdvice::from_bytes(b"BGM+220'").unwrap();
793        assert!(!fragment.is_repetition_active());
794    }
795
796    #[test]
797    fn a_una_overrides_the_syntax_version_default() {
798        // The UNA states all six characters explicitly, so a version 4
799        // interchange that declares the "not used" space really means it.
800        let input = b"UNA:+.? 'UNB+UNOC:4+S+R+260101:0900+IC1'";
801        let ssa = ServiceStringAdvice::from_bytes(input).unwrap();
802        assert!(!ssa.is_repetition_active());
803    }
804
805    #[test]
806    fn version_4_repetitions_parse_without_a_una() {
807        let input = b"UNB+UNOC:4+S+R+260101:0900+IC1'RFF+ON:1*ON:2'UNZ+0+IC1'";
808        let segments: Vec<_> = crate::from_bytes(input)
809            .collect::<Result<Vec<_>, _>>()
810            .unwrap();
811        let rff = segments[1].get_element(0).unwrap();
812        assert_eq!(rff.repeat_count(), 2);
813        assert_eq!(rff.repetition(1).unwrap()[1].0, "2");
814    }
815
816    #[test]
817    fn a_version_3_asterisk_stays_data() {
818        // `*` is an ordinary level A character in syntax version 3; splitting on
819        // it would corrupt the value.
820        let input = b"UNB+UNOC:3+S+R+260101:0900+IC1'FTX+AAA+2*3'UNZ+0+IC1'";
821        let segments: Vec<_> = crate::from_bytes(input)
822            .collect::<Result<Vec<_>, _>>()
823            .unwrap();
824        assert_eq!(segments[1].element_str(1), Some("2*3"));
825    }
826
827    #[test]
828    fn the_ignored_decimal_mark_slot_never_invalidates_a_una() {
829        // Annex B: the character in position 030 "shall be ignored by the
830        // recipient", and it is the one position where a space is allowed.
831        for una in [&b"UNA:+ ? '"[..], &b"UNA:+,? '"[..], &b"UNA:+:? '"[..]] {
832            assert!(
833                ServiceStringAdvice::from_bytes(una).is_ok(),
834                "{:?} must parse",
835                std::str::from_utf8(una).unwrap()
836            );
837        }
838        // An *active* character duplicated is still fatal.
839        assert!(ServiceStringAdvice::from_bytes(b"UNA:+.: '").is_err());
840    }
841
842    #[test]
843    fn minimal_unb_unz() {
844        let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'UNZ+0+1'";
845        let toks = tokens(input);
846        assert!(matches!(toks[0], Token::SegmentTag { value: "UNB", .. }));
847        // should end with UNZ terminator
848        assert!(matches!(toks.last(), Some(Token::SegmentTerminator { .. })));
849    }
850
851    #[test]
852    fn release_character_not_a_delimiter() {
853        // `?+` inside a value must NOT produce a DataElement split
854        let input = b"BGM+220+test?+value'";
855        let toks = tokens(input);
856        // Elements after BGM tag: "220", "test?+value"
857        let vals: Vec<_> = toks
858            .iter()
859            .filter_map(|t| {
860                if let Token::DataElement { value, .. } = t {
861                    Some(*value)
862                } else {
863                    None
864                }
865            })
866            .collect();
867        assert_eq!(vals, vec!["220", "test?+value"]);
868    }
869
870    #[test]
871    fn custom_una_delimiters() {
872        // UNA with `;` as element sep
873        let input = b"UNA:;.? 'BGM;220;hello'";
874        let toks = tokens(input);
875        assert!(matches!(toks[0], Token::SegmentTag { value: "BGM", .. }));
876        let vals: Vec<_> = toks
877            .iter()
878            .filter_map(|t| {
879                if let Token::DataElement { value, .. } = t {
880                    Some(*value)
881                } else {
882                    None
883                }
884            })
885            .collect();
886        assert!(vals.contains(&"220"));
887    }
888
889    #[test]
890    fn tokens_expose_spans() {
891        let input = b"BGM+220+ABC'";
892        let toks = tokens(input);
893        assert!(matches!(
894            toks[0],
895            Token::SegmentTag {
896                value: "BGM",
897                span: Span { start: 0, end: 3 }
898            }
899        ));
900        assert!(matches!(
901            toks[1],
902            Token::DataElement {
903                value: "220",
904                span: Span { start: 4, end: 7 }
905            }
906        ));
907    }
908
909    #[test]
910    fn truncated_input_does_not_panic() {
911        let input = b"UNB+UNOA:1"; // no terminator
912        let _: Vec<_> = Tokenizer::new(input, ServiceStringAdvice::default()).collect();
913        // must not panic regardless of result
914    }
915
916    #[test]
917    fn invalid_segment_tags_are_rejected() {
918        for input in [
919            &b"bgm+220+'"[..],
920            &b"ABCDE+220+'"[..],
921            &b"BGM1+220+'"[..],
922            &b"BGM +220+'"[..],
923            &b" BG+220+'"[..],
924        ] {
925            let result = Tokenizer::new(input, ServiceStringAdvice::default())
926                .collect::<Result<Vec<_>, _>>();
927            assert!(result.is_err(), "expected tag rejection for {input:?}");
928        }
929    }
930
931    #[test]
932    fn element_less_segment_does_not_swallow_the_next_tag() {
933        // `read_tag` must stop at the *nearest* of element-separator and
934        // segment-terminator.  Scanning for `+` first would run past the `'`
935        // and produce the bogus tag "UNZ'UNB".
936        let segs: Vec<_> = crate::from_bytes(b"UNZ'UNB+A'")
937            .collect::<Result<Vec<_>, _>>()
938            .expect("element-less segment must parse");
939        assert_eq!(
940            segs.iter().map(|s| s.tag()).collect::<Vec<_>>(),
941            vec!["UNZ", "UNB"]
942        );
943        assert!(segs[0].elements.is_empty());
944    }
945
946    #[test]
947    fn release_heavy_value_is_bounded_by_the_segment_guard() {
948        // A value consisting solely of release sequences and no delimiter must
949        // trip the per-segment guard rather than scanning the whole input once
950        // per release sequence (which was quadratic).
951        let mut input = b"BGM+".to_vec();
952        input.extend(std::iter::repeat_n(b"?a".as_slice(), 200_000).flatten());
953        let err = crate::from_bytes(&input)
954            .collect::<Result<Vec<_>, _>>()
955            .expect_err("oversized segment must be rejected");
956        assert!(
957            matches!(err, EdifactError::SegmentTooLong { .. }),
958            "expected SegmentTooLong, got {err:?}"
959        );
960    }
961
962    #[test]
963    fn an_oversized_segment_is_reported_as_such_even_with_multi_byte_text() {
964        // The scan window can cut a multi-byte sequence in half.  Validating
965        // UTF-8 before the size guard blamed the payload (`InvalidText`) for
966        // what is really an oversized segment, sending the reader hunting for an
967        // encoding problem that does not exist.
968        let mut input = b"BGM+".to_vec();
969        input.extend(std::iter::repeat_n("ä".as_bytes(), 200_000).flatten());
970        let err = crate::from_bytes(&input)
971            .collect::<Result<Vec<_>, _>>()
972            .expect_err("oversized segment must be rejected");
973        assert!(
974            matches!(err, EdifactError::SegmentTooLong { .. }),
975            "expected SegmentTooLong, got {err:?}"
976        );
977    }
978
979    #[test]
980    fn multi_byte_text_within_the_limit_still_parses() {
981        let segs: Vec<_> = crate::from_bytes("FTX+Grüße aus Köln'".as_bytes())
982            .collect::<Result<Vec<_>, _>>()
983            .expect("valid UTF-8 must parse");
984        assert_eq!(segs[0].element_str(0), Some("Grüße aus Köln"));
985    }
986
987    #[test]
988    fn escaped_terminator_inside_a_value_is_not_a_segment_break() {
989        // Exercises the cached-terminator refresh path: the first `'` is escaped,
990        // so the scan must resume past it and find the real terminator.
991        let segs: Vec<_> = crate::from_bytes(b"FTX+a?'b+c'")
992            .collect::<Result<Vec<_>, _>>()
993            .expect("escaped terminator must parse");
994        assert_eq!(segs.len(), 1);
995        assert_eq!(segs[0].element_str(0), Some("a'b"));
996        assert_eq!(segs[0].element_str(1), Some("c"));
997    }
998
999    #[test]
1000    fn chunked_reader_parses_via_parser() {
1001        // The reader tokenizer path was removed; verify the equivalent via the parser.
1002        let input = b"UNA:+.? 'BGM+220+test?+value'UNT+2+1'";
1003        let segments: Vec<_> =
1004            crate::parser::from_bufread(std::io::BufReader::new(std::io::Cursor::new(input)))
1005                .collect::<Result<_, _>>()
1006                .expect("parser should succeed");
1007        assert!(segments.iter().any(|s| s.tag == "BGM"));
1008        // The release sequence '?+' inside 'test?+value' should survive in the element.
1009        let bgm = segments.iter().find(|s| s.tag == "BGM").unwrap();
1010        let raw_val = bgm
1011            .elements
1012            .get(1)
1013            .and_then(|e| e.components.first())
1014            .map(|(s, _)| s.as_ref());
1015        assert_eq!(raw_val, Some("test+value"));
1016    }
1017}