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}