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];