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