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