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, 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 now first-class fields. `is_valid()` checks
15/// all six for mutual distinctness and printability so that a collision between
16/// the repetition separator and any other delimiter is caught at UNA parse time.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct ServiceStringAdvice {
19 /// Data element separator (default `+`)
20 pub element_sep: u8,
21 /// Component data element separator (default `:`)
22 pub component_sep: u8,
23 /// Release character (default `?`)
24 pub release_char: u8,
25 /// Decimal notation mark (default `.`; UNA byte 5, ISO 9735-1 §7.1).
26 /// Not used by the tokenizer for splitting, but preserved for downstream use.
27 pub decimal_mark: u8,
28 /// Repetition separator (default `*`; UNA byte 7, ISO 9735-4 §3.1).
29 ///
30 /// Some DVGW gas-market profiles and other non-default EDIFACT implementations
31 /// use a non-standard repetition separator. This field is always populated
32 /// from the UNA (or defaults to `b'*'` when no UNA is present) so that
33 /// downstream code can access it without re-parsing the raw UNA bytes.
34 ///
35 /// The tokenizer does not currently split on the repetition separator — that
36 /// responsibility belongs to downstream consumers — but `is_valid()` includes
37 /// it in the six-way uniqueness check to catch delimiter collisions early.
38 pub repetition_sep: u8,
39 /// Segment terminator (default `'`)
40 pub segment_term: u8,
41}
42
43impl Default for ServiceStringAdvice {
44 fn default() -> Self {
45 Self {
46 element_sep: b'+',
47 component_sep: b':',
48 release_char: b'?',
49 decimal_mark: b'.',
50 // Space (0x20) is the conventional "not used" sentinel found at
51 // position 7 in the vast majority of real-world EDIFACT interchanges
52 // that do not employ ISO 9735-4 repetition elements. `is_valid()`
53 // accepts space here without a printability or uniqueness check.
54 repetition_sep: b' ',
55 segment_term: b'\'',
56 }
57 }
58}
59
60impl ServiceStringAdvice {
61 /// Parse a UNA header and validate that all six service characters
62 /// (`element_sep`, `component_sep`, `decimal_mark`, `release_char`,
63 /// `repetition_sep`, and `segment_term`) are mutually distinct and in
64 /// the printable ASCII range `0x21–0x7E`.
65 ///
66 /// Returns [`EdifactError::InvalidUna`] if the invariant is violated.
67 /// Falls back to [`ServiceStringAdvice::default`] when no UNA is present.
68 ///
69 /// This is the **safe, default constructor** — always use this for input from
70 /// an external source. For trusted or internal use where delimiter uniqueness
71 /// is already guaranteed, use [`from_bytes_unchecked`](Self::from_bytes_unchecked).
72 pub fn from_bytes(input: &[u8]) -> Result<Self, crate::error::EdifactError> {
73 let ssa = Self::from_bytes_unchecked(input);
74 if !ssa.is_valid() {
75 return Err(crate::error::EdifactError::InvalidUna);
76 }
77 Ok(ssa)
78 }
79
80 /// Parse a UNA header from the beginning of an EDIFACT interchange **without**
81 /// validating delimiter uniqueness or printability.
82 ///
83 /// If no UNA is present, returns [`ServiceStringAdvice::default`].
84 ///
85 /// The `repetition_sep` field is populated from UNA byte 7 (ISO 9735-4 §3.1)
86 /// or defaults to `b'*'` when no UNA is present.
87 ///
88 /// # When to use
89 ///
90 /// Use this only for trusted internal data (e.g. round-tripping data where
91 /// the UNA invariant is already guaranteed) or in fuzz/property tests that
92 /// intentionally explore degenerate delimiter combinations.
93 ///
94 /// For any external or user-provided input, prefer [`from_bytes`](Self::from_bytes)
95 /// which validates delimiter uniqueness and rejects invalid bytes.
96 pub fn from_bytes_unchecked(input: &[u8]) -> Self {
97 // UNA is 9 bytes: "UNA" + 6 service chars
98 if input.len() >= 9 && &input[..3] == b"UNA" {
99 Self {
100 component_sep: input[3],
101 element_sep: input[4],
102 decimal_mark: input[5],
103 release_char: input[6],
104 repetition_sep: input[7],
105 segment_term: input[8],
106 }
107 } else {
108 Self::default()
109 }
110 }
111
112 /// Return `true` if all active service characters are mutually distinct
113 /// and printable ASCII.
114 ///
115 /// The five *mandatory* characters (`element_sep`, `component_sep`,
116 /// `decimal_mark`, `release_char`, `segment_term`) must all be in the
117 /// printable ASCII range `0x21–0x7E` and mutually distinct (10 pairwise
118 /// checks).
119 ///
120 /// The `repetition_sep` field is also validated when it is **not a space**
121 /// (`0x20`). A space at position 7 of the UNA is the conventional
122 /// "absent" sentinel used by interchanges that do not employ repetition
123 /// elements (ISO 9735-1 / ISO 9735-4 §3.1), and it is accepted without
124 /// a printability or uniqueness check. Any other value must be printable
125 /// ASCII and distinct from the five mandatory characters and from the
126 /// configured repetition separator value.
127 ///
128 /// Bytes outside `0x21–0x7E` (for mandatory chars) or a non-space value
129 /// outside that range (for `repetition_sep`) are rejected because high-bytes
130 /// (`>= 0x80`) would incorrectly bisect multi-byte UTF-8 sequences, and DEL
131 /// (`0x7F`) is a non-printable control character.
132 pub fn is_valid(&self) -> bool {
133 let [e, c, d, r, t] = [
134 self.element_sep,
135 self.component_sep,
136 self.decimal_mark,
137 self.release_char,
138 self.segment_term,
139 ];
140 // All five mandatory chars must be printable ASCII 0x21–0x7E and
141 // mutually distinct (10 pairwise checks).
142 let printable_ascii = |b: u8| (0x21..=0x7E).contains(&b);
143 let basic_valid = printable_ascii(e)
144 && printable_ascii(c)
145 && printable_ascii(d)
146 && printable_ascii(r)
147 && printable_ascii(t)
148 && e != c
149 && e != d
150 && e != r
151 && e != t
152 && c != d
153 && c != r
154 && c != t
155 && d != r
156 && d != t
157 && r != t;
158 if !basic_valid {
159 return false;
160 }
161 // repetition_sep: space (0x20) means "not used" — accepted as-is.
162 // Any other value must be printable ASCII and distinct from all five
163 // mandatory service characters.
164 let rep = self.repetition_sep;
165 if rep == b' ' {
166 true
167 } else {
168 printable_ascii(rep) && rep != e && rep != c && rep != d && rep != r && rep != t
169 }
170 }
171}
172
173/// Token produced by [`Tokenizer`].
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub enum Token<'a> {
176 /// 3-character segment tag (e.g. `"BGM"`)
177 SegmentTag {
178 /// Raw tag value.
179 value: &'a str,
180 /// Source span of the tag.
181 span: Span,
182 },
183 /// Data element value (between element separators)
184 DataElement {
185 /// Raw element value.
186 value: &'a str,
187 /// Source span of the element value.
188 span: Span,
189 },
190 /// Component within a composite data element (between component separators)
191 ComponentElement {
192 /// Raw component value.
193 value: &'a str,
194 /// Source span of the component value.
195 span: Span,
196 },
197 /// Segment terminator — signals the end of a segment
198 SegmentTerminator {
199 /// Source span of the segment terminator byte.
200 span: Span,
201 },
202}
203
204#[derive(Debug)]
205pub(crate) struct RawSegment {
206 pub(crate) bytes: Vec<u8>,
207 pub(crate) start_offset: usize,
208}
209
210/// Zero-copy tokenizer over a byte slice.
211///
212/// Yields `Token` values, each borrowing from the original input.
213///
214/// # Segment size guard
215///
216/// The default constructor [`Tokenizer::new`] enforces a **64 KiB** per-segment
217/// limit, which is sufficient for all well-formed EDIFACT interchanges and guards
218/// against adversarially crafted inputs that omit segment terminators.
219/// Use [`Tokenizer::with_limit`] to raise or lower this threshold, or
220/// [`Tokenizer::unlimited`] to remove it entirely (trusted / pre-validated input only).
221pub struct Tokenizer<'a> {
222 input: &'a [u8],
223 pos: usize,
224 ssa: ServiceStringAdvice,
225 state: TokState,
226 /// Maximum allowed segment byte length (tag + elements, **excluding** the
227 /// segment terminator byte itself). Checked in `read_value` and `read_tag`.
228 /// `usize::MAX` = unlimited.
229 max_segment_bytes: usize,
230 /// Byte position where the current segment started (set in `read_tag`).
231 segment_start: usize,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235enum TokState {
236 /// Expecting a segment tag next
237 ExpectTag,
238 /// Inside a segment; next byte could be element or component sep, release, or terminator
239 InSegment,
240}
241
242impl<'a> Tokenizer<'a> {
243 /// Return the byte offset of the first non-UNA byte in `input`.
244 ///
245 /// If the input starts with the `UNA` service string advice (first 3
246 /// bytes are `b"UNA"`), the UNA header is exactly 9 bytes long and the
247 /// first segment tag starts at offset 9. Otherwise parsing starts at 0.
248 #[inline]
249 fn una_start_pos(input: &[u8]) -> usize {
250 if input.len() >= 9 && &input[..3] == b"UNA" {
251 9
252 } else {
253 0
254 }
255 }
256
257 /// Construct a tokenizer with the default 64 KiB segment-size limit.
258 ///
259 /// If a single segment's byte length exceeds 65 536 bytes, the iterator
260 /// returns [`EdifactError::SegmentTooLong`]. This guards against
261 /// pathological or adversarially crafted inputs that omit segment
262 /// terminators and would otherwise cause unbounded scanning.
263 ///
264 /// Call [`Tokenizer::unlimited`] if you deliberately need to process
265 /// segments larger than 64 KiB, or [`Tokenizer::with_limit`] to supply a
266 /// custom bound.
267 pub fn new(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
268 Self::with_limit(input, ssa, 65_536)
269 }
270
271 /// Construct a tokenizer with **no** segment-size limit.
272 ///
273 /// # Security warning
274 ///
275 /// This constructor imposes **no upper bound** on how many bytes a single
276 /// segment may consume. For untrusted or adversarially crafted input a
277 /// missing segment terminator can cause the tokenizer to scan the entire
278 /// input before returning an error. Prefer [`Tokenizer::new`] (64 KiB
279 /// limit) or [`Tokenizer::with_limit`] for untrusted sources.
280 #[must_use]
281 pub fn unlimited(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
282 Self {
283 input,
284 pos: Self::una_start_pos(input),
285 ssa,
286 state: TokState::ExpectTag,
287 max_segment_bytes: usize::MAX,
288 segment_start: 0,
289 }
290 }
291
292 /// Construct a tokenizer with a segment-size limit.
293 ///
294 /// If a single segment's byte length (from the start of the tag to the end
295 /// of the last value, not including the terminator itself) exceeds `limit`,
296 /// the iterator returns [`EdifactError::SegmentTooLong`].
297 ///
298 /// # Examples
299 ///
300 /// ```
301 /// use edifact_rs::{ServiceStringAdvice, Tokenizer};
302 ///
303 /// let input = b"BGM+220+PO-4711+9'";
304 /// let ssa = ServiceStringAdvice::default();
305 /// let tokens: Vec<_> = Tokenizer::with_limit(input, ssa, 64)
306 /// .collect::<Result<_, _>>()
307 /// .unwrap();
308 /// assert!(!tokens.is_empty());
309 /// ```
310 pub fn with_limit(input: &'a [u8], ssa: ServiceStringAdvice, max_segment_bytes: usize) -> Self {
311 Self {
312 input,
313 pos: Self::una_start_pos(input),
314 ssa,
315 state: TokState::ExpectTag,
316 max_segment_bytes,
317 segment_start: 0,
318 }
319 }
320
321 /// Current byte position in the input.
322 #[inline]
323 pub fn position(&self) -> usize {
324 self.pos
325 }
326
327 /// Return the service string advice active for this tokenizer.
328 #[inline]
329 pub fn service_string_advice(&self) -> ServiceStringAdvice {
330 self.ssa
331 }
332
333 /// Consume leading whitespace / CR / LF between segments (not inside data values).
334 fn skip_inter_segment_whitespace(&mut self) {
335 while self.pos < self.input.len() {
336 match self.input[self.pos] {
337 b' ' | b'\t' | b'\r' | b'\n' => self.pos += 1,
338 _ => break,
339 }
340 }
341 }
342
343 /// Read a field value starting at `self.pos`, advancing past the value.
344 ///
345 /// Recognises the release character (`?` by default) and returns the raw
346 /// slice including release sequences. The parser layer resolves them.
347 ///
348 /// Uses `memchr3` to bulk-scan over non-special bytes between hits, only
349 /// falling back to a per-byte step when a release character is encountered.
350 fn read_value(&mut self) -> Result<(&'a str, Span), EdifactError> {
351 let start = self.pos;
352 let (elem, comp, release, term) = (
353 self.ssa.element_sep,
354 self.ssa.component_sep,
355 self.ssa.release_char,
356 self.ssa.segment_term,
357 );
358 loop {
359 let remaining = &self.input[self.pos..];
360 if remaining.is_empty() {
361 break;
362 }
363 // Scan for release OR a value-terminating delimiter.
364 // memchr3 can hold three bytes; we combine elem/comp/release.
365 // A separate memchr finds term so we take the nearest hit.
366 let hit_ect = memchr3(elem, comp, release, remaining);
367 let hit_term = memchr(term, remaining);
368 let hit = match (hit_ect, hit_term) {
369 (None, None) => {
370 self.pos += remaining.len();
371 break;
372 }
373 (Some(a), None) => a,
374 (None, Some(b)) => b,
375 (Some(a), Some(b)) => a.min(b),
376 };
377 let b = remaining[hit];
378 if b == release {
379 // A release char must be followed by exactly one escaped byte.
380 // If it is the last byte in the buffer the sequence is malformed.
381 if remaining.len() - hit == 1 {
382 return Err(EdifactError::InvalidReleaseSequence {
383 offset: self.pos + hit,
384 });
385 }
386 // Skip release char + the escaped byte.
387 self.pos += hit + 2;
388 continue;
389 }
390 // b is elem, comp, or term — end of value.
391 self.pos += hit;
392 break;
393 }
394 let span = Span::new(start, self.pos);
395 let value = std::str::from_utf8(&self.input[start..self.pos])
396 .map_err(|_| EdifactError::InvalidText { offset: start })?;
397 // Enforce the per-segment byte-length guard.
398 if self.pos - self.segment_start > self.max_segment_bytes {
399 return Err(EdifactError::SegmentTooLong {
400 offset: self.segment_start,
401 limit: self.max_segment_bytes,
402 });
403 }
404 Ok((value, span))
405 }
406
407 /// Fast scan for the segment tag (exactly 3 ASCII uppercase letters).
408 fn read_tag(&mut self) -> Result<Option<Token<'a>>, EdifactError> {
409 self.skip_inter_segment_whitespace();
410 if self.pos >= self.input.len() {
411 return Ok(None);
412 }
413 let start = self.pos;
414 // A segment tag is terminated by the element separator or segment terminator.
415 // Bound the scan to max_segment_bytes + 1 so adversarial input with no delimiters
416 // cannot force memchr to scan arbitrarily large buffers before we return an error.
417 let input_remaining = &self.input[self.pos..];
418 let scan_limit = self
419 .max_segment_bytes
420 .saturating_add(1)
421 .min(input_remaining.len());
422 let remaining = &input_remaining[..scan_limit];
423 let end = memchr(self.ssa.element_sep, remaining)
424 .or_else(|| memchr(self.ssa.segment_term, remaining))
425 .unwrap_or(remaining.len());
426
427 if end == 0 {
428 // First byte is already a delimiter — tag is zero-length, which is invalid.
429 let byte = self.input[self.pos];
430 self.pos += 1;
431 return Err(EdifactError::InvalidDelimiter {
432 byte,
433 offset: start,
434 });
435 }
436
437 // Enforce the per-segment byte-length guard in read_tag as well.
438 // Without this check, adversarial input with no delimiters could cause
439 // memchr to scan the entire remaining buffer (potentially hundreds of MB).
440 if end > self.max_segment_bytes {
441 // Advance past the offending bytes so the iterator can continue.
442 self.pos = start + end;
443 return Err(EdifactError::SegmentTooLong {
444 offset: start,
445 limit: self.max_segment_bytes,
446 });
447 }
448 let tag_bytes = &self.input[start..start + end];
449 // Always advance pos so errors cannot cause an infinite retry loop.
450 self.pos = start + end;
451 // Record segment start for the size-limit check in read_value.
452 self.segment_start = start;
453 let tag = std::str::from_utf8(tag_bytes)
454 .map_err(|_| EdifactError::InvalidSegmentTag(format!("{tag_bytes:?}")))?;
455 if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
456 return Err(EdifactError::InvalidSegmentTag(tag.to_owned()));
457 }
458 self.state = TokState::InSegment;
459 Ok(Some(Token::SegmentTag {
460 value: tag,
461 span: Span::new(start, start + end),
462 }))
463 }
464}
465
466impl<'a> Iterator for Tokenizer<'a> {
467 type Item = Result<Token<'a>, EdifactError>;
468
469 fn next(&mut self) -> Option<Self::Item> {
470 loop {
471 if self.pos >= self.input.len() {
472 return None;
473 }
474
475 match self.state {
476 TokState::ExpectTag => {
477 return match self.read_tag() {
478 Ok(Some(tok)) => Some(Ok(tok)),
479 Ok(None) => None,
480 Err(e) => Some(Err(e)),
481 };
482 }
483 TokState::InSegment => {
484 let b = self.input[self.pos];
485 let (elem, comp, term) = (
486 self.ssa.element_sep,
487 self.ssa.component_sep,
488 self.ssa.segment_term,
489 );
490
491 if b == term {
492 let start = self.pos;
493 self.pos += 1;
494 self.state = TokState::ExpectTag;
495 return Some(Ok(Token::SegmentTerminator {
496 span: Span::new(start, self.pos),
497 }));
498 } else if b == elem {
499 self.pos += 1;
500 let (value, span) = match self.read_value() {
501 Ok(value) => value,
502 Err(error) => return Some(Err(error)),
503 };
504 // Peek: is the *next* byte a component sep?
505 // We emit DataElement for the leading sub-element regardless;
506 // subsequent components within the same element are ComponentElement.
507 return Some(Ok(Token::DataElement { value, span }));
508 } else if b == comp {
509 self.pos += 1;
510 let (value, span) = match self.read_value() {
511 Ok(value) => value,
512 Err(error) => return Some(Err(error)),
513 };
514 return Some(Ok(Token::ComponentElement { value, span }));
515 } else if b == b'\r' || b == b'\n' {
516 self.pos += 1;
517 // inter-element whitespace inside a segment — skip
518 continue;
519 } else {
520 // Unexpected byte inside a segment — skip it and report.
521 let offset = self.pos;
522 self.pos += 1; // always advance to prevent infinite retry loop
523 self.state = TokState::ExpectTag;
524 return Some(Err(EdifactError::InvalidDelimiter { byte: b, offset }));
525 }
526 }
527 }
528 }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 fn tokens(input: &[u8]) -> Vec<Token<'_>> {
537 let ssa = ServiceStringAdvice::from_bytes_unchecked(input);
538 Tokenizer::new(input, ssa)
539 .collect::<Result<Vec<_>, _>>()
540 .expect("tokenize failed")
541 }
542
543 #[test]
544 fn minimal_unb_unz() {
545 let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'UNZ+0+1'";
546 let toks = tokens(input);
547 assert!(matches!(toks[0], Token::SegmentTag { value: "UNB", .. }));
548 // should end with UNZ terminator
549 assert!(matches!(toks.last(), Some(Token::SegmentTerminator { .. })));
550 }
551
552 #[test]
553 fn release_character_not_a_delimiter() {
554 // `?+` inside a value must NOT produce a DataElement split
555 let input = b"BGM+220+test?+value'";
556 let toks = tokens(input);
557 // Elements after BGM tag: "220", "test?+value"
558 let vals: Vec<_> = toks
559 .iter()
560 .filter_map(|t| {
561 if let Token::DataElement { value, .. } = t {
562 Some(*value)
563 } else {
564 None
565 }
566 })
567 .collect();
568 assert_eq!(vals, vec!["220", "test?+value"]);
569 }
570
571 #[test]
572 fn custom_una_delimiters() {
573 // UNA with `;` as element sep
574 let input = b"UNA:;.? 'BGM;220;hello'";
575 let toks = tokens(input);
576 assert!(matches!(toks[0], Token::SegmentTag { value: "BGM", .. }));
577 let vals: Vec<_> = toks
578 .iter()
579 .filter_map(|t| {
580 if let Token::DataElement { value, .. } = t {
581 Some(*value)
582 } else {
583 None
584 }
585 })
586 .collect();
587 assert!(vals.contains(&"220"));
588 }
589
590 #[test]
591 fn tokens_expose_spans() {
592 let input = b"BGM+220+ABC'";
593 let toks = tokens(input);
594 assert!(matches!(
595 toks[0],
596 Token::SegmentTag {
597 value: "BGM",
598 span: Span { start: 0, end: 3 }
599 }
600 ));
601 assert!(matches!(
602 toks[1],
603 Token::DataElement {
604 value: "220",
605 span: Span { start: 4, end: 7 }
606 }
607 ));
608 }
609
610 #[test]
611 fn truncated_input_does_not_panic() {
612 let input = b"UNB+UNOA:1"; // no terminator
613 let _: Vec<_> = Tokenizer::new(input, ServiceStringAdvice::default()).collect();
614 // must not panic regardless of result
615 }
616
617 #[test]
618 fn invalid_segment_tags_are_rejected() {
619 for input in [
620 &b"bgm+220+'"[..],
621 &b"ABCDE+220+'"[..],
622 &b"BGM1+220+'"[..],
623 &b"BGM +220+'"[..],
624 &b" BG+220+'"[..],
625 ] {
626 let result = Tokenizer::new(input, ServiceStringAdvice::default())
627 .collect::<Result<Vec<_>, _>>();
628 assert!(result.is_err(), "expected tag rejection for {input:?}");
629 }
630 }
631
632 #[test]
633 fn chunked_reader_parses_via_parser() {
634 // The reader tokenizer path was removed; verify the equivalent via the parser.
635 let input = b"UNA:+.? 'BGM+220+test?+value'UNT+2+1'";
636 let segments =
637 crate::parser::from_bufread(std::io::BufReader::new(std::io::Cursor::new(input)))
638 .expect("parser should succeed");
639 assert!(segments.iter().any(|s| s.tag == "BGM"));
640 // The release sequence '?+' inside 'test?+value' should survive in the element.
641 let bgm = segments.iter().find(|s| s.tag == "BGM").unwrap();
642 let raw_val = bgm
643 .elements
644 .get(1)
645 .and_then(|e| e.components.first())
646 .map(|(s, _)| s.as_str());
647 assert_eq!(raw_val, Some("test+value"));
648 }
649}