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 (UNA segment).
10///
11/// Defaults: `+` (element), `:` (component), `?` (release), `.` (decimal mark),
12/// `*` (repetition separator), `'` (segment terminator).
13///
14/// All six service characters are first-class fields.  [`is_valid`][Self::is_valid]
15/// checks all six for mutual distinctness and for being printable, non-alphanumeric
16/// ASCII, so a collision between the repetition separator and any other delimiter —
17/// or a delimiter that would clash with segment-tag characters — is caught at UNA
18/// parse time.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct ServiceStringAdvice {
21    /// Data element separator (default `+`)
22    pub element_sep: u8,
23    /// Component data element separator (default `:`)
24    pub component_sep: u8,
25    /// Release character (default `?`)
26    pub release_char: u8,
27    /// Decimal notation mark (default `.`; UNA byte 5, ISO 9735-1 §7.1).
28    /// Not used by the tokenizer for splitting, but preserved for downstream use.
29    pub decimal_mark: u8,
30    /// Repetition separator (UNA byte 7, ISO 9735-4 §3.1).
31    ///
32    /// Defaults to space (`0x20`), the conventional "not used" sentinel, when no
33    /// UNA is present.  Syntax version 4 interchanges — and some industry profiles —
34    /// declare a real repetition separator here.
35    ///
36    /// When the separator is **active** (any value other than space) the
37    /// tokenizer splits on it: a data element carrying `ON:1*ON:2` becomes one
38    /// element with two repetitions rather than one repetition whose second
39    /// component is the literal text `1*ON`.  Use
40    /// [`is_repetition_active`][Self::is_repetition_active] to test for this.
41    pub repetition_sep: u8,
42    /// Segment terminator (default `'`)
43    pub segment_term: u8,
44}
45
46impl Default for ServiceStringAdvice {
47    fn default() -> Self {
48        Self {
49            element_sep: b'+',
50            component_sep: b':',
51            release_char: b'?',
52            decimal_mark: b'.',
53            // Space (0x20) is the conventional "not used" sentinel found at
54            // position 7 in the vast majority of real-world EDIFACT interchanges
55            // that do not employ ISO 9735-4 repetition elements.  `is_valid()`
56            // accepts space here without a printability or uniqueness check.
57            repetition_sep: b' ',
58            segment_term: b'\'',
59        }
60    }
61}
62
63impl ServiceStringAdvice {
64    /// Parse a UNA header and validate that all six service characters
65    /// (`element_sep`, `component_sep`, `decimal_mark`, `release_char`,
66    /// `repetition_sep`, and `segment_term`) are mutually distinct and are
67    /// printable, non-alphanumeric ASCII.  See [`is_valid`][Self::is_valid] for
68    /// the exact rule.
69    ///
70    /// Returns [`EdifactError::InvalidUna`] if the invariant is violated.
71    /// Falls back to [`ServiceStringAdvice::default`] when no UNA is present.
72    ///
73    /// This is the **safe, default constructor** — always use this for input from
74    /// an external source.  For trusted or internal use where delimiter uniqueness
75    /// is already guaranteed, use [`from_bytes_unchecked`](Self::from_bytes_unchecked).
76    pub fn from_bytes(input: &[u8]) -> Result<Self, crate::error::EdifactError> {
77        let ssa = Self::from_bytes_unchecked(input);
78        if !ssa.is_valid() {
79            return Err(crate::error::EdifactError::InvalidUna);
80        }
81        Ok(ssa)
82    }
83
84    /// Parse a UNA header from the beginning of an EDIFACT interchange **without**
85    /// validating delimiter uniqueness or printability.
86    ///
87    /// If no UNA is present, returns [`ServiceStringAdvice::default`].
88    ///
89    /// The `repetition_sep` field is populated from UNA byte 7 (ISO 9735-4 §3.1)
90    /// or defaults to space (`0x20`, the "not used" sentinel) when no UNA is present.
91    ///
92    /// # When to use
93    ///
94    /// Use this only for trusted internal data (e.g. round-tripping data where
95    /// the UNA invariant is already guaranteed) or in fuzz/property tests that
96    /// intentionally explore degenerate delimiter combinations.
97    ///
98    /// For any external or user-provided input, prefer [`from_bytes`](Self::from_bytes)
99    /// which validates delimiter uniqueness and rejects invalid bytes.
100    pub fn from_bytes_unchecked(input: &[u8]) -> Self {
101        // UNA is 9 bytes: "UNA" + 6 service chars
102        if input.len() >= 9 && &input[..3] == b"UNA" {
103            Self {
104                component_sep: input[3],
105                element_sep: input[4],
106                decimal_mark: input[5],
107                release_char: input[6],
108                repetition_sep: input[7],
109                segment_term: input[8],
110            }
111        } else {
112            Self::default()
113        }
114    }
115
116    /// Return `true` if all active service characters are mutually distinct
117    /// and printable ASCII.
118    ///
119    /// The five *mandatory* characters (`element_sep`, `component_sep`,
120    /// `decimal_mark`, `release_char`, `segment_term`) must all be printable,
121    /// **non-alphanumeric** ASCII (`0x21–0x7E`, excluding `0-9A-Za-z`) and
122    /// mutually distinct (10 pairwise checks).  Alphanumerics are rejected
123    /// because segment tags are written verbatim and cannot be escaped, so a
124    /// letter delimiter would make tags containing it unrepresentable.
125    ///
126    /// The `repetition_sep` field is also validated when it is **not a space**
127    /// (`0x20`).  A space at position 7 of the UNA is the conventional
128    /// "absent" sentinel used by interchanges that do not employ repetition
129    /// elements (ISO 9735-1 / ISO 9735-4 §3.1), and it is accepted without
130    /// a printability or uniqueness check.  Any other value must be printable
131    /// non-alphanumeric ASCII and distinct from all five mandatory characters.
132    ///
133    /// High bytes (`>= 0x80`) are rejected because they would incorrectly bisect
134    /// multi-byte UTF-8 sequences, and DEL (`0x7F`) is a non-printable control
135    /// character.
136    pub fn is_valid(&self) -> bool {
137        let [e, c, d, r, t] = [
138            self.element_sep,
139            self.component_sep,
140            self.decimal_mark,
141            self.release_char,
142            self.segment_term,
143        ];
144        // All five mandatory chars must be printable, non-alphanumeric ASCII and
145        // mutually distinct (10 pairwise checks).
146        //
147        // Alphanumerics are excluded because segment tags are always three ASCII
148        // uppercase letters and are written verbatim (a tag cannot be escaped).
149        // A delimiter such as `N` would therefore make `NAD` unrepresentable —
150        // the writer would emit a premature terminator and the result would not
151        // reparse.  Real-world UNA strings use punctuation exclusively, so this
152        // rejects only degenerate configurations.
153        let printable_ascii = |b: u8| (0x21..=0x7E).contains(&b) && !b.is_ascii_alphanumeric();
154        let basic_valid = printable_ascii(e)
155            && printable_ascii(c)
156            && printable_ascii(d)
157            && printable_ascii(r)
158            && printable_ascii(t)
159            && e != c
160            && e != d
161            && e != r
162            && e != t
163            && c != d
164            && c != r
165            && c != t
166            && d != r
167            && d != t
168            && r != t;
169        if !basic_valid {
170            return false;
171        }
172        // repetition_sep: space (0x20) means "not used" — accepted as-is.
173        // Any other value must be printable ASCII and distinct from all five
174        // mandatory service characters.
175        let rep = self.repetition_sep;
176        if rep == b' ' {
177            true
178        } else {
179            printable_ascii(rep) && rep != e && rep != c && rep != d && rep != r && rep != t
180        }
181    }
182
183    /// Returns `true` when this interchange declares a usable repetition
184    /// separator (ISO 9735-4 §3.1).
185    ///
186    /// A space at UNA position 7 is the conventional "not used" sentinel, so it
187    /// reports `false` and the tokenizer never splits on it.
188    ///
189    /// # Example
190    ///
191    /// ```
192    /// use edifact_rs::ServiceStringAdvice;
193    ///
194    /// assert!(!ServiceStringAdvice::default().is_repetition_active());
195    /// assert!(ServiceStringAdvice::from_bytes(b"UNA:+.?*'")?.is_repetition_active());
196    /// # Ok::<(), edifact_rs::EdifactError>(())
197    /// ```
198    #[inline]
199    #[must_use]
200    pub const fn is_repetition_active(&self) -> bool {
201        self.repetition_sep != b' '
202    }
203}
204
205/// Token produced by [`Tokenizer`].
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub enum Token<'a> {
208    /// 3-character segment tag (e.g. `"BGM"`)
209    SegmentTag {
210        /// Raw tag value.
211        value: &'a str,
212        /// Source span of the tag.
213        span: Span,
214    },
215    /// Data element value (between element separators)
216    DataElement {
217        /// Raw element value.
218        value: &'a str,
219        /// Source span of the element value.
220        span: Span,
221    },
222    /// Component within a composite data element (between component separators)
223    ComponentElement {
224        /// Raw component value.
225        value: &'a str,
226        /// Source span of the component value.
227        span: Span,
228    },
229    /// First component of a further repetition of the current data element
230    /// (ISO 9735-4 §3.1).
231    ///
232    /// Only produced when the active [`ServiceStringAdvice`] declares a
233    /// repetition separator — see
234    /// [`is_repetition_active`][ServiceStringAdvice::is_repetition_active].
235    RepeatElement {
236        /// Raw value of the repetition's first component.
237        value: &'a str,
238        /// Source span of the value.
239        span: Span,
240    },
241    /// Segment terminator — signals the end of a segment
242    SegmentTerminator {
243        /// Source span of the segment terminator byte.
244        span: Span,
245    },
246}
247
248#[derive(Debug)]
249pub(crate) struct RawSegment {
250    pub(crate) bytes: Vec<u8>,
251    pub(crate) start_offset: usize,
252}
253
254/// Zero-copy tokenizer over a byte slice.
255///
256/// Yields `Token` values, each borrowing from the original input.
257///
258/// # Segment size guard
259///
260/// The default constructor [`Tokenizer::new`] enforces a **64 KiB** per-segment
261/// limit, which is sufficient for all well-formed EDIFACT interchanges and guards
262/// against adversarially crafted inputs that omit segment terminators.
263/// Use [`Tokenizer::with_limit`] to raise or lower this threshold, or
264/// [`Tokenizer::unlimited`] to remove it entirely (trusted / pre-validated input only).
265pub struct Tokenizer<'a> {
266    input: &'a [u8],
267    pos: usize,
268    ssa: ServiceStringAdvice,
269    state: TokState,
270    /// Maximum allowed segment byte length (tag + elements, **excluding** the
271    /// segment terminator byte itself).  Checked in `read_value` and `read_tag`.
272    /// `usize::MAX` = unlimited.
273    max_segment_bytes: usize,
274    /// Byte position where the current segment started (set in `read_tag`).
275    segment_start: usize,
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279enum TokState {
280    /// Expecting a segment tag next
281    ExpectTag,
282    /// Inside a segment; next byte could be element or component sep, release, or terminator
283    InSegment,
284}
285
286impl<'a> Tokenizer<'a> {
287    /// Return the byte offset of the first non-UNA byte in `input`.
288    ///
289    /// If the input starts with the `UNA` service string advice (first 3
290    /// bytes are `b"UNA"`), the UNA header is exactly 9 bytes long and the
291    /// first segment tag starts at offset 9.  Otherwise parsing starts at 0.
292    ///
293    /// Only correct for a slice that starts at the head of an interchange.
294    /// A slice holding a single already-delimited segment must use
295    /// [`Tokenizer::for_segment`], because `UNA` is also a syntactically valid
296    /// segment tag and skipping nine bytes of it corrupts the parse.
297    #[inline]
298    fn una_start_pos(input: &[u8]) -> usize {
299        if input.len() >= 9 && &input[..3] == b"UNA" {
300            9
301        } else {
302            0
303        }
304    }
305
306    /// Construct a tokenizer over a slice that holds **one already-delimited
307    /// segment**, with no interchange header to skip.
308    ///
309    /// The whole-interchange constructors treat a leading `b"UNA"` as the
310    /// service string advice and jump nine bytes past it.  The reader paths
311    /// re-tokenize each segment from its own slice, where that heuristic is
312    /// wrong: `UNA` is three ASCII uppercase letters and therefore a legal
313    /// segment tag, so `UNA+XXXXXX'` parsed cleanly from a byte slice but was
314    /// rejected as `InvalidSegmentTag` when the identical bytes arrived through
315    /// a reader.
316    #[must_use]
317    pub fn for_segment(
318        input: &'a [u8],
319        ssa: ServiceStringAdvice,
320        max_segment_bytes: usize,
321    ) -> Self {
322        Self {
323            input,
324            pos: 0,
325            ssa,
326            state: TokState::ExpectTag,
327            max_segment_bytes,
328            segment_start: 0,
329        }
330    }
331
332    /// Construct a tokenizer with the default 64 KiB segment-size limit.
333    ///
334    /// If a single segment's byte length exceeds 65 536 bytes, the iterator
335    /// returns [`EdifactError::SegmentTooLong`].  This guards against
336    /// pathological or adversarially crafted inputs that omit segment
337    /// terminators and would otherwise cause unbounded scanning.
338    ///
339    /// Call [`Tokenizer::unlimited`] if you deliberately need to process
340    /// segments larger than 64 KiB, or [`Tokenizer::with_limit`] to supply a
341    /// custom bound.
342    pub fn new(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
343        Self::with_limit(input, ssa, 65_536)
344    }
345
346    /// Construct a tokenizer with **no** segment-size limit.
347    ///
348    /// # Security warning
349    ///
350    /// This constructor imposes **no upper bound** on how many bytes a single
351    /// segment may consume.  For untrusted or adversarially crafted input a
352    /// missing segment terminator can cause the tokenizer to scan the entire
353    /// input before returning an error.  Prefer [`Tokenizer::new`] (64 KiB
354    /// limit) or [`Tokenizer::with_limit`] for untrusted sources.
355    #[must_use]
356    pub fn unlimited(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
357        Self {
358            input,
359            pos: Self::una_start_pos(input),
360            ssa,
361            state: TokState::ExpectTag,
362            max_segment_bytes: usize::MAX,
363            segment_start: 0,
364        }
365    }
366
367    /// Construct a tokenizer with a segment-size limit.
368    ///
369    /// If a single segment's byte length (from the start of the tag to the end
370    /// of the last value, not including the terminator itself) exceeds `limit`,
371    /// the iterator returns [`EdifactError::SegmentTooLong`].
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// use edifact_rs::{ServiceStringAdvice, Tokenizer};
377    ///
378    /// let input = b"BGM+220+PO-4711+9'";
379    /// let ssa = ServiceStringAdvice::default();
380    /// let tokens: Vec<_> = Tokenizer::with_limit(input, ssa, 64)
381    ///     .collect::<Result<_, _>>()
382    ///     .unwrap();
383    /// assert!(!tokens.is_empty());
384    /// ```
385    pub fn with_limit(input: &'a [u8], ssa: ServiceStringAdvice, max_segment_bytes: usize) -> Self {
386        Self {
387            input,
388            pos: Self::una_start_pos(input),
389            ssa,
390            state: TokState::ExpectTag,
391            max_segment_bytes,
392            segment_start: 0,
393        }
394    }
395
396    /// Current byte position in the input.
397    #[inline]
398    pub fn position(&self) -> usize {
399        self.pos
400    }
401
402    /// Return the service string advice active for this tokenizer.
403    #[inline]
404    pub fn service_string_advice(&self) -> ServiceStringAdvice {
405        self.ssa
406    }
407
408    /// Consume leading whitespace / CR / LF between segments (not inside data values).
409    fn skip_inter_segment_whitespace(&mut self) {
410        while self.pos < self.input.len() {
411            match self.input[self.pos] {
412                b' ' | b'\t' | b'\r' | b'\n' => self.pos += 1,
413                _ => break,
414            }
415        }
416    }
417
418    /// Read a field value starting at `self.pos`, advancing past the value.
419    ///
420    /// Recognises the release character (`?` by default) and returns the raw
421    /// slice including release sequences. The parser layer resolves them.
422    ///
423    /// Uses `memchr3` to bulk-scan over non-special bytes between hits, only
424    /// falling back to a per-byte step when a release character is encountered.
425    /// Offset of the next segment terminator — or repetition separator, when the
426    /// interchange declares one — at or after `from`, searching within `window`.
427    ///
428    /// `memchr` tops out at three needles and `read_value` already spends those
429    /// on the element separator, component separator, and release character, so
430    /// the remaining one or two needles are searched separately and cached.
431    #[inline]
432    fn find_stop(&self, window: &[u8]) -> Option<usize> {
433        if self.ssa.is_repetition_active() {
434            memchr2(self.ssa.segment_term, self.ssa.repetition_sep, window)
435        } else {
436            memchr(self.ssa.segment_term, window)
437        }
438    }
439
440    fn read_value(&mut self) -> Result<(&'a str, Span), EdifactError> {
441        let start = self.pos;
442        let (elem, comp, release) = (
443            self.ssa.element_sep,
444            self.ssa.component_sep,
445            self.ssa.release_char,
446        );
447        // Absolute cap on how far this value may extend before the per-segment
448        // byte guard trips.  Bounding the scan window here (rather than only
449        // checking the length after the loop) keeps adversarial input that omits
450        // every delimiter from forcing a scan across the whole remaining input.
451        let scan_end = self
452            .segment_start
453            .saturating_add(self.max_segment_bytes)
454            .saturating_add(1)
455            .min(self.input.len());
456
457        // Absolute offset of the next stop byte (segment terminator, plus the
458        // repetition separator when active) at or after the current search
459        // origin.  `memchr3` below rescans only the bytes it actually consumes,
460        // but a naive re-search per iteration would rescan the whole tail on
461        // every release sequence, making a value such as `?a?a?a…` quadratic.
462        // Caching the hit keeps this search amortised linear: each rescan starts
463        // past the previous hit, so the scanned regions are disjoint.
464        let mut stop_hit = self
465            .find_stop(&self.input[self.pos..scan_end])
466            .map(|i| self.pos + i);
467
468        loop {
469            if self.pos >= scan_end {
470                break;
471            }
472            let remaining = &self.input[self.pos..scan_end];
473            // Refresh the cached stop position once the cursor has moved past it
474            // (only happens when a release sequence escaped a stop byte).
475            if stop_hit.is_some_and(|t| t < self.pos) {
476                stop_hit = self.find_stop(remaining).map(|i| self.pos + i);
477            }
478            let hit_ect = memchr3(elem, comp, release, remaining);
479            let hit_stop = stop_hit.map(|t| t - self.pos);
480            let hit = match (hit_ect, hit_stop) {
481                (None, None) => {
482                    self.pos = scan_end;
483                    break;
484                }
485                (Some(a), None) => a,
486                (None, Some(b)) => b,
487                (Some(a), Some(b)) => a.min(b),
488            };
489            let b = remaining[hit];
490            if b == release {
491                // A release char must be followed by exactly one escaped byte.
492                // If it is the last byte in the buffer the sequence is malformed.
493                if self.pos + hit + 1 >= self.input.len() {
494                    return Err(EdifactError::InvalidReleaseSequence {
495                        offset: self.pos + hit,
496                    });
497                }
498                // Skip release char + the escaped byte.
499                self.pos += hit + 2;
500                continue;
501            }
502            // b is elem, comp, rep, or term — end of value.
503            self.pos += hit;
504            break;
505        }
506        // The size guard is checked *before* UTF-8 validation.  `scan_end` can
507        // cut a multi-byte sequence in half, and reporting that as `InvalidText`
508        // blamed the payload for what is really an oversized segment.
509        if self.pos - self.segment_start > self.max_segment_bytes {
510            return Err(EdifactError::SegmentTooLong {
511                offset: self.segment_start,
512                limit: self.max_segment_bytes,
513            });
514        }
515        let span = Span::new(start, self.pos);
516        let value = std::str::from_utf8(&self.input[start..self.pos])
517            .map_err(|_| EdifactError::InvalidText { offset: start })?;
518        Ok((value, span))
519    }
520
521    /// Fast scan for the segment tag (exactly 3 ASCII uppercase letters).
522    fn read_tag(&mut self) -> Result<Option<Token<'a>>, EdifactError> {
523        self.skip_inter_segment_whitespace();
524        if self.pos >= self.input.len() {
525            return Ok(None);
526        }
527        let start = self.pos;
528        // A segment tag is terminated by the element separator or segment terminator.
529        // Bound the scan to max_segment_bytes + 1 so adversarial input with no delimiters
530        // cannot force memchr to scan arbitrarily large buffers before we return an error.
531        let input_remaining = &self.input[self.pos..];
532        let scan_limit = self
533            .max_segment_bytes
534            .saturating_add(1)
535            .min(input_remaining.len());
536        let remaining = &input_remaining[..scan_limit];
537        // Take the *nearest* of the two terminating delimiters.  Searching for
538        // the element separator first and only falling back to the segment
539        // terminator would run straight past the terminator of an element-less
540        // segment (`UNZ'…`) and swallow the following segment's tag.
541        let end = memchr2(self.ssa.element_sep, self.ssa.segment_term, remaining)
542            .unwrap_or(remaining.len());
543
544        if end == 0 {
545            // First byte is already a delimiter — tag is zero-length, which is invalid.
546            let byte = self.input[self.pos];
547            self.pos += 1;
548            return Err(EdifactError::InvalidDelimiter {
549                byte,
550                offset: start,
551            });
552        }
553
554        // Enforce the per-segment byte-length guard in read_tag as well.
555        // Without this check, adversarial input with no delimiters could cause
556        // memchr to scan the entire remaining buffer (potentially hundreds of MB).
557        if end > self.max_segment_bytes {
558            // Advance past the offending bytes so the iterator can continue.
559            self.pos = start + end;
560            return Err(EdifactError::SegmentTooLong {
561                offset: start,
562                limit: self.max_segment_bytes,
563            });
564        }
565        let tag_bytes = &self.input[start..start + end];
566        // Always advance pos so errors cannot cause an infinite retry loop.
567        self.pos = start + end;
568        // Record segment start for the size-limit check in read_value.
569        self.segment_start = start;
570        let tag = std::str::from_utf8(tag_bytes)
571            .map_err(|_| EdifactError::InvalidSegmentTag(format!("{tag_bytes:?}")))?;
572        if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
573            return Err(EdifactError::InvalidSegmentTag(tag.to_owned()));
574        }
575        self.state = TokState::InSegment;
576        Ok(Some(Token::SegmentTag {
577            value: tag,
578            span: Span::new(start, start + end),
579        }))
580    }
581}
582
583impl<'a> Iterator for Tokenizer<'a> {
584    type Item = Result<Token<'a>, EdifactError>;
585
586    fn next(&mut self) -> Option<Self::Item> {
587        loop {
588            if self.pos >= self.input.len() {
589                return None;
590            }
591
592            match self.state {
593                TokState::ExpectTag => {
594                    return match self.read_tag() {
595                        Ok(Some(tok)) => Some(Ok(tok)),
596                        Ok(None) => None,
597                        Err(e) => Some(Err(e)),
598                    };
599                }
600                TokState::InSegment => {
601                    let b = self.input[self.pos];
602                    let (elem, comp, term) = (
603                        self.ssa.element_sep,
604                        self.ssa.component_sep,
605                        self.ssa.segment_term,
606                    );
607
608                    if b == term {
609                        let start = self.pos;
610                        self.pos += 1;
611                        self.state = TokState::ExpectTag;
612                        return Some(Ok(Token::SegmentTerminator {
613                            span: Span::new(start, self.pos),
614                        }));
615                    } else if b == elem {
616                        self.pos += 1;
617                        let (value, span) = match self.read_value() {
618                            Ok(value) => value,
619                            Err(error) => return Some(Err(error)),
620                        };
621                        // Peek: is the *next* byte a component sep?
622                        // We emit DataElement for the leading sub-element regardless;
623                        // subsequent components within the same element are ComponentElement.
624                        return Some(Ok(Token::DataElement { value, span }));
625                    } else if b == comp {
626                        self.pos += 1;
627                        let (value, span) = match self.read_value() {
628                            Ok(value) => value,
629                            Err(error) => return Some(Err(error)),
630                        };
631                        return Some(Ok(Token::ComponentElement { value, span }));
632                    } else if self.ssa.is_repetition_active() && b == self.ssa.repetition_sep {
633                        self.pos += 1;
634                        let (value, span) = match self.read_value() {
635                            Ok(value) => value,
636                            Err(error) => return Some(Err(error)),
637                        };
638                        return Some(Ok(Token::RepeatElement { value, span }));
639                    } else if b == b'\r' || b == b'\n' {
640                        self.pos += 1;
641                        // inter-element whitespace inside a segment — skip
642                        continue;
643                    } else {
644                        // Unexpected byte inside a segment — skip it and report.
645                        let offset = self.pos;
646                        self.pos += 1; // always advance to prevent infinite retry loop
647                        self.state = TokState::ExpectTag;
648                        return Some(Err(EdifactError::InvalidDelimiter { byte: b, offset }));
649                    }
650                }
651            }
652        }
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    fn tokens(input: &[u8]) -> Vec<Token<'_>> {
661        let ssa = ServiceStringAdvice::from_bytes_unchecked(input);
662        Tokenizer::new(input, ssa)
663            .collect::<Result<Vec<_>, _>>()
664            .expect("tokenize failed")
665    }
666
667    #[test]
668    fn minimal_unb_unz() {
669        let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'UNZ+0+1'";
670        let toks = tokens(input);
671        assert!(matches!(toks[0], Token::SegmentTag { value: "UNB", .. }));
672        // should end with UNZ terminator
673        assert!(matches!(toks.last(), Some(Token::SegmentTerminator { .. })));
674    }
675
676    #[test]
677    fn release_character_not_a_delimiter() {
678        // `?+` inside a value must NOT produce a DataElement split
679        let input = b"BGM+220+test?+value'";
680        let toks = tokens(input);
681        // Elements after BGM tag: "220", "test?+value"
682        let vals: Vec<_> = toks
683            .iter()
684            .filter_map(|t| {
685                if let Token::DataElement { value, .. } = t {
686                    Some(*value)
687                } else {
688                    None
689                }
690            })
691            .collect();
692        assert_eq!(vals, vec!["220", "test?+value"]);
693    }
694
695    #[test]
696    fn custom_una_delimiters() {
697        // UNA with `;` as element sep
698        let input = b"UNA:;.? 'BGM;220;hello'";
699        let toks = tokens(input);
700        assert!(matches!(toks[0], Token::SegmentTag { value: "BGM", .. }));
701        let vals: Vec<_> = toks
702            .iter()
703            .filter_map(|t| {
704                if let Token::DataElement { value, .. } = t {
705                    Some(*value)
706                } else {
707                    None
708                }
709            })
710            .collect();
711        assert!(vals.contains(&"220"));
712    }
713
714    #[test]
715    fn tokens_expose_spans() {
716        let input = b"BGM+220+ABC'";
717        let toks = tokens(input);
718        assert!(matches!(
719            toks[0],
720            Token::SegmentTag {
721                value: "BGM",
722                span: Span { start: 0, end: 3 }
723            }
724        ));
725        assert!(matches!(
726            toks[1],
727            Token::DataElement {
728                value: "220",
729                span: Span { start: 4, end: 7 }
730            }
731        ));
732    }
733
734    #[test]
735    fn truncated_input_does_not_panic() {
736        let input = b"UNB+UNOA:1"; // no terminator
737        let _: Vec<_> = Tokenizer::new(input, ServiceStringAdvice::default()).collect();
738        // must not panic regardless of result
739    }
740
741    #[test]
742    fn invalid_segment_tags_are_rejected() {
743        for input in [
744            &b"bgm+220+'"[..],
745            &b"ABCDE+220+'"[..],
746            &b"BGM1+220+'"[..],
747            &b"BGM +220+'"[..],
748            &b" BG+220+'"[..],
749        ] {
750            let result = Tokenizer::new(input, ServiceStringAdvice::default())
751                .collect::<Result<Vec<_>, _>>();
752            assert!(result.is_err(), "expected tag rejection for {input:?}");
753        }
754    }
755
756    #[test]
757    fn element_less_segment_does_not_swallow_the_next_tag() {
758        // `read_tag` must stop at the *nearest* of element-separator and
759        // segment-terminator.  Scanning for `+` first would run past the `'`
760        // and produce the bogus tag "UNZ'UNB".
761        let segs: Vec<_> = crate::from_bytes(b"UNZ'UNB+A'")
762            .collect::<Result<Vec<_>, _>>()
763            .expect("element-less segment must parse");
764        assert_eq!(
765            segs.iter().map(|s| s.tag).collect::<Vec<_>>(),
766            vec!["UNZ", "UNB"]
767        );
768        assert!(segs[0].elements.is_empty());
769    }
770
771    #[test]
772    fn release_heavy_value_is_bounded_by_the_segment_guard() {
773        // A value consisting solely of release sequences and no delimiter must
774        // trip the per-segment guard rather than scanning the whole input once
775        // per release sequence (which was quadratic).
776        let mut input = b"BGM+".to_vec();
777        input.extend(std::iter::repeat_n(b"?a".as_slice(), 200_000).flatten());
778        let err = crate::from_bytes(&input)
779            .collect::<Result<Vec<_>, _>>()
780            .expect_err("oversized segment must be rejected");
781        assert!(
782            matches!(err, EdifactError::SegmentTooLong { .. }),
783            "expected SegmentTooLong, got {err:?}"
784        );
785    }
786
787    #[test]
788    fn an_oversized_segment_is_reported_as_such_even_with_multi_byte_text() {
789        // The scan window can cut a multi-byte sequence in half.  Validating
790        // UTF-8 before the size guard blamed the payload (`InvalidText`) for
791        // what is really an oversized segment, sending the reader hunting for an
792        // encoding problem that does not exist.
793        let mut input = b"BGM+".to_vec();
794        input.extend(std::iter::repeat_n("ä".as_bytes(), 200_000).flatten());
795        let err = crate::from_bytes(&input)
796            .collect::<Result<Vec<_>, _>>()
797            .expect_err("oversized segment must be rejected");
798        assert!(
799            matches!(err, EdifactError::SegmentTooLong { .. }),
800            "expected SegmentTooLong, got {err:?}"
801        );
802    }
803
804    #[test]
805    fn multi_byte_text_within_the_limit_still_parses() {
806        let segs: Vec<_> = crate::from_bytes("FTX+Grüße aus Köln'".as_bytes())
807            .collect::<Result<Vec<_>, _>>()
808            .expect("valid UTF-8 must parse");
809        assert_eq!(segs[0].element_str(0), Some("Grüße aus Köln"));
810    }
811
812    #[test]
813    fn escaped_terminator_inside_a_value_is_not_a_segment_break() {
814        // Exercises the cached-terminator refresh path: the first `'` is escaped,
815        // so the scan must resume past it and find the real terminator.
816        let segs: Vec<_> = crate::from_bytes(b"FTX+a?'b+c'")
817            .collect::<Result<Vec<_>, _>>()
818            .expect("escaped terminator must parse");
819        assert_eq!(segs.len(), 1);
820        assert_eq!(segs[0].element_str(0), Some("a'b"));
821        assert_eq!(segs[0].element_str(1), Some("c"));
822    }
823
824    #[test]
825    fn chunked_reader_parses_via_parser() {
826        // The reader tokenizer path was removed; verify the equivalent via the parser.
827        let input = b"UNA:+.? 'BGM+220+test?+value'UNT+2+1'";
828        let segments =
829            crate::parser::from_bufread(std::io::BufReader::new(std::io::Cursor::new(input)))
830                .expect("parser should succeed");
831        assert!(segments.iter().any(|s| s.tag == "BGM"));
832        // The release sequence '?+' inside 'test?+value' should survive in the element.
833        let bgm = segments.iter().find(|s| s.tag == "BGM").unwrap();
834        let raw_val = bgm
835            .elements
836            .get(1)
837            .and_then(|e| e.components.first())
838            .map(|(s, _)| s.as_str());
839        assert_eq!(raw_val, Some("test+value"));
840    }
841}