1use encoding_rs::{
2 DecoderResult, Encoding, BIG5, EUC_KR, GBK, SHIFT_JIS, WINDOWS_1250, WINDOWS_1251,
3 WINDOWS_1252, WINDOWS_1253, WINDOWS_1254, WINDOWS_1255, WINDOWS_1256, WINDOWS_1257,
4 WINDOWS_1258, WINDOWS_874,
5};
6use thiserror::Error;
7
8use crate::LanguageId;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum TextEncoding {
14 Windows1250,
16 Windows1251,
18 Windows1252,
20 Windows1253,
22 Windows1254,
24 Windows1255,
26 Windows1256,
28 Windows1257,
30 Windows1258,
32 Windows874,
34 ShiftJis,
36 Gbk,
38 Big5,
40 EucKr,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Error)]
46#[error("character {character:?} at char index {char_index} is not encodable as {encoding:?}")]
47pub struct EncodeTextError {
48 pub char_index: usize,
50 pub character: char,
52 pub encoding: TextEncoding,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Error)]
58#[error("malformed byte sequence at byte index {byte_index} while decoding {encoding:?}")]
59pub struct DecodeTextError {
60 pub byte_index: usize,
62 pub encoding: TextEncoding,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Error)]
68#[error("unsupported language id {} for text encoding", language_id.raw())]
69pub struct LanguageEncodingError {
70 pub language_id: LanguageId,
72}
73
74pub fn decode_text(bytes: &[u8], encoding: TextEncoding) -> String {
79 let (decoded, _actual, _had_errors) = encoding_rs_codec(encoding).decode(bytes);
80 decoded.into_owned()
81}
82
83pub fn decode_text_strict(bytes: &[u8], encoding: TextEncoding) -> Result<String, DecodeTextError> {
91 let codec = encoding_rs_codec(encoding);
92 let Some(decoded) = codec.decode_without_bom_handling_and_without_replacement(bytes) else {
93 let byte_index = first_malformed_byte_index(bytes, codec);
94 return Err(DecodeTextError {
95 byte_index,
96 encoding,
97 });
98 };
99 Ok(decoded.into_owned())
100}
101
102pub fn encode_text(input: &str, encoding: TextEncoding) -> Result<Vec<u8>, EncodeTextError> {
109 let codec = encoding_rs_codec(encoding);
110 let (encoded, _actual, had_errors) = codec.encode(input);
111 if !had_errors {
112 return Ok(encoded.into_owned());
113 }
114
115 let (char_index, character) = first_unencodable(input, codec);
116 Err(EncodeTextError {
117 char_index,
118 character,
119 encoding,
120 })
121}
122
123pub fn text_encoding_for_language(
135 language_id: impl Into<LanguageId>,
136) -> Result<TextEncoding, LanguageEncodingError> {
137 let language_id = language_id.into();
138 let encoding = match language_id.raw() {
139 0..=4 => TextEncoding::Windows1252,
141 5 => TextEncoding::Windows1250,
142 32..=40 | 104 => TextEncoding::Windows1250,
144 41..=46 => TextEncoding::Windows1251,
146 47 => TextEncoding::Windows1253,
148 48..=50 => TextEncoding::Windows1254,
150 51 => TextEncoding::Windows1255,
152 52..=54 => TextEncoding::Windows1256,
154 55..=57 | 105 => TextEncoding::Windows1257,
156 58 => TextEncoding::Windows1258,
158 59 => TextEncoding::Windows874,
160 128 => TextEncoding::EucKr,
162 129 => TextEncoding::Big5,
163 130 => TextEncoding::Gbk,
164 131 => TextEncoding::ShiftJis,
165 70..=72 => {
169 return Err(LanguageEncodingError { language_id });
170 }
171 _ => TextEncoding::Windows1252,
173 };
174 Ok(encoding)
175}
176
177fn encoding_rs_codec(encoding: TextEncoding) -> &'static Encoding {
178 match encoding {
179 TextEncoding::Windows1250 => WINDOWS_1250,
180 TextEncoding::Windows1251 => WINDOWS_1251,
181 TextEncoding::Windows1252 => WINDOWS_1252,
182 TextEncoding::Windows1253 => WINDOWS_1253,
183 TextEncoding::Windows1254 => WINDOWS_1254,
184 TextEncoding::Windows1255 => WINDOWS_1255,
185 TextEncoding::Windows1256 => WINDOWS_1256,
186 TextEncoding::Windows1257 => WINDOWS_1257,
187 TextEncoding::Windows1258 => WINDOWS_1258,
188 TextEncoding::Windows874 => WINDOWS_874,
189 TextEncoding::ShiftJis => SHIFT_JIS,
190 TextEncoding::Gbk => GBK,
191 TextEncoding::Big5 => BIG5,
192 TextEncoding::EucKr => EUC_KR,
193 }
194}
195
196fn first_unencodable(input: &str, codec: &'static Encoding) -> (usize, char) {
197 let mut buf = [0u8; 4];
198 for (char_index, ch) in input.chars().enumerate() {
199 let s = ch.encode_utf8(&mut buf);
200 let (_encoded, _actual, had_errors) = codec.encode(s);
201 if had_errors {
202 return (char_index, ch);
203 }
204 }
205 (0, '\u{FFFD}')
206}
207
208fn first_malformed_byte_index(bytes: &[u8], codec: &'static Encoding) -> usize {
209 let mut decoder = codec.new_decoder_without_bom_handling();
210 let mut output = String::new();
211 let mut input = bytes;
212 let mut consumed_total = 0usize;
213
214 loop {
215 let reserve = decoder
216 .max_utf8_buffer_length_without_replacement(input.len())
217 .unwrap_or(input.len().saturating_mul(4).saturating_add(16));
218 output.reserve(reserve.max(8));
219
220 let (result, read) = decoder.decode_to_string_without_replacement(input, &mut output, true);
221 consumed_total = consumed_total.saturating_add(read);
222 input = &input[read..];
223
224 match result {
225 DecoderResult::InputEmpty => return bytes.len(),
226 DecoderResult::OutputFull => continue,
227 DecoderResult::Malformed(_, _) => return consumed_total,
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn windows_1252_roundtrip_is_lossless_for_supported_text() {
238 let original = "“Hello” € test";
239 let encoded = encode_text(original, TextEncoding::Windows1252).expect("must encode");
240 let decoded = decode_text(&encoded, TextEncoding::Windows1252);
241 assert_eq!(decoded, original);
242 }
243
244 #[test]
245 fn strict_encoder_rejects_unencodable_characters() {
246 let err = encode_text("hello 😀", TextEncoding::Windows1252).expect_err("must fail");
247 assert_eq!(err.character, '😀');
248 }
249
250 #[test]
251 fn windows_1250_roundtrip_is_lossless_for_supported_text() {
252 let original = "Zażółć gęślą jaźń";
253 let encoded = encode_text(original, TextEncoding::Windows1250).expect("must encode");
254 let decoded = decode_text(&encoded, TextEncoding::Windows1250);
255 assert_eq!(decoded, original);
256 }
257
258 #[test]
259 fn strict_decoder_rejects_malformed_multibyte_sequences() {
260 let err = decode_text_strict(&[0x81], TextEncoding::ShiftJis).expect_err("must fail");
261 assert_eq!(err.encoding, TextEncoding::ShiftJis);
262 }
263
264 #[test]
265 fn strict_decoder_roundtrips_valid_multibyte_sequences() {
266 let encoded = encode_text("テスト", TextEncoding::ShiftJis).expect("must encode");
267 let decoded = decode_text_strict(&encoded, TextEncoding::ShiftJis).expect("must decode");
268 assert_eq!(decoded, "テスト");
269 }
270
271 #[test]
272 fn language_id_mapping_returns_expected_encodings() {
273 assert_eq!(
274 text_encoding_for_language(0).expect("english"),
275 TextEncoding::Windows1252
276 );
277 assert_eq!(
278 text_encoding_for_language(5).expect("polish"),
279 TextEncoding::Windows1250
280 );
281 assert_eq!(
282 text_encoding_for_language(59).expect("thai"),
283 TextEncoding::Windows874
284 );
285 assert_eq!(
286 text_encoding_for_language(131).expect("japanese"),
287 TextEncoding::ShiftJis
288 );
289 }
290
291 #[test]
292 fn language_id_mapping_rejects_unsupported_legacy_codepages() {
293 let err = text_encoding_for_language(70).expect_err("must fail");
294 assert_eq!(err.language_id, LanguageId::from_raw(70));
295 }
296}