1
0

text_files.py 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. """Lecture des fichiers texte (.txt, .md, .csv, code…) joints aux messages."""
  2. from __future__ import annotations
  3. import codecs
  4. from .errors import UserFacingError
  5. # Fichiers lus comme du texte brut : notes, données, sous-titres, configuration et code.
  6. TEXT_EXTENSIONS = (
  7. ".txt", ".md", ".log", ".csv", ".tsv", ".json", ".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg",
  8. ".srt", ".vtt", ".html", ".htm", ".css", ".tex", ".sql",
  9. ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".kt", ".c", ".h", ".cpp", ".hpp", ".cs",
  10. ".go", ".rs", ".rb", ".php", ".lua", ".sh", ".bat", ".ps1",
  11. )
  12. # Taille maximale d'un fichier .txt (environ 25 000 tokens, pour limiter le coût d'un message).
  13. TEXT_FILE_MAX_BYTES = 100_000
  14. class TextFileError(UserFacingError):
  15. """Fichier texte illisible ou refusé, avec un message affichable sur Discord."""
  16. def _cp1252_fallback(error: UnicodeDecodeError) -> tuple[str, int]:
  17. """Relit en Windows-1252 les seuls octets qui ne sont pas de l'UTF-8 valide.
  18. Un octet isolé dans un fichier UTF-8 n'abîme ainsi que lui-même au lieu de
  19. déformer tous les accents du fichier.
  20. """
  21. return error.object[error.start:error.end].decode("cp1252", errors="replace"), error.end
  22. codecs.register_error("rika_cp1252_fallback", _cp1252_fallback)
  23. def _decode_invalid_utf8(data: bytes) -> str:
  24. """Décode un fichier qui n'est pas de l'UTF-8 valide : Windows-1252 en entier, ou mélangé.
  25. Dans un vrai fichier Windows-1252 (Bloc-notes « ANSI »), chaque accent est un octet
  26. invalide en UTF-8, mais deux ou trois octets voisins forment parfois un caractère
  27. UTF-8 valide par hasard (« é », espace insécable et « » » donnent « 頻 »). Le fichier
  28. n'est donc lu en mélangé (UTF-8, octets invalides en Windows-1252) que s'il a plus
  29. de caractères UTF-8 valides que d'octets invalides, ou s'il commence par un BOM UTF-8.
  30. """
  31. if not data.startswith(codecs.BOM_UTF8):
  32. lenient = data.decode("utf-8", errors="replace") # un « � » par suite d'octets invalides
  33. # Sans les « � » déjà dans le fichier (EF BF BD est toujours décodé d'un bloc).
  34. invalid = lenient.count("�") - data.count("�".encode("utf-8"))
  35. valid = sum(1 for char in lenient if char > "\x7f") - invalid
  36. if invalid >= valid:
  37. return data.decode("cp1252", errors="replace")
  38. return data.decode("utf-8-sig", errors="rika_cp1252_fallback")
  39. def decode_text_file(data: bytes) -> str:
  40. """Décode le contenu d'un fichier .txt (UTF-16 avec BOM, UTF-8, sinon Windows-1252 ou mélangé)."""
  41. if data.startswith((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
  42. # « Unicode » du Bloc-notes : deux octets par caractère, dont beaucoup de \x00.
  43. try:
  44. text = data.decode("utf-16")
  45. except UnicodeDecodeError:
  46. raise TextFileError("Ce fichier ne contient pas du texte lisible.")
  47. else:
  48. try:
  49. text = data.decode("utf-8-sig")
  50. except UnicodeDecodeError:
  51. text = _decode_invalid_utf8(data)
  52. if "\x00" in text:
  53. raise TextFileError("Ce fichier ne contient pas du texte lisible.")
  54. text = text.replace("\r\n", "\n").strip()
  55. if not text:
  56. raise TextFileError("Ce fichier texte est vide.")
  57. return text