Read ``path`` as text, picking a decoder from any leading BOM. Falls back to utf-8 with ``errors="replace"`` for the common case (no BOM). When a BOM is present we use the codec it implies (with ``errors="replace"`` so a malformed trailing byte doesn't bring down the whole read).
(path: str)
| 307 | |
| 308 | |
| 309 | def _read_text_with_encoding(path: str) -> str | None: |
| 310 | """Read ``path`` as text, picking a decoder from any leading BOM. |
| 311 | |
| 312 | Falls back to utf-8 with ``errors="replace"`` for the common case |
| 313 | (no BOM). When a BOM is present we use the codec it implies (with |
| 314 | ``errors="replace"`` so a malformed trailing byte doesn't bring |
| 315 | down the whole read). |
| 316 | |
| 317 | Post-decode garbage check: if the decoded text is mostly U+FFFD |
| 318 | replacement characters, returns ``None`` so the caller drops the |
| 319 | attachment instead of inlining mojibake. This closes the adversarial |
| 320 | case where a binary blob starts with `\\xff\\xfe` (fake UTF-16 BOM) |
| 321 | and an extension we haven't enumerated — without the check, the |
| 322 | BOM short-circuit would route the blob to the text branch and the |
| 323 | `errors="replace"` decode would land replacement-char mojibake in |
| 324 | the prompt (same failure family as Bug A even though no NULs leak). |
| 325 | |
| 326 | Returns ``None`` on OSError or on garbled-decode so the caller |
| 327 | matches the existing "drop this attachment silently" behaviour for |
| 328 | unreadable files. Callers should still emit a binary-style reminder |
| 329 | via the @-mention `binary` attachment kind when this returns None |
| 330 | for a non-OSError reason — see ``expand_at_mentions``. |
| 331 | """ |
| 332 | try: |
| 333 | with open(path, "rb") as fh: |
| 334 | head = fh.read(4) |
| 335 | except OSError: |
| 336 | return None |
| 337 | encoding = _detect_bom_encoding(head) or "utf-8" |
| 338 | try: |
| 339 | with open(path, "r", encoding=encoding, errors="replace") as fh: |
| 340 | data = fh.read() |
| 341 | except OSError: |
| 342 | return None |
| 343 | except (LookupError, UnicodeError): |
| 344 | # Pathological codec name from a future BOM check, or a UTF-32 |
| 345 | # decode failure with errors=replace (rare but theoretically |
| 346 | # possible). Fall back to a raw utf-8 replacement read so the |
| 347 | # path still produces *some* text rather than a silent drop. |
| 348 | try: |
| 349 | with open(path, "r", encoding="utf-8", errors="replace") as fh: |
| 350 | data = fh.read() |
| 351 | except OSError: |
| 352 | return None |
| 353 | if _decoded_text_looks_garbled(data): |
| 354 | return None |
| 355 | return data |
| 356 | |
| 357 | |
| 358 | def _binary_hint_for_ext(ext: str) -> str: |
no test coverage detected