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