fix: stop misclassifying UTF-8 files cut at the 1000-byte sample boundary as binary - #81810
Conversation
…dary as binary read_file samples the first 1000 bytes with head -c 1000 and decodes with errors=replace. When byte 1000 lands mid-character in a multi-byte UTF-8 sequence, the truncated tail decodes to a single trailing U+FFFD, and _is_likely_binary() treated any U+FFFD as binary evidence — so perfectly valid text files were refused with 'Binary file - cannot display as text'. Distinguish the truncation artifact from real undecodable bytes: a true binary sample carries U+FFFD in the middle of the sample and/or more than once, whereas a boundary cut produces at most one replacement char squeezed against the tail (UTF-8 sequences are ≤4 bytes, so a cut loses ≤3 bytes). A lone trailing U+FFFD now falls through to the existing non-printable ratio check, which still catches NUL-heavy binary content. Adds regression tests covering the artifact case, mid-sample U+FFFD, multiple U+FFFDs, and the ratio check still firing with a trailing artifact char.
Related: this fixes the #76886 UTF-8 sample-boundary false-binary family, but competes with #80250 and #79408 on trailing-U+FFFD safety semantics. This PR permits exactly one replacement character near the sample tail; the other open PRs use broader trailing-run carve-outs. |
|
Thanks for the triage note. Since the trailing-U+FFFD safety semantics are the point of comparison here, let me spell out why this PR deliberately chose the narrower carve-out, and what the trade-off is. The three approaches side by side
Why "exactly one" is the physically correct bound
The unbounded The safety directionThe original code's comment is explicit: the U+FFFD check exists so a read→edit→write round-trip cannot silently overwrite real bytes with replacement chars. A fix for a false-positive (valid text flagged binary) should not weaken that false-negative guard. This PR keeps the guard intact for every case except the single provable artifact: one U+FFFD in the final 3 chars, which is the maximum damage a 4-byte UTF-8 cut can do. I've added a regression test ( |
|
Closing — superseded by #81961, which fixed this bug class (1000-byte sample cutting a multibyte char → binary false positive) at the byte layer across read_file, patch, and search_files, with regression tests for truncated-CJK/BOM/UTF-16/NUL cases. ~24 PRs raced on this one; earliest diagnosis credit to @webtecnica (#76924), merged implementation from @ayushnangia (#80440). Thanks for jumping on it. |
Problem
read_filesamples the first 1000 bytes withhead -c 1000and decodes witherrors=replace. When byte 1000 lands mid-character in a multi-byte UTF-8 sequence, the truncated tail decodes to a single trailing U+FFFD, and_is_likely_binary()treated any U+FFFD as binary evidence. Result: perfectly valid UTF-8 text files whose 1000th byte lands mid-character are refused withBinary file - cannot display as text.Reproduction
head -c 1000cuts the 汉字 at byte 1000errors=replacedecode yieldsa...a\ufffd_is_likely_binaryreturns True → file refused, despite being 100% valid UTF-8 (filesaysUnicode text, UTF-8 text)Fix
Distinguish the truncation artifact from real undecodable bytes:
A lone trailing U+FFFD now falls through to the existing non-printable ratio check, which still catches NUL-heavy binary content. The original mojibake-corruption guard is preserved: files with genuine undecodable bytes anywhere in the sample are still treated as binary (read-only).
Tests
Added 5 regression tests to
tests/tools/test_file_operations_edge_cases.py:test_truncation_artifact_trailing_replacement_char— the bug case (artifact ≠ binary)test_replacement_char_in_middle_is_binary— mid-sample U+FFFD still binarytest_multiple_replacement_chars_is_binary— two U+FFFDs still binarytest_trailing_replacement_char_with_binary_ratio— ratio check still catches NUL-heavy contentAll
tests/tools/file_operations tests pass (73 passed). Fulltests/tools/run: 5443 passed; remaining failures are pre-existing environment issues (missingparallel-weblazy dep, external modal/fal/video services).Verification
End-to-end repro: a valid UTF-8 file with a boundary-cut character now reads correctly via
read_file(content intact), while a real binary file (NUL + non-UTF-8 bytes) is still rejected as binary.