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}