Skip to main content

edifact_rs/
tokenizer.rs

1//! EDIFACT tokenizer — splits raw bytes into typed tokens.
2//!
3//! Respects UNA service string advice for non-default delimiters.
4//! Uses `memchr` for fast delimiter scanning (no byte-by-byte inner loops).
5
6use crate::{error::EdifactError, model::Span};
7use memchr::{memchr, 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}