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