1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct EncodeError {
19 pub character: char,
21 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
38pub 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 other => encode_legacy_single_byte_str(input, dst, other),
92 }
93}
94
95fn 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
118fn 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 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}