Browse Source

Fichiers texte : un octet invalide n'est plus relu qu'à lui seul en Windows-1252

Dès qu'un seul octet d'un fichier n'était pas de l'UTF-8 valide (fichier
coupé au milieu d'un caractère, octet glissé par un vieux logiciel),
decode_text_file relisait le fichier entier en Windows-1252 : tous les
accents corrects devenaient « é », « è »… et Claude lisait un texte
déformé.

Un gestionnaire d'erreurs enregistré avec codecs.register_error relit
maintenant en Windows-1252 les seuls octets invalides. Un vrai fichier
« ANSI » reste lu accent par accent comme avant, un fichier mélangé
(lignes UTF-8 et Windows-1252) est lu correctement, et un octet abîmé
n'abîme que lui-même.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 23 giờ trước cách đây
mục cha
commit
321fece0d9
3 tập tin đã thay đổi với 31 bổ sung và 6 xóa
  1. 2 0
      CHANGELOG.md
  2. 15 6
      bot/text_files.py
  3. 14 0
      tests/test_attachments_prep.py

+ 2 - 0
CHANGELOG.md

@@ -20,6 +20,8 @@ Les versions les plus récentes sont en haut. Chaque entrée commence par un ré
 
 - **Les grandes images en gris 16 bits ne deviennent plus toutes blanches.** Un scan, une radio ou une photo d'astronomie en PNG ou TIFF 16 bits (ou 32 bits), une fois réduit, arrivait tout blanc ou tout noir chez Claude. Ses gris sont maintenant ramenés sur 8 bits sans être écrêtés (Pillow 11 ou plus, installé avec les autres dépendances).
 
+- **Un octet abîmé ne déforme plus tous les accents d'un fichier texte.** Dans un `.txt`, `.log` ou `.csv` en UTF-8, un seul octet invalide faisait lire tout le fichier en Windows-1252 (« Café »). Seul cet octet est maintenant relu ainsi ; les fichiers « ANSI » et les fichiers mélangés restent bien lus.
+
 ---
 
 ## Version 5.12

+ 15 - 6
bot/text_files.py

@@ -21,8 +21,21 @@ class TextFileError(UserFacingError):
     """Fichier texte illisible ou refusé, avec un message affichable sur Discord."""
 
 
+def _cp1252_fallback(error: UnicodeDecodeError) -> tuple[str, int]:
+    """Relit en Windows-1252 les seuls octets qui ne sont pas de l'UTF-8 valide.
+
+    Un fichier d'un ancien éditeur Windows (Bloc-notes « ANSI ») est ainsi lu
+    correctement, accent par accent, et un octet isolé dans un fichier UTF-8
+    n'abîme que lui-même au lieu de déformer tous les accents du fichier.
+    """
+    return error.object[error.start:error.end].decode("cp1252", errors="replace"), error.end
+
+
+codecs.register_error("rika_cp1252_fallback", _cp1252_fallback)
+
+
 def decode_text_file(data: bytes) -> str:
-    """Décode le contenu d'un fichier .txt (UTF-16 avec BOM, UTF-8, sinon Windows-1252)."""
+    """Décode le contenu d'un fichier .txt (UTF-16 avec BOM, sinon UTF-8 avec les octets invalides en Windows-1252)."""
     if data.startswith((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
         # « Unicode » du Bloc-notes : deux octets par caractère, dont beaucoup de \x00.
         try:
@@ -30,11 +43,7 @@ def decode_text_file(data: bytes) -> str:
         except UnicodeDecodeError:
             raise TextFileError("Ce fichier ne contient pas du texte lisible.")
     else:
-        try:
-            text = data.decode("utf-8-sig")
-        except UnicodeDecodeError:
-            # Fichiers enregistrés par d'anciens éditeurs Windows (Bloc-notes, etc.).
-            text = data.decode("cp1252", errors="replace")
+        text = data.decode("utf-8-sig", errors="rika_cp1252_fallback")
     if "\x00" in text:
         raise TextFileError("Ce fichier ne contient pas du texte lisible.")
     text = text.replace("\r\n", "\n").strip()

+ 14 - 0
tests/test_attachments_prep.py

@@ -454,6 +454,20 @@ class DecodeTextFileTest(unittest.TestCase):
     def test_bytes_undefined_in_windows_1252(self):
         self.assertEqual(decode_text_file(b"caf\xe9 \x81"), "café �")
 
+    def test_utf8_with_one_invalid_byte(self):
+        # Un octet isolé ne fait plus relire tout le fichier en Windows-1252 (« Café »).
+        data = "Café, élève, à bientôt".encode("utf-8") + b"\xff"
+        self.assertEqual(decode_text_file(data), "Café, élève, à bientôtÿ")
+
+    def test_utf8_and_windows_1252_lines(self):
+        data = "Ligne en UTF-8 : élève\n".encode("utf-8") + "Ligne en ANSI : été".encode("cp1252")
+        self.assertEqual(decode_text_file(data), "Ligne en UTF-8 : élève\nLigne en ANSI : été")
+
+    def test_utf8_cut_in_the_middle_of_a_character(self):
+        # Seul le caractère coupé est abîmé, le reste du texte garde ses accents.
+        data = "Café ".encode("utf-8") + "é".encode("utf-8")[:1] + " à bientôt".encode("utf-8")
+        self.assertEqual(decode_text_file(data), "Café Ã à bientôt")
+
     def test_utf16_with_bom(self):
         for encoding in ("utf-16-le", "utf-16-be"):
             with self.subTest(encoding):