1use std::path::Path;
21
22const CP1252_HIGH: [char; 32] = [
25 '\u{20AC}', '\u{0081}', '\u{201A}', '\u{0192}', '\u{201E}', '\u{2026}', '\u{2020}', '\u{2021}',
26 '\u{02C6}', '\u{2030}', '\u{0160}', '\u{2039}', '\u{0152}', '\u{008D}', '\u{017D}', '\u{008F}',
27 '\u{0090}', '\u{2018}', '\u{2019}', '\u{201C}', '\u{201D}', '\u{2022}', '\u{2013}', '\u{2014}',
28 '\u{02DC}', '\u{2122}', '\u{0161}', '\u{203A}', '\u{0153}', '\u{009D}', '\u{017E}', '\u{0178}',
29];
30
31const UTF8_BOM: &[u8] = &[0xEF, 0xBB, 0xBF];
32const UTF16_LE_BOM: &[u8] = &[0xFF, 0xFE];
33const UTF16_BE_BOM: &[u8] = &[0xFE, 0xFF];
34const SNIFF_BYTES: usize = 8192;
36
37#[must_use]
40pub(crate) fn has_utf16_bom(bytes: &[u8]) -> bool {
41 bytes.starts_with(UTF16_LE_BOM) || bytes.starts_with(UTF16_BE_BOM)
42}
43
44pub(crate) fn read_text(path: impl AsRef<Path>) -> std::io::Result<String> {
50 let bytes = std::fs::read(path.as_ref())?;
51 if looks_binary(&bytes) {
52 return Err(std::io::Error::new(
53 std::io::ErrorKind::InvalidData,
54 "binary content",
55 ));
56 }
57 Ok(decode(bytes))
58}
59
60pub fn read_text_checked(path: impl AsRef<Path>) -> std::io::Result<Decoded> {
65 let bytes = std::fs::read(path.as_ref())?;
66 if looks_binary(&bytes) {
67 return Err(std::io::Error::new(
68 std::io::ErrorKind::InvalidData,
69 "binary content",
70 ));
71 }
72 Ok(decode_checked(bytes))
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub(crate) enum DecodeLoss {
78 InvalidUtf8,
80 Utf16OddTail,
82 Utf16UnpairedSurrogate,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct Decoded {
96 pub text: String,
97 pub(crate) encoding: Encoding,
98 pub(crate) loss: Option<DecodeLoss>,
99}
100
101impl Decoded {
102 #[must_use]
103 pub fn complete(&self) -> bool {
104 self.loss.is_none()
105 }
106}
107
108#[must_use]
110pub(crate) fn decode_checked(mut bytes: Vec<u8>) -> Decoded {
111 if bytes.starts_with(UTF16_LE_BOM) {
112 return decode_utf16(&bytes[2..], u16::from_le_bytes, Encoding::Utf16Le);
113 }
114 if bytes.starts_with(UTF16_BE_BOM) {
115 return decode_utf16(&bytes[2..], u16::from_be_bytes, Encoding::Utf16Be);
116 }
117 let bom = bytes.starts_with(UTF8_BOM);
118 if bom {
119 bytes.drain(..UTF8_BOM.len());
120 }
121 match String::from_utf8(bytes) {
122 Ok(text) => Decoded {
123 text,
124 encoding: if bom {
125 Encoding::Utf8Bom
126 } else {
127 Encoding::Utf8
128 },
129 loss: None,
130 },
131 Err(err) => {
132 let bytes = err.into_bytes();
133 if is_mostly_utf8(&bytes) {
134 Decoded {
135 text: String::from_utf8_lossy(&bytes).into_owned(),
136 encoding: Encoding::Utf8Lossy,
137 loss: Some(DecodeLoss::InvalidUtf8),
138 }
139 } else {
140 Decoded {
141 text: decode_cp1252(&bytes),
142 encoding: Encoding::Windows1252,
143 loss: None,
144 }
145 }
146 }
147 }
148}
149
150#[must_use]
152pub fn looks_binary(bytes: &[u8]) -> bool {
153 !has_utf16_bom(bytes) && bytes.iter().take(SNIFF_BYTES).any(|&b| b == 0)
154}
155
156#[must_use]
158pub(crate) fn decode(bytes: Vec<u8>) -> String {
159 decode_checked(bytes).text
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub(crate) enum Encoding {
165 Utf8,
166 Utf8Bom,
167 Utf16Le,
168 Utf16Be,
169 Utf8Lossy,
171 Windows1252,
172}
173
174impl Encoding {
175 #[must_use]
176 pub(crate) fn label(self) -> &'static str {
177 match self {
178 Encoding::Utf8 => "utf-8",
179 Encoding::Utf8Bom => "utf-8 with BOM",
180 Encoding::Utf16Le => "utf-16le (BOM)",
181 Encoding::Utf16Be => "utf-16be (BOM)",
182 Encoding::Utf8Lossy => "utf-8 with invalid bytes (replaced by U+FFFD)",
183 Encoding::Windows1252 => "windows-1252 (legacy ANSI)",
184 }
185 }
186}
187
188#[must_use]
191pub(crate) fn detect_encoding(bytes: &[u8]) -> Encoding {
192 if bytes.starts_with(UTF16_LE_BOM) {
193 Encoding::Utf16Le
194 } else if bytes.starts_with(UTF16_BE_BOM) {
195 Encoding::Utf16Be
196 } else if let Some(body) = bytes.strip_prefix(UTF8_BOM) {
197 match detect_single_byte_or_utf8(body) {
198 Encoding::Utf8 => Encoding::Utf8Bom,
199 other => other,
200 }
201 } else {
202 detect_single_byte_or_utf8(bytes)
203 }
204}
205
206fn detect_single_byte_or_utf8(bytes: &[u8]) -> Encoding {
208 if std::str::from_utf8(bytes).is_ok() {
209 Encoding::Utf8
210 } else if is_mostly_utf8(bytes) {
211 Encoding::Utf8Lossy
212 } else {
213 Encoding::Windows1252
214 }
215}
216
217fn decode_utf16(body: &[u8], unit: fn([u8; 2]) -> u16, encoding: Encoding) -> Decoded {
218 let (pairs, tail) = body.as_chunks::<2>();
221 let mut loss = (!tail.is_empty()).then_some(DecodeLoss::Utf16OddTail);
222 let units = pairs.iter().map(|&pair| unit(pair));
223 let text = char::decode_utf16(units)
224 .map(|unit| {
225 unit.unwrap_or_else(|_| {
226 loss = Some(DecodeLoss::Utf16UnpairedSurrogate);
227 char::REPLACEMENT_CHARACTER
228 })
229 })
230 .collect();
231 Decoded {
232 text,
233 encoding,
234 loss,
235 }
236}
237
238fn is_mostly_utf8(bytes: &[u8]) -> bool {
242 let mut multibyte = 0usize;
243 let mut invalid = 0usize;
244 for chunk in bytes.utf8_chunks() {
245 multibyte += chunk.valid().chars().filter(|c| c.len_utf8() > 1).count();
246 if !chunk.invalid().is_empty() {
247 invalid += 1;
248 }
249 }
250 multibyte > invalid
251}
252
253fn decode_cp1252(bytes: &[u8]) -> String {
254 bytes
255 .iter()
256 .map(|&b| match b {
257 0x80..=0x9F => CP1252_HIGH[usize::from(b - 0x80)],
258 _ => char::from(b),
259 })
260 .collect()
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
270 fn decoder_reports_every_loss_and_keeps_faithful_encodings_complete() {
271 let utf16 = |bom: &[u8], units: &[u16], be: bool, tail: &[u8]| {
272 let mut bytes = bom.to_vec();
273 for unit in units {
274 bytes.extend_from_slice(&if be {
275 unit.to_be_bytes()
276 } else {
277 unit.to_le_bytes()
278 });
279 }
280 bytes.extend_from_slice(tail);
281 decode_checked(bytes)
282 };
283 let hi: Vec<u16> = "hi\n".encode_utf16().collect();
284 for (bom, be) in [(UTF16_LE_BOM, false), (UTF16_BE_BOM, true)] {
285 let clean = utf16(bom, &hi, be, &[]);
286 assert_eq!((clean.text.as_str(), clean.loss), ("hi\n", None));
287 assert_eq!(
288 utf16(bom, &hi, be, &[0x41]).loss,
289 Some(DecodeLoss::Utf16OddTail)
290 );
291 let unpaired = utf16(bom, &[0x0068, 0xD800, 0x0069], be, &[]);
292 assert_eq!(unpaired.loss, Some(DecodeLoss::Utf16UnpairedSurrogate));
293 assert!(!unpaired.complete());
294 }
295
296 let mut stray = "fn grüezi() { let ä = \"ö\"; }\n".as_bytes().to_vec();
297 stray.push(0xFF);
298 let lossy = decode_checked(stray);
299 assert_eq!(lossy.encoding, Encoding::Utf8Lossy);
300 assert_eq!(lossy.loss, Some(DecodeLoss::InvalidUtf8));
301
302 let ansi = decode_checked(b"// Gr\xF6\xDFe\r\n".to_vec());
304 assert_eq!((ansi.encoding, ansi.loss), (Encoding::Windows1252, None));
305 assert_eq!(ansi.text, "// Größe\r\n");
306 let mut bom = UTF8_BOM.to_vec();
307 bom.extend_from_slice(b"class A {}\n");
308 let with_bom = decode_checked(bom);
309 assert_eq!(
310 (with_bom.encoding, with_bom.loss),
311 (Encoding::Utf8Bom, None)
312 );
313 assert!(decode_checked(b"plain\n".to_vec()).complete());
314 }
315
316 #[test]
317 fn utf8_passes_through_and_loses_its_bom() {
318 assert_eq!(decode("fn grüezi() {}\n".into()), "fn grüezi() {}\n");
319 let mut bom = UTF8_BOM.to_vec();
320 bom.extend_from_slice(b"class A {}\n");
321 assert_eq!(decode(bom), "class A {}\n");
322 }
323
324 #[test]
325 fn windows_1252_umlauts_and_punctuation_decode() {
326 let ansi = b"// Gr\xF6\xDFe \x96 \x93Wert\x94 \x80\r\nint x;\r\n".to_vec();
328 let text = decode(ansi);
329 assert_eq!(text, "// Größe – “Wert” €\r\nint x;\r\n");
330 assert_eq!(text.lines().count(), 2, "line structure must be preserved");
331 }
332
333 #[test]
334 fn every_cp1252_byte_maps_to_one_char() {
335 let all: Vec<u8> = (0x80..=0xFF).collect();
336 assert_eq!(decode_cp1252(&all).chars().count(), all.len());
337 }
338
339 #[test]
340 fn utf16_with_bom_decodes_in_both_byte_orders() {
341 let text = "public class Größe {}\r\n";
342 let mut le = UTF16_LE_BOM.to_vec();
343 let mut be = UTF16_BE_BOM.to_vec();
344 for unit in text.encode_utf16() {
345 le.extend_from_slice(&unit.to_le_bytes());
346 be.extend_from_slice(&unit.to_be_bytes());
347 }
348 assert!(has_utf16_bom(&le) && has_utf16_bom(&be));
349 assert_eq!(decode(le.clone()), text);
350 assert_eq!(decode(be), text);
351 le.push(b'x'); assert_eq!(decode(le), text);
353 }
354
355 #[test]
356 fn mostly_utf8_with_a_corrupt_byte_stays_utf8() {
357 let mut bytes = "// äöü éè — fine\n".as_bytes().to_vec();
358 bytes.push(0xFF);
359 bytes.extend_from_slice(b"\nint y;\n");
360 let text = decode(bytes);
361 assert!(text.starts_with("// äöü éè — fine\n"), "{text}");
362 assert!(text.contains('\u{FFFD}'));
363 }
364
365 #[test]
366 fn detect_encoding_names_the_branch_decode_takes() {
367 assert_eq!(detect_encoding(b"plain"), Encoding::Utf8);
368 assert_eq!(detect_encoding(b"\xEF\xBB\xBFx"), Encoding::Utf8Bom);
369 assert_eq!(detect_encoding(b"\xFF\xFEx\x00"), Encoding::Utf16Le);
370 assert_eq!(detect_encoding(b"\xFE\xFF\x00x"), Encoding::Utf16Be);
371 assert_eq!(detect_encoding(b"Gr\xF6\xDFe"), Encoding::Windows1252);
372 let mut lossy = "äöü".as_bytes().to_vec();
373 lossy.push(0xFF);
374 assert_eq!(detect_encoding(&lossy), Encoding::Utf8Lossy);
375 assert_eq!(
378 detect_encoding(b"\xEF\xBB\xBF\xFF\xFE"),
379 Encoding::Windows1252
380 );
381 assert_eq!(decode(b"\xEF\xBB\xBF\xFF\xFE".to_vec()), "\u{FF}\u{FE}");
382 }
383
384 #[test]
385 fn read_text_reads_files_read_to_string_rejects() {
386 let dir = tempfile::tempdir().unwrap();
387 let path = dir.path().join("legacy.cs");
388 std::fs::write(&path, b"// K\xFCndigung\nclass K {}\n").unwrap();
389 assert!(std::fs::read_to_string(&path).is_err());
390 assert_eq!(read_text(&path).unwrap(), "// Kündigung\nclass K {}\n");
391 }
392}