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 #[inline]
293 fn una_start_pos(input: &[u8]) -> usize {
294 if input.len() >= 9 && &input[..3] == b"UNA" {
295 9
296 } else {
297 0
298 }
299 }
300
301 /// Construct a tokenizer with the default 64 KiB segment-size limit.
302 ///
303 /// If a single segment's byte length exceeds 65 536 bytes, the iterator
304 /// returns [`EdifactError::SegmentTooLong`]. This guards against
305 /// pathological or adversarially crafted inputs that omit segment
306 /// terminators and would otherwise cause unbounded scanning.
307 ///
308 /// Call [`Tokenizer::unlimited`] if you deliberately need to process
309 /// segments larger than 64 KiB, or [`Tokenizer::with_limit`] to supply a
310 /// custom bound.
311 pub fn new(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
312 Self::with_limit(input, ssa, 65_536)
313 }
314
315 /// Construct a tokenizer with **no** segment-size limit.
316 ///
317 /// # Security warning
318 ///
319 /// This constructor imposes **no upper bound** on how many bytes a single
320 /// segment may consume. For untrusted or adversarially crafted input a
321 /// missing segment terminator can cause the tokenizer to scan the entire
322 /// input before returning an error. Prefer [`Tokenizer::new`] (64 KiB
323 /// limit) or [`Tokenizer::with_limit`] for untrusted sources.
324 #[must_use]
325 pub fn unlimited(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
326 Self {
327 input,
328 pos: Self::una_start_pos(input),
329 ssa,
330 state: TokState::ExpectTag,
331 max_segment_bytes: usize::MAX,
332 segment_start: 0,
333 }
334 }
335
336 /// Construct a tokenizer with a segment-size limit.
337 ///
338 /// If a single segment's byte length (from the start of the tag to the end
339 /// of the last value, not including the terminator itself) exceeds `limit`,
340 /// the iterator returns [`EdifactError::SegmentTooLong`].
341 ///
342 /// # Examples
343 ///
344 /// ```
345 /// use edifact_rs::{ServiceStringAdvice, Tokenizer};
346 ///
347 /// let input = b"BGM+220+PO-4711+9'";
348 /// let ssa = ServiceStringAdvice::default();
349 /// let tokens: Vec<_> = Tokenizer::with_limit(input, ssa, 64)
350 /// .collect::<Result<_, _>>()
351 /// .unwrap();
352 /// assert!(!tokens.is_empty());
353 /// ```
354 pub fn with_limit(input: &'a [u8], ssa: ServiceStringAdvice, max_segment_bytes: usize) -> Self {
355 Self {
356 input,
357 pos: Self::una_start_pos(input),
358 ssa,
359 state: TokState::ExpectTag,
360 max_segment_bytes,
361 segment_start: 0,
362 }
363 }
364
365 /// Current byte position in the input.
366 #[inline]
367 pub fn position(&self) -> usize {
368 self.pos
369 }
370
371 /// Return the service string advice active for this tokenizer.
372 #[inline]
373 pub fn service_string_advice(&self) -> ServiceStringAdvice {
374 self.ssa
375 }
376
377 /// Consume leading whitespace / CR / LF between segments (not inside data values).
378 fn skip_inter_segment_whitespace(&mut self) {
379 while self.pos < self.input.len() {
380 match self.input[self.pos] {
381 b' ' | b'\t' | b'\r' | b'\n' => self.pos += 1,
382 _ => break,
383 }
384 }
385 }
386
387 /// Read a field value starting at `self.pos`, advancing past the value.
388 ///
389 /// Recognises the release character (`?` by default) and returns the raw
390 /// slice including release sequences. The parser layer resolves them.
391 ///
392 /// Uses `memchr3` to bulk-scan over non-special bytes between hits, only
393 /// falling back to a per-byte step when a release character is encountered.
394 /// Offset of the next segment terminator — or repetition separator, when the
395 /// interchange declares one — at or after `from`, searching within `window`.
396 ///
397 /// `memchr` tops out at three needles and `read_value` already spends those
398 /// on the element separator, component separator, and release character, so
399 /// the remaining one or two needles are searched separately and cached.
400 #[inline]
401 fn find_stop(&self, window: &[u8]) -> Option<usize> {
402 if self.ssa.is_repetition_active() {
403 memchr2(self.ssa.segment_term, self.ssa.repetition_sep, window)
404 } else {
405 memchr(self.ssa.segment_term, window)
406 }
407 }
408
409 fn read_value(&mut self) -> Result<(&'a str, Span), EdifactError> {
410 let start = self.pos;
411 let (elem, comp, release) = (
412 self.ssa.element_sep,
413 self.ssa.component_sep,
414 self.ssa.release_char,
415 );
416 // Absolute cap on how far this value may extend before the per-segment
417 // byte guard trips. Bounding the scan window here (rather than only
418 // checking the length after the loop) keeps adversarial input that omits
419 // every delimiter from forcing a scan across the whole remaining input.
420 let scan_end = self
421 .segment_start
422 .saturating_add(self.max_segment_bytes)
423 .saturating_add(1)
424 .min(self.input.len());
425
426 // Absolute offset of the next stop byte (segment terminator, plus the
427 // repetition separator when active) at or after the current search
428 // origin. `memchr3` below rescans only the bytes it actually consumes,
429 // but a naive re-search per iteration would rescan the whole tail on
430 // every release sequence, making a value such as `?a?a?a…` quadratic.
431 // Caching the hit keeps this search amortised linear: each rescan starts
432 // past the previous hit, so the scanned regions are disjoint.
433 let mut stop_hit = self
434 .find_stop(&self.input[self.pos..scan_end])
435 .map(|i| self.pos + i);
436
437 loop {
438 if self.pos >= scan_end {
439 break;
440 }
441 let remaining = &self.input[self.pos..scan_end];
442 // Refresh the cached stop position once the cursor has moved past it
443 // (only happens when a release sequence escaped a stop byte).
444 if stop_hit.is_some_and(|t| t < self.pos) {
445 stop_hit = self.find_stop(remaining).map(|i| self.pos + i);
446 }
447 let hit_ect = memchr3(elem, comp, release, remaining);
448 let hit_stop = stop_hit.map(|t| t - self.pos);
449 let hit = match (hit_ect, hit_stop) {
450 (None, None) => {
451 self.pos = scan_end;
452 break;
453 }
454 (Some(a), None) => a,
455 (None, Some(b)) => b,
456 (Some(a), Some(b)) => a.min(b),
457 };
458 let b = remaining[hit];
459 if b == release {
460 // A release char must be followed by exactly one escaped byte.
461 // If it is the last byte in the buffer the sequence is malformed.
462 if self.pos + hit + 1 >= self.input.len() {
463 return Err(EdifactError::InvalidReleaseSequence {
464 offset: self.pos + hit,
465 });
466 }
467 // Skip release char + the escaped byte.
468 self.pos += hit + 2;
469 continue;
470 }
471 // b is elem, comp, rep, or term — end of value.
472 self.pos += hit;
473 break;
474 }
475 // The size guard is checked *before* UTF-8 validation. `scan_end` can
476 // cut a multi-byte sequence in half, and reporting that as `InvalidText`
477 // blamed the payload for what is really an oversized segment.
478 if self.pos - self.segment_start > self.max_segment_bytes {
479 return Err(EdifactError::SegmentTooLong {
480 offset: self.segment_start,
481 limit: self.max_segment_bytes,
482 });
483 }
484 let span = Span::new(start, self.pos);
485 let value = std::str::from_utf8(&self.input[start..self.pos])
486 .map_err(|_| EdifactError::InvalidText { offset: start })?;
487 Ok((value, span))
488 }
489
490 /// Fast scan for the segment tag (exactly 3 ASCII uppercase letters).
491 fn read_tag(&mut self) -> Result<Option<Token<'a>>, EdifactError> {
492 self.skip_inter_segment_whitespace();
493 if self.pos >= self.input.len() {
494 return Ok(None);
495 }
496 let start = self.pos;
497 // A segment tag is terminated by the element separator or segment terminator.
498 // Bound the scan to max_segment_bytes + 1 so adversarial input with no delimiters
499 // cannot force memchr to scan arbitrarily large buffers before we return an error.
500 let input_remaining = &self.input[self.pos..];
501 let scan_limit = self
502 .max_segment_bytes
503 .saturating_add(1)
504 .min(input_remaining.len());
505 let remaining = &input_remaining[..scan_limit];
506 // Take the *nearest* of the two terminating delimiters. Searching for
507 // the element separator first and only falling back to the segment
508 // terminator would run straight past the terminator of an element-less
509 // segment (`UNZ'…`) and swallow the following segment's tag.
510 let end = memchr2(self.ssa.element_sep, self.ssa.segment_term, remaining)
511 .unwrap_or(remaining.len());
512
513 if end == 0 {
514 // First byte is already a delimiter — tag is zero-length, which is invalid.
515 let byte = self.input[self.pos];
516 self.pos += 1;
517 return Err(EdifactError::InvalidDelimiter {
518 byte,
519 offset: start,
520 });
521 }
522
523 // Enforce the per-segment byte-length guard in read_tag as well.
524 // Without this check, adversarial input with no delimiters could cause
525 // memchr to scan the entire remaining buffer (potentially hundreds of MB).
526 if end > self.max_segment_bytes {
527 // Advance past the offending bytes so the iterator can continue.
528 self.pos = start + end;
529 return Err(EdifactError::SegmentTooLong {
530 offset: start,
531 limit: self.max_segment_bytes,
532 });
533 }
534 let tag_bytes = &self.input[start..start + end];
535 // Always advance pos so errors cannot cause an infinite retry loop.
536 self.pos = start + end;
537 // Record segment start for the size-limit check in read_value.
538 self.segment_start = start;
539 let tag = std::str::from_utf8(tag_bytes)
540 .map_err(|_| EdifactError::InvalidSegmentTag(format!("{tag_bytes:?}")))?;
541 if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
542 return Err(EdifactError::InvalidSegmentTag(tag.to_owned()));
543 }
544 self.state = TokState::InSegment;
545 Ok(Some(Token::SegmentTag {
546 value: tag,
547 span: Span::new(start, start + end),
548 }))
549 }
550}
551
552impl<'a> Iterator for Tokenizer<'a> {
553 type Item = Result<Token<'a>, EdifactError>;
554
555 fn next(&mut self) -> Option<Self::Item> {
556 loop {
557 if self.pos >= self.input.len() {
558 return None;
559 }
560
561 match self.state {
562 TokState::ExpectTag => {
563 return match self.read_tag() {
564 Ok(Some(tok)) => Some(Ok(tok)),
565 Ok(None) => None,
566 Err(e) => Some(Err(e)),
567 };
568 }
569 TokState::InSegment => {
570 let b = self.input[self.pos];
571 let (elem, comp, term) = (
572 self.ssa.element_sep,
573 self.ssa.component_sep,
574 self.ssa.segment_term,
575 );
576
577 if b == term {
578 let start = self.pos;
579 self.pos += 1;
580 self.state = TokState::ExpectTag;
581 return Some(Ok(Token::SegmentTerminator {
582 span: Span::new(start, self.pos),
583 }));
584 } else if b == elem {
585 self.pos += 1;
586 let (value, span) = match self.read_value() {
587 Ok(value) => value,
588 Err(error) => return Some(Err(error)),
589 };
590 // Peek: is the *next* byte a component sep?
591 // We emit DataElement for the leading sub-element regardless;
592 // subsequent components within the same element are ComponentElement.
593 return Some(Ok(Token::DataElement { value, span }));
594 } else if b == comp {
595 self.pos += 1;
596 let (value, span) = match self.read_value() {
597 Ok(value) => value,
598 Err(error) => return Some(Err(error)),
599 };
600 return Some(Ok(Token::ComponentElement { value, span }));
601 } else if self.ssa.is_repetition_active() && b == self.ssa.repetition_sep {
602 self.pos += 1;
603 let (value, span) = match self.read_value() {
604 Ok(value) => value,
605 Err(error) => return Some(Err(error)),
606 };
607 return Some(Ok(Token::RepeatElement { value, span }));
608 } else if b == b'\r' || b == b'\n' {
609 self.pos += 1;
610 // inter-element whitespace inside a segment — skip
611 continue;
612 } else {
613 // Unexpected byte inside a segment — skip it and report.
614 let offset = self.pos;
615 self.pos += 1; // always advance to prevent infinite retry loop
616 self.state = TokState::ExpectTag;
617 return Some(Err(EdifactError::InvalidDelimiter { byte: b, offset }));
618 }
619 }
620 }
621 }
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 fn tokens(input: &[u8]) -> Vec<Token<'_>> {
630 let ssa = ServiceStringAdvice::from_bytes_unchecked(input);
631 Tokenizer::new(input, ssa)
632 .collect::<Result<Vec<_>, _>>()
633 .expect("tokenize failed")
634 }
635
636 #[test]
637 fn minimal_unb_unz() {
638 let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'UNZ+0+1'";
639 let toks = tokens(input);
640 assert!(matches!(toks[0], Token::SegmentTag { value: "UNB", .. }));
641 // should end with UNZ terminator
642 assert!(matches!(toks.last(), Some(Token::SegmentTerminator { .. })));
643 }
644
645 #[test]
646 fn release_character_not_a_delimiter() {
647 // `?+` inside a value must NOT produce a DataElement split
648 let input = b"BGM+220+test?+value'";
649 let toks = tokens(input);
650 // Elements after BGM tag: "220", "test?+value"
651 let vals: Vec<_> = toks
652 .iter()
653 .filter_map(|t| {
654 if let Token::DataElement { value, .. } = t {
655 Some(*value)
656 } else {
657 None
658 }
659 })
660 .collect();
661 assert_eq!(vals, vec!["220", "test?+value"]);
662 }
663
664 #[test]
665 fn custom_una_delimiters() {
666 // UNA with `;` as element sep
667 let input = b"UNA:;.? 'BGM;220;hello'";
668 let toks = tokens(input);
669 assert!(matches!(toks[0], Token::SegmentTag { value: "BGM", .. }));
670 let vals: Vec<_> = toks
671 .iter()
672 .filter_map(|t| {
673 if let Token::DataElement { value, .. } = t {
674 Some(*value)
675 } else {
676 None
677 }
678 })
679 .collect();
680 assert!(vals.contains(&"220"));
681 }
682
683 #[test]
684 fn tokens_expose_spans() {
685 let input = b"BGM+220+ABC'";
686 let toks = tokens(input);
687 assert!(matches!(
688 toks[0],
689 Token::SegmentTag {
690 value: "BGM",
691 span: Span { start: 0, end: 3 }
692 }
693 ));
694 assert!(matches!(
695 toks[1],
696 Token::DataElement {
697 value: "220",
698 span: Span { start: 4, end: 7 }
699 }
700 ));
701 }
702
703 #[test]
704 fn truncated_input_does_not_panic() {
705 let input = b"UNB+UNOA:1"; // no terminator
706 let _: Vec<_> = Tokenizer::new(input, ServiceStringAdvice::default()).collect();
707 // must not panic regardless of result
708 }
709
710 #[test]
711 fn invalid_segment_tags_are_rejected() {
712 for input in [
713 &b"bgm+220+'"[..],
714 &b"ABCDE+220+'"[..],
715 &b"BGM1+220+'"[..],
716 &b"BGM +220+'"[..],
717 &b" BG+220+'"[..],
718 ] {
719 let result = Tokenizer::new(input, ServiceStringAdvice::default())
720 .collect::<Result<Vec<_>, _>>();
721 assert!(result.is_err(), "expected tag rejection for {input:?}");
722 }
723 }
724
725 #[test]
726 fn element_less_segment_does_not_swallow_the_next_tag() {
727 // `read_tag` must stop at the *nearest* of element-separator and
728 // segment-terminator. Scanning for `+` first would run past the `'`
729 // and produce the bogus tag "UNZ'UNB".
730 let segs: Vec<_> = crate::from_bytes(b"UNZ'UNB+A'")
731 .collect::<Result<Vec<_>, _>>()
732 .expect("element-less segment must parse");
733 assert_eq!(
734 segs.iter().map(|s| s.tag).collect::<Vec<_>>(),
735 vec!["UNZ", "UNB"]
736 );
737 assert!(segs[0].elements.is_empty());
738 }
739
740 #[test]
741 fn release_heavy_value_is_bounded_by_the_segment_guard() {
742 // A value consisting solely of release sequences and no delimiter must
743 // trip the per-segment guard rather than scanning the whole input once
744 // per release sequence (which was quadratic).
745 let mut input = b"BGM+".to_vec();
746 input.extend(std::iter::repeat_n(b"?a".as_slice(), 200_000).flatten());
747 let err = crate::from_bytes(&input)
748 .collect::<Result<Vec<_>, _>>()
749 .expect_err("oversized segment must be rejected");
750 assert!(
751 matches!(err, EdifactError::SegmentTooLong { .. }),
752 "expected SegmentTooLong, got {err:?}"
753 );
754 }
755
756 #[test]
757 fn an_oversized_segment_is_reported_as_such_even_with_multi_byte_text() {
758 // The scan window can cut a multi-byte sequence in half. Validating
759 // UTF-8 before the size guard blamed the payload (`InvalidText`) for
760 // what is really an oversized segment, sending the reader hunting for an
761 // encoding problem that does not exist.
762 let mut input = b"BGM+".to_vec();
763 input.extend(std::iter::repeat_n("ä".as_bytes(), 200_000).flatten());
764 let err = crate::from_bytes(&input)
765 .collect::<Result<Vec<_>, _>>()
766 .expect_err("oversized segment must be rejected");
767 assert!(
768 matches!(err, EdifactError::SegmentTooLong { .. }),
769 "expected SegmentTooLong, got {err:?}"
770 );
771 }
772
773 #[test]
774 fn multi_byte_text_within_the_limit_still_parses() {
775 let segs: Vec<_> = crate::from_bytes("FTX+Grüße aus Köln'".as_bytes())
776 .collect::<Result<Vec<_>, _>>()
777 .expect("valid UTF-8 must parse");
778 assert_eq!(segs[0].element_str(0), Some("Grüße aus Köln"));
779 }
780
781 #[test]
782 fn escaped_terminator_inside_a_value_is_not_a_segment_break() {
783 // Exercises the cached-terminator refresh path: the first `'` is escaped,
784 // so the scan must resume past it and find the real terminator.
785 let segs: Vec<_> = crate::from_bytes(b"FTX+a?'b+c'")
786 .collect::<Result<Vec<_>, _>>()
787 .expect("escaped terminator must parse");
788 assert_eq!(segs.len(), 1);
789 assert_eq!(segs[0].element_str(0), Some("a'b"));
790 assert_eq!(segs[0].element_str(1), Some("c"));
791 }
792
793 #[test]
794 fn chunked_reader_parses_via_parser() {
795 // The reader tokenizer path was removed; verify the equivalent via the parser.
796 let input = b"UNA:+.? 'BGM+220+test?+value'UNT+2+1'";
797 let segments =
798 crate::parser::from_bufread(std::io::BufReader::new(std::io::Cursor::new(input)))
799 .expect("parser should succeed");
800 assert!(segments.iter().any(|s| s.tag == "BGM"));
801 // The release sequence '?+' inside 'test?+value' should survive in the element.
802 let bgm = segments.iter().find(|s| s.tag == "BGM").unwrap();
803 let raw_val = bgm
804 .elements
805 .get(1)
806 .and_then(|e| e.components.first())
807 .map(|(s, _)| s.as_str());
808 assert_eq!(raw_val, Some("test+value"));
809 }
810}