use crate::{error::EdifactError, model::Span};
use memchr::{memchr, memchr2, memchr3};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ServiceStringAdvice {
pub element_sep: u8,
pub component_sep: u8,
pub release_char: u8,
pub decimal_mark: u8,
pub repetition_sep: u8,
pub segment_term: u8,
}
impl Default for ServiceStringAdvice {
fn default() -> Self {
Self {
element_sep: b'+',
component_sep: b':',
release_char: b'?',
decimal_mark: b'.',
repetition_sep: b' ',
segment_term: b'\'',
}
}
}
impl ServiceStringAdvice {
pub fn from_bytes(input: &[u8]) -> Result<Self, crate::error::EdifactError> {
let ssa = Self::from_bytes_unchecked(input);
if !ssa.is_valid() {
return Err(crate::error::EdifactError::InvalidUna);
}
Ok(ssa)
}
pub fn from_bytes_unchecked(input: &[u8]) -> Self {
let input = &input[prologue_len(input)..];
if input.len() >= 9 && &input[..3] == b"UNA" {
Self {
component_sep: input[3],
element_sep: input[4],
decimal_mark: input[5],
release_char: input[6],
repetition_sep: input[7],
segment_term: input[8],
}
} else {
Self::for_syntax_version(sniff_syntax_version(input))
}
}
#[must_use]
pub const fn for_syntax_version(version: Option<u8>) -> Self {
Self {
element_sep: b'+',
component_sep: b':',
release_char: b'?',
decimal_mark: b'.',
repetition_sep: match version {
Some(4) => b'*',
_ => b' ',
},
segment_term: b'\'',
}
}
pub fn is_valid(&self) -> bool {
let printable_ascii = |b: u8| (0x21..=0x7E).contains(&b) && !b.is_ascii_alphanumeric();
if !(0x20..=0x7E).contains(&self.decimal_mark) {
return false;
}
let active: [u8; 5] = [
self.component_sep,
self.element_sep,
self.release_char,
self.segment_term,
self.repetition_sep,
];
let active = &active[..if self.is_repetition_active() { 5 } else { 4 }];
active.iter().all(|&b| printable_ascii(b))
&& (0..active.len()).all(|i| active[i + 1..].iter().all(|&other| active[i] != other))
}
#[inline]
#[must_use]
pub const fn is_repetition_active(&self) -> bool {
self.repetition_sep != b' '
}
}
pub(crate) const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
#[inline]
pub(crate) fn prologue_len(input: &[u8]) -> usize {
let mut pos = usize::from(input.starts_with(&UTF8_BOM)) * 3;
while pos < input.len() && matches!(input[pos], b' ' | b'\t' | b'\r' | b'\n') {
pos += 1;
}
pos
}
#[inline]
pub(crate) fn is_valid_segment_tag(tag: &str) -> bool {
tag.len() == 3 && tag.bytes().all(|b| b.is_ascii_uppercase())
}
fn sniff_syntax_version(input: &[u8]) -> Option<u8> {
let pos = prologue_len(input);
if input.len() < pos + 4 || &input[pos..pos + 3] != b"UNB" || input[pos + 3] != b'+' {
return None;
}
let s001 = &input[pos + 4..];
let end = s001
.iter()
.position(|&b| b == b'+' || b == b'\'')
.unwrap_or(s001.len());
let mut components = s001[..end].split(|&b| b == b':');
let _identifier = components.next()?;
match components.next()? {
[digit @ b'1'..=b'9'] => Some(digit - b'0'),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Token<'a> {
SegmentTag {
value: &'a str,
span: Span,
},
DataElement {
value: &'a str,
span: Span,
},
ComponentElement {
value: &'a str,
span: Span,
},
RepeatElement {
value: &'a str,
span: Span,
},
SegmentTerminator {
span: Span,
},
}
#[derive(Debug)]
pub(crate) struct RawSegment {
pub(crate) bytes: Vec<u8>,
pub(crate) start_offset: usize,
}
pub struct Tokenizer<'a> {
input: &'a [u8],
pos: usize,
ssa: ServiceStringAdvice,
state: TokState,
max_segment_bytes: usize,
segment_start: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TokState {
ExpectTag,
InSegment,
}
impl<'a> Tokenizer<'a> {
#[inline]
fn una_start_pos(input: &[u8]) -> usize {
let start = prologue_len(input);
if input.len() >= start + 9 && &input[start..start + 3] == b"UNA" {
start + 9
} else {
start
}
}
#[must_use]
pub fn for_segment(
input: &'a [u8],
ssa: ServiceStringAdvice,
max_segment_bytes: usize,
) -> Self {
Self {
input,
pos: 0,
ssa,
state: TokState::ExpectTag,
max_segment_bytes,
segment_start: 0,
}
}
pub fn new(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
Self::with_limit(input, ssa, 65_536)
}
#[must_use]
pub fn unlimited(input: &'a [u8], ssa: ServiceStringAdvice) -> Self {
Self {
input,
pos: Self::una_start_pos(input),
ssa,
state: TokState::ExpectTag,
max_segment_bytes: usize::MAX,
segment_start: 0,
}
}
pub fn with_limit(input: &'a [u8], ssa: ServiceStringAdvice, max_segment_bytes: usize) -> Self {
Self {
input,
pos: Self::una_start_pos(input),
ssa,
state: TokState::ExpectTag,
max_segment_bytes,
segment_start: 0,
}
}
#[inline]
pub fn position(&self) -> usize {
self.pos
}
#[inline]
pub fn service_string_advice(&self) -> ServiceStringAdvice {
self.ssa
}
fn skip_inter_segment_whitespace(&mut self) {
while self.pos < self.input.len() {
match self.input[self.pos] {
b' ' | b'\t' | b'\r' | b'\n' => self.pos += 1,
_ => break,
}
}
}
#[inline]
fn find_stop(&self, window: &[u8]) -> Option<usize> {
if self.ssa.is_repetition_active() {
memchr2(self.ssa.segment_term, self.ssa.repetition_sep, window)
} else {
memchr(self.ssa.segment_term, window)
}
}
fn read_value(&mut self) -> Result<(&'a str, Span), EdifactError> {
let start = self.pos;
let (elem, comp, release) = (
self.ssa.element_sep,
self.ssa.component_sep,
self.ssa.release_char,
);
let scan_end = self
.segment_start
.saturating_add(self.max_segment_bytes)
.saturating_add(1)
.min(self.input.len());
let mut stop_hit = self
.find_stop(&self.input[self.pos..scan_end])
.map(|i| self.pos + i);
loop {
if self.pos >= scan_end {
break;
}
let remaining = &self.input[self.pos..scan_end];
if stop_hit.is_some_and(|t| t < self.pos) {
stop_hit = self.find_stop(remaining).map(|i| self.pos + i);
}
let hit_ect = memchr3(elem, comp, release, remaining);
let hit_stop = stop_hit.map(|t| t - self.pos);
let hit = match (hit_ect, hit_stop) {
(None, None) => {
self.pos = scan_end;
break;
}
(Some(a), None) => a,
(None, Some(b)) => b,
(Some(a), Some(b)) => a.min(b),
};
let b = remaining[hit];
if b == release {
if self.pos + hit + 1 >= self.input.len() {
return Err(EdifactError::InvalidReleaseSequence {
offset: self.pos + hit,
});
}
self.pos += hit + 2;
continue;
}
self.pos += hit;
break;
}
if self.pos - self.segment_start > self.max_segment_bytes {
return Err(EdifactError::SegmentTooLong {
offset: self.segment_start,
limit: self.max_segment_bytes,
});
}
let span = Span::new(start, self.pos);
let value = std::str::from_utf8(&self.input[start..self.pos])
.map_err(|_| EdifactError::InvalidText { offset: start })?;
Ok((value, span))
}
fn read_tag(&mut self) -> Result<Option<Token<'a>>, EdifactError> {
self.skip_inter_segment_whitespace();
if self.pos >= self.input.len() {
return Ok(None);
}
let start = self.pos;
let input_remaining = &self.input[self.pos..];
let scan_limit = self
.max_segment_bytes
.saturating_add(1)
.min(input_remaining.len());
let remaining = &input_remaining[..scan_limit];
let end = memchr2(self.ssa.element_sep, self.ssa.segment_term, remaining)
.unwrap_or(remaining.len());
if end == 0 {
let byte = self.input[self.pos];
self.pos += 1;
return Err(EdifactError::InvalidDelimiter {
byte,
offset: start,
});
}
if end > self.max_segment_bytes {
self.pos = start + end;
return Err(EdifactError::SegmentTooLong {
offset: start,
limit: self.max_segment_bytes,
});
}
let tag_bytes = &self.input[start..start + end];
self.pos = start + end;
self.segment_start = start;
let tag = std::str::from_utf8(tag_bytes)
.map_err(|_| EdifactError::InvalidSegmentTag(format!("{tag_bytes:?}")))?;
if !is_valid_segment_tag(tag) {
return Err(EdifactError::InvalidSegmentTag(tag.to_owned()));
}
self.state = TokState::InSegment;
Ok(Some(Token::SegmentTag {
value: tag,
span: Span::new(start, start + end),
}))
}
}
impl<'a> Iterator for Tokenizer<'a> {
type Item = Result<Token<'a>, EdifactError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.pos >= self.input.len() {
return None;
}
match self.state {
TokState::ExpectTag => {
return match self.read_tag() {
Ok(Some(tok)) => Some(Ok(tok)),
Ok(None) => None,
Err(e) => Some(Err(e)),
};
}
TokState::InSegment => {
let b = self.input[self.pos];
let (elem, comp, term) = (
self.ssa.element_sep,
self.ssa.component_sep,
self.ssa.segment_term,
);
if b == term {
let start = self.pos;
self.pos += 1;
self.state = TokState::ExpectTag;
return Some(Ok(Token::SegmentTerminator {
span: Span::new(start, self.pos),
}));
} else if b == elem {
self.pos += 1;
let (value, span) = match self.read_value() {
Ok(value) => value,
Err(error) => return Some(Err(error)),
};
return Some(Ok(Token::DataElement { value, span }));
} else if b == comp {
self.pos += 1;
let (value, span) = match self.read_value() {
Ok(value) => value,
Err(error) => return Some(Err(error)),
};
return Some(Ok(Token::ComponentElement { value, span }));
} else if self.ssa.is_repetition_active() && b == self.ssa.repetition_sep {
self.pos += 1;
let (value, span) = match self.read_value() {
Ok(value) => value,
Err(error) => return Some(Err(error)),
};
return Some(Ok(Token::RepeatElement { value, span }));
} else if b == b'\r' || b == b'\n' {
self.pos += 1;
continue;
} else {
let offset = self.pos;
self.pos += 1; self.state = TokState::ExpectTag;
return Some(Err(EdifactError::InvalidDelimiter { byte: b, offset }));
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tokens(input: &[u8]) -> Vec<Token<'_>> {
let ssa = ServiceStringAdvice::from_bytes_unchecked(input);
Tokenizer::new(input, ssa)
.collect::<Result<Vec<_>, _>>()
.expect("tokenize failed")
}
#[test]
fn syntax_version_4_activates_the_default_repetition_separator() {
let v4 = ServiceStringAdvice::from_bytes(b"UNB+UNOC:4+S+R+260101:0900+IC1'").unwrap();
assert!(v4.is_repetition_active());
assert_eq!(v4.repetition_sep, b'*');
let v3 = ServiceStringAdvice::from_bytes(b"UNB+UNOC:3+S+R+260101:0900+IC1'").unwrap();
assert!(!v3.is_repetition_active());
let fragment = ServiceStringAdvice::from_bytes(b"BGM+220'").unwrap();
assert!(!fragment.is_repetition_active());
}
#[test]
fn a_una_overrides_the_syntax_version_default() {
let input = b"UNA:+.? 'UNB+UNOC:4+S+R+260101:0900+IC1'";
let ssa = ServiceStringAdvice::from_bytes(input).unwrap();
assert!(!ssa.is_repetition_active());
}
#[test]
fn version_4_repetitions_parse_without_a_una() {
let input = b"UNB+UNOC:4+S+R+260101:0900+IC1'RFF+ON:1*ON:2'UNZ+0+IC1'";
let segments: Vec<_> = crate::from_bytes(input)
.collect::<Result<Vec<_>, _>>()
.unwrap();
let rff = segments[1].get_element(0).unwrap();
assert_eq!(rff.repeat_count(), 2);
assert_eq!(rff.repetition(1).unwrap()[1].0, "2");
}
#[test]
fn a_version_3_asterisk_stays_data() {
let input = b"UNB+UNOC:3+S+R+260101:0900+IC1'FTX+AAA+2*3'UNZ+0+IC1'";
let segments: Vec<_> = crate::from_bytes(input)
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(segments[1].element_str(1), Some("2*3"));
}
#[test]
fn the_ignored_decimal_mark_slot_never_invalidates_a_una() {
for una in [&b"UNA:+ ? '"[..], &b"UNA:+,? '"[..], &b"UNA:+:? '"[..]] {
assert!(
ServiceStringAdvice::from_bytes(una).is_ok(),
"{:?} must parse",
std::str::from_utf8(una).unwrap()
);
}
assert!(ServiceStringAdvice::from_bytes(b"UNA:+.: '").is_err());
}
#[test]
fn minimal_unb_unz() {
let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'UNZ+0+1'";
let toks = tokens(input);
assert!(matches!(toks[0], Token::SegmentTag { value: "UNB", .. }));
assert!(matches!(toks.last(), Some(Token::SegmentTerminator { .. })));
}
#[test]
fn release_character_not_a_delimiter() {
let input = b"BGM+220+test?+value'";
let toks = tokens(input);
let vals: Vec<_> = toks
.iter()
.filter_map(|t| {
if let Token::DataElement { value, .. } = t {
Some(*value)
} else {
None
}
})
.collect();
assert_eq!(vals, vec!["220", "test?+value"]);
}
#[test]
fn custom_una_delimiters() {
let input = b"UNA:;.? 'BGM;220;hello'";
let toks = tokens(input);
assert!(matches!(toks[0], Token::SegmentTag { value: "BGM", .. }));
let vals: Vec<_> = toks
.iter()
.filter_map(|t| {
if let Token::DataElement { value, .. } = t {
Some(*value)
} else {
None
}
})
.collect();
assert!(vals.contains(&"220"));
}
#[test]
fn tokens_expose_spans() {
let input = b"BGM+220+ABC'";
let toks = tokens(input);
assert!(matches!(
toks[0],
Token::SegmentTag {
value: "BGM",
span: Span { start: 0, end: 3 }
}
));
assert!(matches!(
toks[1],
Token::DataElement {
value: "220",
span: Span { start: 4, end: 7 }
}
));
}
#[test]
fn truncated_input_does_not_panic() {
let input = b"UNB+UNOA:1"; let _: Vec<_> = Tokenizer::new(input, ServiceStringAdvice::default()).collect();
}
#[test]
fn invalid_segment_tags_are_rejected() {
for input in [
&b"bgm+220+'"[..],
&b"ABCDE+220+'"[..],
&b"BGM1+220+'"[..],
&b"BGM +220+'"[..],
&b" BG+220+'"[..],
] {
let result = Tokenizer::new(input, ServiceStringAdvice::default())
.collect::<Result<Vec<_>, _>>();
assert!(result.is_err(), "expected tag rejection for {input:?}");
}
}
#[test]
fn element_less_segment_does_not_swallow_the_next_tag() {
let segs: Vec<_> = crate::from_bytes(b"UNZ'UNB+A'")
.collect::<Result<Vec<_>, _>>()
.expect("element-less segment must parse");
assert_eq!(
segs.iter().map(|s| s.tag()).collect::<Vec<_>>(),
vec!["UNZ", "UNB"]
);
assert!(segs[0].elements.is_empty());
}
#[test]
fn release_heavy_value_is_bounded_by_the_segment_guard() {
let mut input = b"BGM+".to_vec();
input.extend(std::iter::repeat_n(b"?a".as_slice(), 200_000).flatten());
let err = crate::from_bytes(&input)
.collect::<Result<Vec<_>, _>>()
.expect_err("oversized segment must be rejected");
assert!(
matches!(err, EdifactError::SegmentTooLong { .. }),
"expected SegmentTooLong, got {err:?}"
);
}
#[test]
fn an_oversized_segment_is_reported_as_such_even_with_multi_byte_text() {
let mut input = b"BGM+".to_vec();
input.extend(std::iter::repeat_n("ä".as_bytes(), 200_000).flatten());
let err = crate::from_bytes(&input)
.collect::<Result<Vec<_>, _>>()
.expect_err("oversized segment must be rejected");
assert!(
matches!(err, EdifactError::SegmentTooLong { .. }),
"expected SegmentTooLong, got {err:?}"
);
}
#[test]
fn multi_byte_text_within_the_limit_still_parses() {
let segs: Vec<_> = crate::from_bytes("FTX+Grüße aus Köln'".as_bytes())
.collect::<Result<Vec<_>, _>>()
.expect("valid UTF-8 must parse");
assert_eq!(segs[0].element_str(0), Some("Grüße aus Köln"));
}
#[test]
fn escaped_terminator_inside_a_value_is_not_a_segment_break() {
let segs: Vec<_> = crate::from_bytes(b"FTX+a?'b+c'")
.collect::<Result<Vec<_>, _>>()
.expect("escaped terminator must parse");
assert_eq!(segs.len(), 1);
assert_eq!(segs[0].element_str(0), Some("a'b"));
assert_eq!(segs[0].element_str(1), Some("c"));
}
#[test]
fn chunked_reader_parses_via_parser() {
let input = b"UNA:+.? 'BGM+220+test?+value'UNT+2+1'";
let segments: Vec<_> =
crate::parser::from_bufread(std::io::BufReader::new(std::io::Cursor::new(input)))
.collect::<Result<_, _>>()
.expect("parser should succeed");
assert!(segments.iter().any(|s| s.tag == "BGM"));
let bgm = segments.iter().find(|s| s.tag == "BGM").unwrap();
let raw_val = bgm
.elements
.get(1)
.and_then(|e| e.components.first())
.map(|(s, _)| s.as_ref());
assert_eq!(raw_val, Some("test+value"));
}
}