Skip to main content

edifact_rs/
charset.rs

1//! EDIFACT character repertoires — the `UNB` S001 DE 0001 syntax identifier.
2//!
3//! An EDIFACT interchange is self-describing about its encoding: `UNB` S001
4//! component 1 names the repertoire the payload is written in, and the service
5//! characters, segment tags, and that identifier itself are always ASCII, so the
6//! header can be read before the encoding is known (ISO 9735-1 §4).
7//!
8//! # Why this exists
9//!
10//! **UTF-8 is not a superset of `UNOC`.** `UNOC` is ISO 8859-1, where `ü` is the
11//! single byte `0xFC` — which is not valid UTF-8. A German `ORDERS` carrying
12//! `Müller` is a perfectly conformant `UNOC` interchange, and decoding it as
13//! UTF-8 fails outright. The same holds for every `UNOD`…`UNOK` interchange.
14//!
15//! [`Charset`] converts those payloads to UTF-8 so the rest of the crate — which
16//! is UTF-8 throughout — can parse them:
17//!
18//! ```
19//! use edifact_rs::{Charset, decode_interchange, from_bytes};
20//!
21//! // A UNOC interchange: `Müller` is `4D FC 6C 6C 65 72`, not valid UTF-8.
22//! let mut raw = b"UNB+UNOC:3+S+R+200101:0900+1'NAD+BY+M".to_vec();
23//! raw.push(0xFC);
24//! raw.extend_from_slice(b"ller'UNZ+0+1'");
25//!
26//! // Parsing the raw bytes fails …
27//! assert!(from_bytes(&raw).collect::<Result<Vec<_>, _>>().is_err());
28//!
29//! // … so decode first. The syntax identifier is read out of the UNB.
30//! let utf8 = decode_interchange(&raw)?;
31//! let segments: Vec<_> = from_bytes(&utf8).collect::<Result<Vec<_>, _>>()?;
32//! assert_eq!(segments[1].element_str(1), Some("Müller"));
33//! # Ok::<(), edifact_rs::EdifactError>(())
34//! ```
35//!
36//! # Decoding is permissive, validation is strict
37//!
38//! [`decode`][Charset::decode] treats `UNOA` and `UNOB` as ASCII rather than
39//! enforcing their restricted repertoires, because real interchanges routinely
40//! carry a character or two outside level A and refusing to *parse* them would
41//! hide every other finding behind an encoding error. The repertoire is checked
42//! separately by [`permits`][Charset::permits], which the envelope validator
43//! surfaces as [`EdifactError::CharacterNotInRepertoire`] — a validation issue
44//! you can see alongside the rest of the report, or suppress.
45
46use crate::error::EdifactError;
47use std::borrow::Cow;
48use std::io::Read;
49
50/// An EDIFACT character repertoire, named by `UNB` S001 DE 0001.
51///
52/// Every variant is a **single-byte, ASCII-transparent** encoding or UTF-8, which
53/// is what lets the tokenizer scan for delimiters before decoding: bytes
54/// `0x00..=0x7F` mean the same thing in all of them.
55///
56/// `UNOX` (ISO 2022 code extension) and `KECA` (Korean) are deliberately absent:
57/// both are stateful or multi-byte in a way that would invalidate byte-level
58/// delimiter scanning, and pretending to support them would be worse than saying
59/// so. [`Charset::from_syntax_identifier`] reports
60/// [`EdifactError::UnsupportedCharset`] for them.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62#[non_exhaustive]
63pub enum Charset {
64    /// `UNOA` — ISO 9735 level A: upper-case letters, digits, space, and a fixed
65    /// punctuation set. Decoded as ASCII; see [`permits`][Self::permits].
66    UnoA,
67    /// `UNOB` — ISO 9735 level B: level A plus lower-case letters.
68    UnoB,
69    /// `UNOC` — ISO 8859-1 (Latin-1). By far the most common non-ASCII repertoire.
70    UnoC,
71    /// `UNOD` — ISO 8859-2 (Latin-2, Central European).
72    UnoD,
73    /// `UNOE` — ISO 8859-5 (Latin/Cyrillic).
74    UnoE,
75    /// `UNOF` — ISO 8859-7 (Latin/Greek).
76    UnoF,
77    /// `UNOG` — ISO 8859-3 (Latin-3, South European).
78    UnoG,
79    /// `UNOH` — ISO 8859-4 (Latin-4, North European).
80    UnoH,
81    /// `UNOI` — ISO 8859-6 (Latin/Arabic).
82    UnoI,
83    /// `UNOJ` — ISO 8859-8 (Latin/Hebrew).
84    UnoJ,
85    /// `UNOK` — ISO 8859-9 (Latin-5, Turkish).
86    UnoK,
87    /// `UNOY` — ISO 10646-1 / UTF-8. The identity transform for this crate.
88    UnoY,
89}
90
91/// Marks a code point the relevant ISO 8859 part leaves undefined.
92const UNDEFINED: u16 = 0xFFFF;
93
94/// How a repertoire maps bytes `0x80..=0xFF`.
95#[derive(Debug, Clone, Copy)]
96enum HighHalf {
97    /// Nothing above `0x7F` is in the repertoire (the ASCII subsets, and UTF-8,
98    /// which is decoded whole rather than byte by byte).
99    None,
100    /// The code point equals the byte value — ISO 8859-1.
101    Identity,
102    /// A part-specific table for `0xA0..=0xFF`; `0x80..=0x9F` stay C1 controls.
103    Table(&'static [u16; 96]),
104}
105
106/// ISO 9735 level A punctuation, in addition to `A`–`Z`, `0`–`9`, and space.
107const LEVEL_A_PUNCTUATION: &[char] = &[
108    '.', ',', '-', '(', ')', '/', '=', '\'', '+', ':', '?', '!', '"', '%', '&', '*', ';', '<', '>',
109];
110
111impl Charset {
112    /// Resolve a `UNB` S001 DE 0001 syntax identifier.
113    ///
114    /// # Errors
115    ///
116    /// [`EdifactError::UnsupportedCharset`] for `UNOX` and `KECA`, which this
117    /// crate cannot represent as a single-byte transform, and
118    /// [`EdifactError::UnrecognisedSyntaxIdentifier`] for anything not in
119    /// ISO 9735-1.
120    pub fn from_syntax_identifier(identifier: &str) -> Result<Self, EdifactError> {
121        Ok(match identifier {
122            "UNOA" => Self::UnoA,
123            "UNOB" => Self::UnoB,
124            "UNOC" => Self::UnoC,
125            "UNOD" => Self::UnoD,
126            "UNOE" => Self::UnoE,
127            "UNOF" => Self::UnoF,
128            "UNOG" => Self::UnoG,
129            "UNOH" => Self::UnoH,
130            "UNOI" => Self::UnoI,
131            "UNOJ" => Self::UnoJ,
132            "UNOK" => Self::UnoK,
133            "UNOY" => Self::UnoY,
134            "UNOX" | "KECA" => {
135                return Err(EdifactError::UnsupportedCharset {
136                    syntax_identifier: identifier.to_owned(),
137                });
138            }
139            other => {
140                return Err(EdifactError::UnrecognisedSyntaxIdentifier(other.to_owned()));
141            }
142        })
143    }
144
145    /// The `UNB` S001 DE 0001 identifier for this repertoire.
146    #[must_use]
147    pub const fn syntax_identifier(self) -> &'static str {
148        match self {
149            Self::UnoA => "UNOA",
150            Self::UnoB => "UNOB",
151            Self::UnoC => "UNOC",
152            Self::UnoD => "UNOD",
153            Self::UnoE => "UNOE",
154            Self::UnoF => "UNOF",
155            Self::UnoG => "UNOG",
156            Self::UnoH => "UNOH",
157            Self::UnoI => "UNOI",
158            Self::UnoJ => "UNOJ",
159            Self::UnoK => "UNOK",
160            Self::UnoY => "UNOY",
161        }
162    }
163
164    /// `true` when the payload is already UTF-8 and needs no transcoding.
165    ///
166    /// True for `UNOY`, and for `UNOA`/`UNOB` whose repertoires are ASCII
167    /// subsets — a conformant payload in either is valid UTF-8 unchanged.
168    #[must_use]
169    pub const fn is_utf8(self) -> bool {
170        matches!(self, Self::UnoY | Self::UnoA | Self::UnoB)
171    }
172
173    /// How this repertoire maps bytes `0x80..=0xFF`.
174    ///
175    /// Modelled explicitly rather than as an `Option<table>`: "no table" is true
176    /// of both the ASCII subsets — where nothing above `0x7F` exists at all — and
177    /// of ISO 8859-1, where the code point *is* the byte. Collapsing the two let
178    /// a `UNOA` writer happily emit `0xFC` for `ü`.
179    const fn high_half(self) -> HighHalf {
180        match self {
181            // Level A and level B are subsets of ASCII: the high half is empty.
182            Self::UnoA | Self::UnoB => HighHalf::None,
183            // UTF-8 is not a single-byte encoding; the byte-wise path never runs.
184            Self::UnoY => HighHalf::None,
185            // ISO 8859-1 maps every byte to the code point of the same value.
186            Self::UnoC => HighHalf::Identity,
187            Self::UnoD => HighHalf::Table(&UNOD_HIGH),
188            Self::UnoE => HighHalf::Table(&UNOE_HIGH),
189            Self::UnoF => HighHalf::Table(&UNOF_HIGH),
190            Self::UnoG => HighHalf::Table(&UNOG_HIGH),
191            Self::UnoH => HighHalf::Table(&UNOH_HIGH),
192            Self::UnoI => HighHalf::Table(&UNOI_HIGH),
193            Self::UnoJ => HighHalf::Table(&UNOJ_HIGH),
194            Self::UnoK => HighHalf::Table(&UNOK_HIGH),
195        }
196    }
197
198    /// Map one non-ASCII byte to its code point.
199    ///
200    /// `0x80..=0x9F` is the C1 control range, which every ISO 8859 part maps to
201    /// `U+0080..=U+009F`. Above that the part-specific table applies; a slot the
202    /// standard leaves undefined yields `None`.
203    fn decode_byte(self, byte: u8) -> Option<char> {
204        debug_assert!(byte >= 0x80, "decode_byte is for the non-ASCII half only");
205        match self.high_half() {
206            HighHalf::None => None,
207            HighHalf::Identity => char::from_u32(u32::from(byte)),
208            HighHalf::Table(table) => {
209                if byte < 0xA0 {
210                    return char::from_u32(u32::from(byte));
211                }
212                match table[usize::from(byte - 0xA0)] {
213                    UNDEFINED => None,
214                    code => char::from_u32(u32::from(code)),
215                }
216            }
217        }
218    }
219
220    /// Map one code point back to its byte, or `None` when unrepresentable.
221    fn encode_char(self, ch: char) -> Option<u8> {
222        let code = u32::from(ch);
223        if code < 0x80 {
224            return Some(code as u8);
225        }
226        match self.high_half() {
227            HighHalf::None => None,
228            HighHalf::Identity => u8::try_from(code).ok(),
229            HighHalf::Table(table) => {
230                if code < 0xA0 {
231                    return Some(code as u8);
232                }
233                table
234                    .iter()
235                    .position(|&c| c != UNDEFINED && u32::from(c) == code)
236                    .map(|i| (0xA0 + i) as u8)
237            }
238        }
239    }
240
241    /// Whether `ch` is in this repertoire.
242    ///
243    /// This is the strict ISO 9735 rule, and it is *not* applied while decoding —
244    /// see the module documentation. `UNOY` permits every `char`.
245    ///
246    /// # Example
247    ///
248    /// ```
249    /// use edifact_rs::Charset;
250    ///
251    /// assert!(Charset::UnoA.permits('A'));
252    /// assert!(!Charset::UnoA.permits('a'));   // level A is upper-case only
253    /// assert!(Charset::UnoB.permits('a'));
254    /// assert!(!Charset::UnoB.permits('ü'));   // level B is still ASCII
255    /// assert!(Charset::UnoC.permits('ü'));    // ISO 8859-1
256    /// assert!(!Charset::UnoC.permits('€'));   // not in Latin-1
257    /// ```
258    #[must_use]
259    pub fn permits(self, ch: char) -> bool {
260        match self {
261            Self::UnoY => true,
262            Self::UnoA => {
263                ch.is_ascii_uppercase()
264                    || ch.is_ascii_digit()
265                    || ch == ' '
266                    || LEVEL_A_PUNCTUATION.contains(&ch)
267            }
268            Self::UnoB => {
269                ch.is_ascii_lowercase()
270                    || ch.is_ascii_uppercase()
271                    || ch.is_ascii_digit()
272                    || ch == ' '
273                    || LEVEL_A_PUNCTUATION.contains(&ch)
274            }
275            _ => self.encode_char(ch).is_some(),
276        }
277    }
278
279    /// The first character of `text` that this repertoire cannot carry, with its
280    /// byte offset within `text`.
281    ///
282    /// `None` when every character is permitted.
283    #[must_use]
284    pub fn first_violation(self, text: &str) -> Option<(usize, char)> {
285        if self == Self::UnoY {
286            return None;
287        }
288        text.char_indices().find(|&(_, ch)| !self.permits(ch))
289    }
290
291    /// Decode `bytes` from this repertoire into UTF-8 text.
292    ///
293    /// Returns [`Cow::Borrowed`] — and copies nothing — when `bytes` is pure
294    /// ASCII, which covers the overwhelming majority of real EDIFACT even in a
295    /// `UNOC` interchange.
296    ///
297    /// # Errors
298    ///
299    /// [`EdifactError::InvalidText`] when a byte falls in a slot the repertoire
300    /// leaves undefined, or when a `UNOY` payload is not valid UTF-8. The
301    /// reported offset is the byte position within `bytes`.
302    pub fn decode(self, bytes: &[u8]) -> Result<Cow<'_, str>, EdifactError> {
303        if bytes.is_ascii() {
304            // SAFETY-FREE: an all-ASCII slice is valid UTF-8 by construction, and
305            // every supported repertoire agrees with ASCII on 0x00..=0x7F.
306            return std::str::from_utf8(bytes).map(Cow::Borrowed).map_err(|e| {
307                EdifactError::InvalidText {
308                    offset: e.valid_up_to(),
309                }
310            });
311        }
312        if self.is_utf8() {
313            return std::str::from_utf8(bytes).map(Cow::Borrowed).map_err(|e| {
314                EdifactError::InvalidText {
315                    offset: e.valid_up_to(),
316                }
317            });
318        }
319        // Every ISO 8859 code point is below U+0800, so two UTF-8 bytes is the
320        // worst case per input byte.
321        let mut out = String::with_capacity(bytes.len() + bytes.len() / 2);
322        for (offset, &byte) in bytes.iter().enumerate() {
323            if byte < 0x80 {
324                out.push(byte as char);
325            } else {
326                let ch = self
327                    .decode_byte(byte)
328                    .ok_or(EdifactError::InvalidText { offset })?;
329                out.push(ch);
330            }
331        }
332        Ok(Cow::Owned(out))
333    }
334
335    /// Encode UTF-8 `text` into this repertoire's bytes.
336    ///
337    /// Returns [`Cow::Borrowed`] when `text` is pure ASCII.
338    ///
339    /// # Errors
340    ///
341    /// [`EdifactError::CharacterNotInRepertoire`] for the first character the
342    /// repertoire cannot carry.
343    pub fn encode(self, text: &str) -> Result<Cow<'_, [u8]>, EdifactError> {
344        if text.is_ascii() || self == Self::UnoY {
345            return Ok(Cow::Borrowed(text.as_bytes()));
346        }
347        let mut out = Vec::with_capacity(text.len());
348        for (offset, ch) in text.char_indices() {
349            let byte = self
350                .encode_char(ch)
351                .ok_or(EdifactError::CharacterNotInRepertoire {
352                    charset: self.syntax_identifier(),
353                    character: ch,
354                    offset,
355                })?;
356            out.push(byte);
357        }
358        Ok(Cow::Owned(out))
359    }
360
361    /// Transcode a whole interchange to UTF-8 bytes.
362    ///
363    /// Returns [`Cow::Borrowed`] when nothing needs changing, so the zero-copy
364    /// path through [`from_bytes`][crate::from_bytes] is preserved for ASCII and
365    /// `UNOY` payloads.
366    ///
367    /// # Spans
368    ///
369    /// Byte offsets in the decoded buffer do **not** line up with the original
370    /// when transcoding actually happened — one `0xFC` becomes two bytes. Every
371    /// [`Span`][crate::Span] produced downstream indexes the **decoded** buffer,
372    /// which is the one you hold and the one diagnostics render against.
373    ///
374    /// # Errors
375    ///
376    /// As [`decode`][Self::decode].
377    pub fn transcode_to_utf8(self, bytes: &[u8]) -> Result<Cow<'_, [u8]>, EdifactError> {
378        match self.decode(bytes)? {
379            Cow::Borrowed(_) => Ok(Cow::Borrowed(bytes)),
380            Cow::Owned(text) => Ok(Cow::Owned(text.into_bytes())),
381        }
382    }
383
384    /// Wrap a reader so it yields UTF-8, transcoding from this repertoire.
385    ///
386    /// This is the streaming counterpart of
387    /// [`transcode_to_utf8`][Self::transcode_to_utf8]: it keeps the crate's
388    /// constant-memory guarantee intact for `UNOC`…`UNOK` input, which buffering
389    /// the whole interchange in order to decode it would not.
390    ///
391    /// # Example
392    ///
393    /// ```
394    /// use edifact_rs::{Charset, from_reader_collect};
395    ///
396    /// let mut raw = b"UNB+UNOC:3+S+R+200101:0900+1'NAD+BY+M".to_vec();
397    /// raw.push(0xFC);
398    /// raw.extend_from_slice(b"ller'UNZ+0+1'");
399    ///
400    /// let reader = Charset::UnoC.decoding_reader(std::io::Cursor::new(raw));
401    /// let segments = from_reader_collect(reader)?;
402    /// assert_eq!(segments[1].element_str(1), Some("Müller"));
403    /// # Ok::<(), edifact_rs::EdifactError>(())
404    /// ```
405    pub fn decoding_reader<R: Read>(self, reader: R) -> DecodingReader<R> {
406        DecodingReader {
407            inner: reader,
408            charset: self,
409            src: Vec::new(),
410            src_pos: 0,
411            spill: [0; 4],
412            spill_len: 0,
413            spill_pos: 0,
414        }
415    }
416}
417
418impl std::fmt::Display for Charset {
419    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420        f.write_str(self.syntax_identifier())
421    }
422}
423
424/// Adapter that transcodes a single-byte EDIFACT repertoire to UTF-8 on the fly.
425///
426/// Built by [`Charset::decoding_reader`].
427pub struct DecodingReader<R> {
428    inner: R,
429    charset: Charset,
430    /// Source bytes read but not yet transcoded.
431    src: Vec<u8>,
432    src_pos: usize,
433    /// A character that did not fit in the caller's buffer on the previous call.
434    spill: [u8; 4],
435    spill_len: u8,
436    spill_pos: u8,
437}
438
439impl<R: Read> Read for DecodingReader<R> {
440    fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
441        if out.is_empty() {
442            return Ok(0);
443        }
444        let mut written = 0;
445
446        // Finish the character that straddled the previous call's buffer end.
447        while self.spill_pos < self.spill_len && written < out.len() {
448            out[written] = self.spill[usize::from(self.spill_pos)];
449            self.spill_pos += 1;
450            written += 1;
451        }
452        if self.spill_pos == self.spill_len {
453            self.spill_pos = 0;
454            self.spill_len = 0;
455        }
456
457        loop {
458            if written == out.len() {
459                return Ok(written);
460            }
461            if self.src_pos == self.src.len() {
462                self.src.clear();
463                self.src_pos = 0;
464                self.src.resize(8192, 0);
465                let n = self.inner.read(&mut self.src)?;
466                self.src.truncate(n);
467                if n == 0 {
468                    return Ok(written);
469                }
470            }
471
472            let byte = self.src[self.src_pos];
473            self.src_pos += 1;
474
475            // `UNOY` is already UTF-8, and `UNOA`/`UNOB` are ASCII subsets whose
476            // conformant payloads are too — there is nothing to map, and mapping
477            // would fail: their high half is empty by definition.  Passing the
478            // byte through is both correct and what makes `decode_reader` safe to
479            // wrap around an interchange that turns out not to need decoding.
480            if byte < 0x80 || self.charset.is_utf8() {
481                out[written] = byte;
482                written += 1;
483                continue;
484            }
485
486            let ch = self.charset.decode_byte(byte).ok_or_else(|| {
487                std::io::Error::new(
488                    std::io::ErrorKind::InvalidData,
489                    format!(
490                        "byte 0x{byte:02X} is undefined in {}",
491                        self.charset.syntax_identifier()
492                    ),
493                )
494            })?;
495            let mut buf = [0u8; 4];
496            let encoded = ch.encode_utf8(&mut buf).as_bytes();
497            let room = out.len() - written;
498            let direct = room.min(encoded.len());
499            out[written..written + direct].copy_from_slice(&encoded[..direct]);
500            written += direct;
501            // Carry whatever did not fit into the next call.
502            if direct < encoded.len() {
503                let rest = &encoded[direct..];
504                self.spill[..rest.len()].copy_from_slice(rest);
505                self.spill_len = rest.len() as u8;
506                self.spill_pos = 0;
507                return Ok(written);
508            }
509        }
510    }
511}
512
513/// Read the `UNB` syntax identifier (S001 DE 0001) straight out of the raw bytes.
514///
515/// Deliberately byte-level rather than a call into the parser: the whole point is
516/// to learn the encoding *before* decoding anything, and running the parser over
517/// a `UNOC` interchange can fail on a sender name two elements later. Service
518/// characters, segment tags, and DE 0001 itself are ASCII in every repertoire
519/// (ISO 9735-1 §4), so this scan is always safe.
520///
521/// Returns `Ok(None)` when the input carries no `UNB` — an interchange fragment,
522/// or a bare message.
523///
524/// # Errors
525///
526/// As [`Charset::from_syntax_identifier`], plus [`EdifactError::InvalidUna`] when
527/// a `UNA` header is present but malformed.
528pub fn sniff_charset(input: &[u8]) -> Result<Option<Charset>, EdifactError> {
529    let ssa = crate::tokenizer::ServiceStringAdvice::from_bytes(input)?;
530    let mut pos = if input.len() >= 9 && &input[..3] == b"UNA" {
531        9
532    } else {
533        0
534    };
535    while pos < input.len() && matches!(input[pos], b' ' | b'\t' | b'\r' | b'\n') {
536        pos += 1;
537    }
538    if input.len() < pos + 4 || &input[pos..pos + 3] != b"UNB" || input[pos + 3] != ssa.element_sep
539    {
540        return Ok(None);
541    }
542    let start = pos + 4;
543    let end = input[start..]
544        .iter()
545        .position(|&b| b == ssa.component_sep || b == ssa.element_sep || b == ssa.segment_term)
546        .map_or(input.len(), |i| start + i);
547    let identifier = std::str::from_utf8(&input[start..end])
548        .map_err(|_| EdifactError::InvalidText { offset: start })?;
549    if identifier.is_empty() {
550        return Ok(None);
551    }
552    Charset::from_syntax_identifier(identifier).map(Some)
553}
554
555/// How many leading bytes [`decode_reader`] buffers in order to find the `UNB`.
556///
557/// A `UNA` is nine bytes and a `UNB` is a few hundred at the very most, so this
558/// is generous by an order of magnitude while still being a fixed, small cost.
559const SNIFF_PROBE_BYTES: usize = 4096;
560
561/// Wrap a reader so it yields UTF-8, reading the repertoire from the stream's own
562/// `UNB`.
563///
564/// The streaming counterpart of [`decode_interchange`], and the one to reach for
565/// when the repertoire is not known in advance:
566/// [`Charset::decoding_reader`] requires naming it, because a `Read` cannot be
567/// rewound after peeking.  This buffers the first 4 KiB, reads `UNB` S001 out of
568/// them, and chains them back in front of the rest — so nothing is lost and peak
569/// memory stays bounded regardless of interchange size.
570///
571/// A stream with no `UNB` is passed through unchanged.
572///
573/// # Example
574///
575/// ```
576/// use edifact_rs::{decode_reader, from_reader_collect};
577///
578/// let mut raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'NAD+BY+M".to_vec();
579/// raw.push(0xFC); // `ü` in ISO 8859-1
580/// raw.extend_from_slice(b"ller'UNZ+0+IC1'");
581///
582/// // The repertoire is discovered, not declared by the caller.
583/// let segments = from_reader_collect(decode_reader(std::io::Cursor::new(raw))?)?;
584/// assert_eq!(segments[1].element_str(1), Some("Müller"));
585/// # Ok::<(), edifact_rs::EdifactError>(())
586/// ```
587///
588/// # Errors
589///
590/// As [`sniff_charset`], plus any I/O error raised while reading the probe.
591#[allow(clippy::type_complexity)]
592pub fn decode_reader<R: Read>(
593    mut reader: R,
594) -> Result<DecodingReader<std::io::Chain<std::io::Cursor<Vec<u8>>, R>>, EdifactError> {
595    let mut head = vec![0u8; SNIFF_PROBE_BYTES];
596    let mut filled = 0;
597    while filled < head.len() {
598        // `read` is free to return short; loop until the probe is full or the
599        // stream ends, or a `UNB` split across two reads would go unnoticed.
600        match reader.read(&mut head[filled..])? {
601            0 => break,
602            n => filled += n,
603        }
604    }
605    head.truncate(filled);
606
607    // No `UNB` means nothing declares a repertoire; pass the bytes through.
608    let charset = sniff_charset(&head)?.unwrap_or(Charset::UnoY);
609    Ok(charset.decoding_reader(std::io::Cursor::new(head).chain(reader)))
610}
611
612/// Decode a whole interchange to UTF-8, reading the repertoire from its own `UNB`.
613///
614/// The one call to reach for when handling third-party EDIFACT: it is a no-op
615/// (and copies nothing) for ASCII and `UNOY` input, and converts `UNOC`…`UNOK`
616/// payloads that the rest of the crate would otherwise reject as invalid UTF-8.
617///
618/// An input with no `UNB` is returned unchanged, so wrapping a bare message in
619/// this call is harmless.
620///
621/// # Errors
622///
623/// As [`sniff_charset`] and [`Charset::decode`].
624pub fn decode_interchange(input: &[u8]) -> Result<Cow<'_, [u8]>, EdifactError> {
625    match sniff_charset(input)? {
626        Some(charset) => charset.transcode_to_utf8(input),
627        None => Ok(Cow::Borrowed(input)),
628    }
629}
630
631/// ISO 8859-2 (Latin-2, Central European) — code points for bytes `0xA0..=0xFF`.
632static UNOD_HIGH: [u16; 96] = [
633    0x00A0, 0x0104, 0x02D8, 0x0141, 0x00A4, 0x013D, 0x015A, 0x00A7, 0x00A8, 0x0160, 0x015E, 0x0164,
634    0x0179, 0x00AD, 0x017D, 0x017B, 0x00B0, 0x0105, 0x02DB, 0x0142, 0x00B4, 0x013E, 0x015B, 0x02C7,
635    0x00B8, 0x0161, 0x015F, 0x0165, 0x017A, 0x02DD, 0x017E, 0x017C, 0x0154, 0x00C1, 0x00C2, 0x0102,
636    0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E,
637    0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170,
638    0x00DC, 0x00DD, 0x0162, 0x00DF, 0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7,
639    0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, 0x0111, 0x0144, 0x0148, 0x00F3,
640    0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9,
641];
642
643/// ISO 8859-5 (Latin/Cyrillic) — code points for bytes `0xA0..=0xFF`.
644static UNOE_HIGH: [u16; 96] = [
645    0x00A0, 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A, 0x040B,
646    0x040C, 0x00AD, 0x040E, 0x040F, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
647    0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422, 0x0423,
648    0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
649    0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B,
650    0x043C, 0x043D, 0x043E, 0x043F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
651    0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, 0x2116, 0x0451, 0x0452, 0x0453,
652    0x0454, 0x0455, 0x0456, 0x0457, 0x0458, 0x0459, 0x045A, 0x045B, 0x045C, 0x00A7, 0x045E, 0x045F,
653];
654
655/// ISO 8859-7 (Latin/Greek) — code points for bytes `0xA0..=0xFF`.
656static UNOF_HIGH: [u16; 96] = [
657    0x00A0, 0x2018, 0x2019, 0x00A3, 0x20AC, 0x20AF, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x037A, 0x00AB,
658    0x00AC, 0x00AD, 0xFFFF, 0x2015, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x0384, 0x0385, 0x0386, 0x00B7,
659    0x0388, 0x0389, 0x038A, 0x00BB, 0x038C, 0x00BD, 0x038E, 0x038F, 0x0390, 0x0391, 0x0392, 0x0393,
660    0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F,
661    0x03A0, 0x03A1, 0xFFFF, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x03AA, 0x03AB,
662    0x03AC, 0x03AD, 0x03AE, 0x03AF, 0x03B0, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7,
663    0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C2, 0x03C3,
664    0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x03CA, 0x03CB, 0x03CC, 0x03CD, 0x03CE, 0xFFFF,
665];
666
667/// ISO 8859-3 (Latin-3, South European) — code points for bytes `0xA0..=0xFF`.
668static UNOG_HIGH: [u16; 96] = [
669    0x00A0, 0x0126, 0x02D8, 0x00A3, 0x00A4, 0xFFFF, 0x0124, 0x00A7, 0x00A8, 0x0130, 0x015E, 0x011E,
670    0x0134, 0x00AD, 0xFFFF, 0x017B, 0x00B0, 0x0127, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x0125, 0x00B7,
671    0x00B8, 0x0131, 0x015F, 0x011F, 0x0135, 0x00BD, 0xFFFF, 0x017C, 0x00C0, 0x00C1, 0x00C2, 0xFFFF,
672    0x00C4, 0x010A, 0x0108, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF,
673    0xFFFF, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x0120, 0x00D6, 0x00D7, 0x011C, 0x00D9, 0x00DA, 0x00DB,
674    0x00DC, 0x016C, 0x015C, 0x00DF, 0x00E0, 0x00E1, 0x00E2, 0xFFFF, 0x00E4, 0x010B, 0x0109, 0x00E7,
675    0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, 0xFFFF, 0x00F1, 0x00F2, 0x00F3,
676    0x00F4, 0x0121, 0x00F6, 0x00F7, 0x011D, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x016D, 0x015D, 0x02D9,
677];
678
679/// ISO 8859-4 (Latin-4, North European) — code points for bytes `0xA0..=0xFF`.
680static UNOH_HIGH: [u16; 96] = [
681    0x00A0, 0x0104, 0x0138, 0x0156, 0x00A4, 0x0128, 0x013B, 0x00A7, 0x00A8, 0x0160, 0x0112, 0x0122,
682    0x0166, 0x00AD, 0x017D, 0x00AF, 0x00B0, 0x0105, 0x02DB, 0x0157, 0x00B4, 0x0129, 0x013C, 0x02C7,
683    0x00B8, 0x0161, 0x0113, 0x0123, 0x0167, 0x014A, 0x017E, 0x014B, 0x0100, 0x00C1, 0x00C2, 0x00C3,
684    0x00C4, 0x00C5, 0x00C6, 0x012E, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x0116, 0x00CD, 0x00CE, 0x012A,
685    0x0110, 0x0145, 0x014C, 0x0136, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x0172, 0x00DA, 0x00DB,
686    0x00DC, 0x0168, 0x016A, 0x00DF, 0x0101, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x012F,
687    0x010D, 0x00E9, 0x0119, 0x00EB, 0x0117, 0x00ED, 0x00EE, 0x012B, 0x0111, 0x0146, 0x014D, 0x0137,
688    0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x0173, 0x00FA, 0x00FB, 0x00FC, 0x0169, 0x016B, 0x02D9,
689];
690
691/// ISO 8859-6 (Latin/Arabic) — code points for bytes `0xA0..=0xFF`.
692static UNOI_HIGH: [u16; 96] = [
693    0x00A0, 0xFFFF, 0xFFFF, 0xFFFF, 0x00A4, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
694    0x060C, 0x00AD, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
695    0xFFFF, 0xFFFF, 0xFFFF, 0x061B, 0xFFFF, 0xFFFF, 0xFFFF, 0x061F, 0xFFFF, 0x0621, 0x0622, 0x0623,
696    0x0624, 0x0625, 0x0626, 0x0627, 0x0628, 0x0629, 0x062A, 0x062B, 0x062C, 0x062D, 0x062E, 0x062F,
697    0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637, 0x0638, 0x0639, 0x063A, 0xFFFF,
698    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x0640, 0x0641, 0x0642, 0x0643, 0x0644, 0x0645, 0x0646, 0x0647,
699    0x0648, 0x0649, 0x064A, 0x064B, 0x064C, 0x064D, 0x064E, 0x064F, 0x0650, 0x0651, 0x0652, 0xFFFF,
700    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
701];
702
703/// ISO 8859-8 (Latin/Hebrew) — code points for bytes `0xA0..=0xFF`.
704static UNOJ_HIGH: [u16; 96] = [
705    0x00A0, 0xFFFF, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00D7, 0x00AB,
706    0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7,
707    0x00B8, 0x00B9, 0x00F7, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
708    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
709    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
710    0xFFFF, 0xFFFF, 0xFFFF, 0x2017, 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7,
711    0x05D8, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF, 0x05E0, 0x05E1, 0x05E2, 0x05E3,
712    0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0xFFFF, 0xFFFF, 0x200E, 0x200F, 0xFFFF,
713];
714
715/// ISO 8859-9 (Latin-5, Turkish) — code points for bytes `0xA0..=0xFF`.
716static UNOK_HIGH: [u16; 96] = [
717    0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB,
718    0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7,
719    0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, 0x00C0, 0x00C1, 0x00C2, 0x00C3,
720    0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF,
721    0x011E, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB,
722    0x00DC, 0x0130, 0x015E, 0x00DF, 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7,
723    0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, 0x011F, 0x00F1, 0x00F2, 0x00F3,
724    0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0131, 0x015F, 0x00FF,
725];