Skip to main content

oxixml_encoding/
encoder.rs

1//! Encoding UTF-8 text (Rust's native string representation) into a legacy byte encoding.
2//!
3//! This is the direction an XML *writer* needs: given a `&str` (always valid UTF-8, since
4//! that's what every Rust string is) and a target [`Encoding`], produce the bytes that encoding
5//! would decode back to that text — or, if some character has no representation in the target
6//! encoding, report exactly which one and where.
7
8use crate::cjk::{big5, euc_kr, gb18030};
9use crate::jis::{euc_jp, iso2022jp, shift_jis};
10use crate::label::Encoding;
11use crate::tables::{
12    LATIN1_CODEPOINTS, WINDOWS_1252_CODEPOINTS, encode_single_byte, encode_single_byte_upper,
13    legacy_single_byte_table,
14};
15
16/// An error reported by [`encode_from_utf8`]: `character` has no representation in the
17/// requested target encoding.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct EncodeError {
20    /// The character that could not be represented in the target encoding.
21    pub character: char,
22    /// The byte offset of `character` within the source `&str` passed to
23    /// [`encode_from_utf8`] (suitable for indexing/slicing that same `&str`).
24    pub position: usize,
25}
26
27impl std::fmt::Display for EncodeError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(
30            f,
31            "character {:?} (U+{:04X}) at byte offset {} has no representation in the target encoding",
32            self.character, self.character as u32, self.position
33        )
34    }
35}
36
37impl std::error::Error for EncodeError {}
38
39/// Encode `input` into `encoding`, appending the resulting bytes to `dst`.
40///
41/// [`Encoding::Utf8`] and the two UTF-16 variants can represent every Rust `char` (i.e. every
42/// Unicode scalar value) and therefore never fail. [`Encoding::Gb18030`] is *almost* total in the
43/// same sense — every scalar value except `U+E5E5` has a representation (see the type's own docs
44/// for why that one code point is excluded) — but every other encoding, including
45/// [`Encoding::Latin1`], [`Encoding::Windows1252`], [`Encoding::Ascii`], and the remaining legacy
46/// multi-byte encodings ([`Encoding::Gbk`], [`Encoding::Big5`], [`Encoding::EucKr`], and the three
47/// legacy Japanese ones), can only represent a subset of Unicode; the first character outside that
48/// subset is reported as an [`EncodeError`]. Characters encoded before the failing one have
49/// already been appended to `dst`, so partial output is available even after an error (useful
50/// for producing a best-effort diagnostic, or for switching strategies — e.g. falling back to a
51/// numeric character reference — and resuming from `error.position`).
52///
53/// # Errors
54///
55/// Returns [`EncodeError`] if `input` contains a character with no representation in `encoding`.
56///
57/// # Examples
58///
59/// ```
60/// use oxixml_encoding::{encode_from_utf8, Encoding};
61///
62/// let mut dst = Vec::new();
63/// encode_from_utf8("café", Encoding::Latin1, &mut dst).expect("café is representable in Latin-1");
64/// assert_eq!(dst, b"caf\xE9");
65///
66/// let mut dst = Vec::new();
67/// let err = encode_from_utf8("日本語", Encoding::Ascii, &mut dst).unwrap_err();
68/// assert_eq!(err.character, '日');
69/// assert_eq!(err.position, 0);
70/// ```
71pub fn encode_from_utf8(
72    input: &str,
73    encoding: Encoding,
74    dst: &mut Vec<u8>,
75) -> Result<(), EncodeError> {
76    match encoding {
77        Encoding::Utf8 => {
78            dst.extend_from_slice(input.as_bytes());
79            Ok(())
80        }
81        Encoding::Utf16Le => {
82            encode_utf16(input, dst, false);
83            Ok(())
84        }
85        Encoding::Utf16Be => {
86            encode_utf16(input, dst, true);
87            Ok(())
88        }
89        Encoding::Latin1 => encode_single_byte_str(input, dst, &LATIN1_CODEPOINTS),
90        Encoding::Windows1252 => encode_single_byte_str(input, dst, &WINDOWS_1252_CODEPOINTS),
91        Encoding::Ascii => encode_ascii(input, dst),
92        Encoding::ShiftJis => shift_jis::encode(input, dst),
93        Encoding::EucJp => euc_jp::encode(input, dst),
94        Encoding::Iso2022Jp => iso2022jp::encode(input, dst),
95        Encoding::Gbk => gb18030::encode(input, dst, true),
96        Encoding::Gb18030 => gb18030::encode(input, dst, false),
97        Encoding::Big5 => big5::encode(input, dst),
98        Encoding::EucKr => euc_kr::encode(input, dst),
99        // Every remaining variant is one of the 26 legacy single-byte encodings.
100        other => encode_legacy_single_byte_str(input, dst, other),
101    }
102}
103
104/// Encode `input` into one of the 26 legacy single-byte encodings backed by a 128-entry
105/// upper-half table (see [`Encoding::is_legacy_single_byte_table`]).
106fn encode_legacy_single_byte_str(
107    input: &str,
108    dst: &mut Vec<u8>,
109    encoding: Encoding,
110) -> Result<(), EncodeError> {
111    let table = legacy_single_byte_table(encoding);
112    dst.reserve(input.len());
113    for (position, character) in input.char_indices() {
114        match encode_single_byte_upper(table, character as u32) {
115            Some(byte) => dst.push(byte),
116            None => {
117                return Err(EncodeError {
118                    character,
119                    position,
120                });
121            }
122        }
123    }
124    Ok(())
125}
126
127/// Every Unicode scalar value is representable in UTF-16 (that is the entire point of the
128/// surrogate mechanism), so this never fails.
129fn encode_utf16(input: &str, dst: &mut Vec<u8>, big_endian: bool) {
130    dst.reserve(input.len() * 2);
131    for unit in input.encode_utf16() {
132        let bytes = if big_endian {
133            unit.to_be_bytes()
134        } else {
135            unit.to_le_bytes()
136        };
137        dst.extend_from_slice(&bytes);
138    }
139}
140
141fn encode_single_byte_str(
142    input: &str,
143    dst: &mut Vec<u8>,
144    table: &[u32; 256],
145) -> Result<(), EncodeError> {
146    dst.reserve(input.len());
147    for (position, character) in input.char_indices() {
148        match encode_single_byte(table, character as u32) {
149            Some(byte) => dst.push(byte),
150            None => {
151                return Err(EncodeError {
152                    character,
153                    position,
154                });
155            }
156        }
157    }
158    Ok(())
159}
160
161fn encode_ascii(input: &str, dst: &mut Vec<u8>) -> Result<(), EncodeError> {
162    dst.reserve(input.len());
163    for (position, character) in input.char_indices() {
164        if character.is_ascii() {
165            dst.push(character as u8);
166        } else {
167            return Err(EncodeError {
168                character,
169                position,
170            });
171        }
172    }
173    Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn utf8_target_is_a_plain_copy() -> Result<(), EncodeError> {
182        let mut dst = Vec::new();
183        encode_from_utf8("héllo→🎉", Encoding::Utf8, &mut dst)?;
184        assert_eq!(dst, "héllo→🎉".as_bytes());
185        Ok(())
186    }
187
188    #[test]
189    fn utf16_targets_never_fail_even_for_astral_characters() -> Result<(), EncodeError> {
190        let mut le = Vec::new();
191        encode_from_utf8("🎉", Encoding::Utf16Le, &mut le)?;
192        assert_eq!(
193            le,
194            "🎉"
195                .encode_utf16()
196                .flat_map(|u| u.to_le_bytes())
197                .collect::<Vec<_>>()
198        );
199
200        let mut be = Vec::new();
201        encode_from_utf8("🎉", Encoding::Utf16Be, &mut be)?;
202        assert_eq!(
203            be,
204            "🎉"
205                .encode_utf16()
206                .flat_map(|u| u.to_be_bytes())
207                .collect::<Vec<_>>()
208        );
209        Ok(())
210    }
211
212    #[test]
213    fn latin1_round_trips_within_0x00_to_0xff() -> Result<(), EncodeError> {
214        let mut dst = Vec::new();
215        encode_from_utf8("café", Encoding::Latin1, &mut dst)?;
216        assert_eq!(dst, b"caf\xE9");
217        Ok(())
218    }
219
220    #[test]
221    fn latin1_rejects_out_of_range_character() {
222        let mut dst = Vec::new();
223        let err = encode_from_utf8("→", Encoding::Latin1, &mut dst).expect_err("must fail");
224        assert_eq!(err.character, '→');
225        assert_eq!(err.position, 0);
226    }
227
228    #[test]
229    fn windows1252_encodes_smart_quotes() -> Result<(), EncodeError> {
230        let mut dst = Vec::new();
231        encode_from_utf8("\u{2018}hi\u{2019}", Encoding::Windows1252, &mut dst)?;
232        assert_eq!(dst, [0x91, b'h', b'i', 0x92]);
233        Ok(())
234    }
235
236    #[test]
237    fn windows1252_rejects_character_outside_the_repertoire() {
238        let mut dst = Vec::new();
239        let err = encode_from_utf8("あ", Encoding::Windows1252, &mut dst).expect_err("must fail");
240        assert_eq!(err.character, 'あ');
241    }
242
243    #[test]
244    fn ascii_rejects_first_non_ascii_character_with_byte_position() {
245        let mut dst = Vec::new();
246        let err = encode_from_utf8("ok日本語", Encoding::Ascii, &mut dst).expect_err("must fail");
247        assert_eq!(dst, b"ok");
248        assert_eq!(err.character, '日');
249        assert_eq!(err.position, 2);
250    }
251
252    #[test]
253    fn error_display_is_human_readable() {
254        let err = EncodeError {
255            character: 'あ',
256            position: 5,
257        };
258        assert_eq!(
259            err.to_string(),
260            "character 'あ' (U+3042) at byte offset 5 has no representation in the target encoding"
261        );
262    }
263
264    #[test]
265    fn legacy_single_byte_encodes_within_repertoire() -> Result<(), EncodeError> {
266        // windows-1250 0xA5 == Ą (LATIN CAPITAL LETTER A WITH OGONEK).
267        let mut dst = Vec::new();
268        encode_from_utf8("\u{0104}", Encoding::Windows1250, &mut dst)?;
269        assert_eq!(dst, vec![0xA5]);
270        Ok(())
271    }
272
273    #[test]
274    fn legacy_single_byte_rejects_character_outside_the_repertoire() {
275        let mut dst = Vec::new();
276        let err = encode_from_utf8("日", Encoding::Iso8859_2, &mut dst).expect_err("must fail");
277        assert_eq!(err.character, '日');
278        assert_eq!(err.position, 0);
279    }
280
281    #[test]
282    fn every_legacy_single_byte_encoding_round_trips_its_own_mapped_bytes() {
283        for &encoding in Encoding::ALL {
284            if !encoding.is_legacy_single_byte_table() {
285                continue;
286            }
287            let table = legacy_single_byte_table(encoding);
288            for pointer in 0u8..128 {
289                let codepoint = table[pointer as usize];
290                if codepoint == crate::tables::UNMAPPED {
291                    continue;
292                }
293                let Some(c) = char::from_u32(codepoint) else {
294                    continue;
295                };
296                let mut dst = Vec::new();
297                let text = c.to_string();
298                encode_from_utf8(&text, encoding, &mut dst)
299                    .unwrap_or_else(|e| panic!("{encoding}: {text:?}: {e}"));
300                assert_eq!(dst, vec![0x80 + pointer], "{encoding}: {text:?}");
301            }
302        }
303    }
304
305    #[test]
306    fn shift_jis_euc_jp_iso2022jp_are_routed_to_the_jis_module() -> Result<(), EncodeError> {
307        let mut dst = Vec::new();
308        encode_from_utf8("日", Encoding::ShiftJis, &mut dst)?;
309        assert_eq!(dst, vec![0x93, 0xFA]);
310
311        let mut dst = Vec::new();
312        encode_from_utf8("日", Encoding::EucJp, &mut dst)?;
313        assert_eq!(dst, vec![0xC6, 0xFC]);
314
315        let mut dst = Vec::new();
316        encode_from_utf8("日", Encoding::Iso2022Jp, &mut dst)?;
317        assert_eq!(dst, vec![0x1B, 0x24, 0x42, 0x46, 0x7C, 0x1B, 0x28, 0x42]);
318        Ok(())
319    }
320
321    #[test]
322    fn gbk_gb18030_big5_euc_kr_are_routed_to_the_cjk_module() -> Result<(), EncodeError> {
323        let mut dst = Vec::new();
324        encode_from_utf8("中", Encoding::Gbk, &mut dst)?;
325        assert_eq!(dst, vec![0xD6, 0xD0]);
326
327        let mut dst = Vec::new();
328        encode_from_utf8("中", Encoding::Gb18030, &mut dst)?;
329        assert_eq!(dst, vec![0xD6, 0xD0]);
330
331        let mut dst = Vec::new();
332        encode_from_utf8("中", Encoding::Big5, &mut dst)?;
333        assert_eq!(dst, vec![0xA4, 0xA4]);
334
335        let mut dst = Vec::new();
336        encode_from_utf8("한", Encoding::EucKr, &mut dst)?;
337        assert_eq!(dst, vec![0xC7, 0xD1]);
338        Ok(())
339    }
340}