Repository navigation
detect_text_encoding does not trim trailing NULs, so NUL-padded text is reported as binary #3506
Description
Summary
Before it classifies an encoding, libmagic strips the trailing NUL bytes from the buffer. PolyFile's
detect_text_encoding does not, so a buffer that is text followed by NUL padding is classified as
binary and falls through to application/octet-stream.
| input | file 5.48 |
PolyFile |
|---|---|---|
de ca ff ed 00 00 00 00 |
text/plain; charset=binary |
application/octet-stream |
de ca ff ed |
text/plain; charset=iso-8859-1 |
text/plain |
The two inputs differ only by four trailing NULs, and file classifies both as text.
Reproducer
$ printf '\xde\xca\xff\xed\x00\x00\x00\x00' > card.bin
$ ./file/src/file -m tests/magic.mgc -i --keep-going card.bin
card.bin: text/plain; charset=binary
$ ./file/src/file -m tests/magic.mgc --keep-going card.bin
card.bin: ISO-8859 text, with no line terminatorsfrom polyfile.magic import MAGIC_DEFS, MagicMatcher, MatchContext, detect_text_encoding
matcher = MagicMatcher.parse(*MAGIC_DEFS)
print(detect_text_encoding(open("card.bin", "rb").read()))
print({mt for m in matcher.match(MatchContext.load("card.bin", only_match_mime=True))
for mt in m.mimetypes})None
{'application/octet-stream'}
Expected iso-8859-1 and {'text/plain'}. Dropping the four trailing NULs from the input already
produces both, so the classifier is correct and only the missing trim step separates the two
results.
Cause
file_ascmagic copies the buffer, trims its trailing NULs, and only then calls file_encoding
(file/src/ascmagic.c:82-89):
bb = *b;
bb.flen = trim_nuls(CAST(const unsigned char *, b->fbuf), b->flen);
/*
* Avoid trimming at an odd byte if the original buffer was evenly
* sized; this avoids losing the last character on UTF-16 LE text
*/
if ((bb.flen & 1) && !(b->flen & 1))
bb.flen++;trim_nuls (file/src/ascmagic.c:62-67) leaves at least one byte:
while (nbytes > 1 && buf[nbytes - 1] == '\0')
nbytes--;detect_text_encoding (polyfile/magic.py:3031) has no equivalent step. It classifies the raw
buffer, _only_contains(data, _TEXT_BYTES) fails on the NULs, and it returns None.
MagicMatcher.match then takes the OctetStreamTest branch (polyfile/magic.py:3453-3456).
A faithful port needs both halves: the trim itself and the odd-byte adjustment, which adds one byte
back when the trimmed length is odd and the original length was even, so that the last character of
UTF-16LE text survives.
Why file prints text/plain; charset=binary
The pairing looks self-contradictory but is intended, and it is worth recording because it makes the
expected value confusing to read. file_buffer classifies the buffer twice:
funcs.c:351initializescode_mime = "binary", andfuncs.c:369-370callsfile_encodingon
the untrimmed buffer. The NULs make that call fail, socode_mimekeeps its default and the
; charset=suffix atfuncs.c:517-521reportsbinary.funcs.c:498callsfile_ascmagic, which classifies the trimmed buffer and supplies the
text/plaintype.
So the charset comes from the untrimmed buffer and the type from the trimmed one. Only the type is
at issue here; PolyFile emits no charset at all, which is #3488.
Impact
pocs-master/mocks/card.bin in the corkami corpus, currently skipped by KNOWN_BAD_FILES in
tests/test_corkami.py. Its skip comment claimed PolyFile was the more accurate of the two, which
no longer holds: after #3468 pinned the character class table to libmagic's text_chars, five of
the seven sibling mocks in that list began agreeing with file, and this one is left behind by the
missing trim rather than by a deliberate difference.
More generally this affects any NUL-padded text: fixed-width records, padded string tables, and
files rounded up to a block boundary.
The fix is small and self-contained in detect_text_encoding. It is independent of #3488, which
changes when the plain-text result is emitted rather than whether these bytes count as text.
Related to #3507, the EBCDIC gap in the same function.
Activity
ESultanik commentedon Sep 11, 2026
Anchor correction
Verified against
masterat906d916.detect_text_encodingis now atpolyfile/magic.py:4034-4057, not:3031. The body is tenlines and is a single dispatch chain: a
len(data) < 2guard at:4048-4049, thenascii(
:4050-4051),utf-8(:4052-4053),_looks_like_ucs(:4054-4056), and_eight_bit_encoding(:4057).The trim belongs at the top of the body, before the
len(data) < 2guard — libmagic trims infile_ascmagicbefore it callsfile_encoding, and a NUL-padded one-byte buffer trims to nothing,so ordering matters.
The
OctetStreamTestfall-through isMagicMatcher.matchat:4818-4822, not:3453-3456.TextEncodingDescription.detect(:4166-4183) is what reachesdetect_text_encoding, at:4180.Helper functions in the same contiguous region, for reference:
_text_char_classes:3958,_only_contains:3995,_looks_like_utf8:3999,_looks_like_ucs:4009,_eight_bit_encoding:4025.LIBMAGIC_ENCODING_NAMESis at:4060-4069.Sequencing
#3507 rewrites the same ten-line function body. Do not run them concurrently. This one lands
first, and #3507 branches from master after it merges.
Verification
KNOWN_BAD_FILESintests/test_corkami.pyis at:19-34; this issue's entry is:20-24(
card.bin, MD579f509d30245f6c7574a1ace1696be1a). Remove it in this change. Unliketest_magic.py'sKNOWN_FAILURES, that list is a plainskipTestwith no unexpectedly-passingguard, so removing the entry and showing the Corkami suite green is the only thing that proves the
fix.
Existing tests that assert on this function:
MagicTest.test_text_character_classes(
tests/test_magic.py:333-341),test_iso_8859_text_is_not_binary(:343-365),test_text_char_class_boundaries(:320-331), andUCSTextBufferTest.test_an_eight_bit_input_is_not_decoded(:2000-2016).