Skip to content

utf-8-variants: the $ anchors swallow a trailing newline #236

Description

@sebollin

The three regexes in ftfy/bad_codecs/utf8_variants.py use $ to mean "the buffer ends here". In Python, $ also matches just before a trailing newline, so a \n at the end of the input gets swallowed into the special-byte match.

Two ways it shows up:

>>> import ftfy.bad_codecs
>>> b"\xc0\n".decode("utf-8-variants")
'\x00'

NULL_EXPR matches \xc0 followed by $ (which sits before the final \n), so _buffer_decode_step takes the Java-null branch and returns ("", 2) — consuming the newline as if it were the \x80. The \n disappears.

>>> b"\xed\xa0\xbd\xed\xb8\n".decode("utf-8-variants")
'😊'

Worse: CESU8_RE matches with its last group hitting $ before the \n, and then _buffer_decode_surrogates computes the codepoint from input[5] & 0x3F, which is 0x0A & 0x3F == 10. So an invalid five-byte sequence decodes to U+1F60A instead of raising, and again the newline is gone.

It's reachable from the public API, and it's the kind of input that shows up for real — a truncated CESU-8 emoji at the end of a line:

>>> ftfy.fix_encoding("Hi guys í ½í¸\n")
'Hi guys 😊'

Both the character and the line break are wrong there.

The fix looks like a one-character change per anchor: \Z instead of $, since \Z only matches at the very end of the string. That keeps the intent for truncated input in the streaming decoder, which is what the $ alternatives are for.

I ran into this porting the codec to R for lupa, where I decode CESU-8 without Python available; my differential battery of 65,268 byte sequences flagged exactly these four:

b"\xc0\n"
b"\x0a\xc0\n"
b"\xc3\xa9\xc0\n"
b"\xed\xa0\xbd\xed\xb8\n"

Everything else decodes identically before and after. I'll send a PR with the change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions