Decode BOM-marked Unicode web content

This commit is contained in:
oobabooga 2026-07-15 11:33:49 -03:00
commit 4eb12832c4
2 changed files with 49 additions and 3 deletions

View file

@ -3602,6 +3602,15 @@ _BINARY_MAGIC = (
b"\x28\xb5\x2f\xfd", # zstd
)
# Check UTF-32 first because its little-endian BOM starts with the UTF-16 BOM.
_UNICODE_BOM_CODECS = (
(codecs.BOM_UTF32_LE, "utf-32"),
(codecs.BOM_UTF32_BE, "utf-32"),
(codecs.BOM_UTF16_LE, "utf-16"),
(codecs.BOM_UTF16_BE, "utf-16"),
(codecs.BOM_UTF8, "utf-8-sig"),
)
# A cp1252 retry needs 75% ASCII structure so it cannot rescue high-byte binary.
_MIN_SINGLE_BYTE_ASCII_RATIO = 3 / 4
_ASCII_TEXT_BYTES = frozenset((*range(0x20, 0x7F), 0x09, 0x0A, 0x0D, 0x1B))
@ -3616,7 +3625,11 @@ def _looks_binary(text: str) -> bool:
def _has_binary_magic(data: bytes) -> bool:
"""Whether a common binary signature follows optional BOM or whitespace."""
head = data[:1024].lstrip().removeprefix(b"\xef\xbb\xbf").lstrip()
head = data[:1024].lstrip()
for bom, _codec in _UNICODE_BOM_CODECS:
if head.startswith(bom):
head = head.removeprefix(bom).lstrip()
break
return head.startswith(_BINARY_MAGIC)
@ -3858,7 +3871,11 @@ def _fetch_page_text(
declared = resp.headers.get_content_charset()
declared_codec = codecs.lookup(declared).name if declared else None
raw_html = raw_bytes.decode(declared or "utf-8", errors = "replace")
bom_codec = next(
(codec for bom, codec in _UNICODE_BOM_CODECS if raw_bytes.startswith(bom)),
None,
)
raw_html = raw_bytes.decode(declared or bom_codec or "utf-8", errors = "replace")
# Catch mislabeled or unlabeled binary, including valid UTF-8 controls.
if _looks_binary(raw_html):

View file

@ -5,6 +5,7 @@
from __future__ import annotations
import codecs
import sys
from email.message import Message
from pathlib import Path
@ -122,6 +123,23 @@ def test_excel_labeled_csv_kept_after_sniffing(monkeypatch):
assert "binary content" not in out
@pytest.mark.parametrize(
"bom,encoding",
[
(codecs.BOM_UTF16_LE, "utf-16-le"),
(codecs.BOM_UTF16_BE, "utf-16-be"),
(codecs.BOM_UTF32_LE, "utf-32-le"),
(codecs.BOM_UTF32_BE, "utf-32-be"),
],
)
@pytest.mark.parametrize("content_type", ["text/plain", "application/vnd.ms-excel"])
def test_bom_unicode_text_without_charset_kept(monkeypatch, bom, encoding, content_type):
body = bom + ("name,value\nreadable,42\n" * 100).encode(encoding)
out = _fetch_with(monkeypatch, body, content_type)
assert "readable" in out
assert "binary content" not in out
def test_valid_utf8_binary_caught_by_control_chars(monkeypatch):
# These controls are valid UTF-8 and therefore produce no replacement chars.
body = bytes([0, 1, 2, 3, 4, 5, 6, 7]) * 400
@ -146,7 +164,18 @@ def test_text_labeled_binary_caught_by_magic(monkeypatch, magic):
assert "binary content" in out
@pytest.mark.parametrize("prefix", [b"\xef\xbb\xbf", b" \r\n", b"\t\xef\xbb\xbf "])
@pytest.mark.parametrize(
"prefix",
[
codecs.BOM_UTF8,
codecs.BOM_UTF16_LE,
codecs.BOM_UTF16_BE,
codecs.BOM_UTF32_LE,
codecs.BOM_UTF32_BE,
b" \r\n",
b"\t\xef\xbb\xbf ",
],
)
def test_pdf_magic_after_harmless_prefix(monkeypatch, prefix):
body = prefix + b"%PDF-1.7\n" + b"1 0 obj<</Type/Catalog>>endobj\n" * 100
out = _fetch_with(monkeypatch, body, "text/plain")