Skip to content

fix(encoding): respect quoting when reading the Content-Type charset - #194

Closed
athul-22 wants to merge 1 commit into
lexmount:mainfrom
athul-22:fix/content-type-charset-quoting
Closed

fix(encoding): respect quoting when reading the Content-Type charset#194
athul-22 wants to merge 1 commit into
lexmount:mainfrom
athul-22:fix/content-type-charset-quoting

Conversation

@athul-22

Copy link
Copy Markdown
Contributor

Fixes #193.

charset_from_content_type split a Content-Type on every ; and = without tracking quoted strings, so a quoted parameter value was not opaque and quoting backslashes were never removed. This decides the document's transport encoding, which outranks the meta prescan and the fallback.

Content-Type before after
text/html; boundary="; charset=gbk" gbk (none)
text/html; name="a\"; charset=gbk"; charset=utf-8 gbk utf-8
text/html; charset="utf\-8" utf\-8 → dropped → windows-1252 utf-8

In the first, the only parameter is boundary, whose value is the string ; charset=gbk — the header declares no charset at all. In the second, \" does not close the quoted string, so name is a"; charset=gbk and the real charset is utf-8. In the third, the quoted value unescapes to utf-8, but the literal utf\-8 is not a valid label, so the charset was dropped and any non-ASCII content rendered as mojibake.

Both the MIME Sniffing Standard's "parse a MIME type" and Blink's HeaderFieldTokenizer — which this workspace already mirrors in moli-header-field and already uses for Content-Disposition in moli-multipart — treat a quoted string as opaque and remove quoting backslashes. The two references agree on all three rows.

No behaviour is tightened

This deliberately keeps the parser's existing tolerances rather than swapping in a strict implementation, because real headers depend on them. I compared eighteen header forms before and after; the three above change and the other fifteen are byte-identical, including:

  • whitespace around =charset = utf-8
  • apostrophe delimiters — charset='utf-8'
  • an unterminated quoted string — charset="utf-8
  • uppercase parameter names, a trailing ;, a value with no media type, and a ,-joined duplicate header

A strict MIME parser would reject the first two, so I did not use one. The change is confined to what quoting means.

Implementation notes

  • Parameters are split only on the ; characters outside a quoted string, and a \ inside one steps over the byte it escapes.
  • Byte indices land only on ASCII bytes. A UTF-8 continuation byte can never equal ", \ or ;, so scanning stays on character boundaries.
  • An unterminated quoted string keeps what was read, matching what trim_matches did before.

Verification

Per AGENTS.md, on aarch64-darwin:

  • cargo fmt --all — clean
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — clean
  • cargo test -p moli-encoding — 56 passed

Four tests were added: the quoted-boundary case, the escaped-quote case, the backslash-unescape case (each asserted through decode_html_document as well as the parser), and one pinning the tolerances listed above.

cargo nextest run --no-fail-fast was still running locally when this was opened; CI covers it here.

`charset_from_content_type` split a `Content-Type` on every `;` and `=`
without tracking quoted strings, so a quoted parameter value was not
opaque and quoting backslashes were never removed. This decides the
document's transport encoding, which outranks every other signal.

Three headers were read wrongly:

    text/html; boundary="; charset=gbk"
        The only parameter is `boundary`, whose value is the string
        `; charset=gbk`. The header declares no charset, but the inner
        `;` was treated as a separator and the document decoded as GBK.

    text/html; name="a\"; charset=gbk"; charset=utf-8
        `\"` does not close the quoted string, so the charset is utf-8.
        The `\"` was read as a terminator and the gbk inside the quoted
        value won instead.

    text/html; charset="utf\-8"
        The quoted value unescapes to `utf-8`. The literal `utf\-8` was
        returned, which is not a valid label, so the charset was dropped
        and the document fell back to windows-1252.

Split parameters only on the `;` characters outside a quoted string, and
unescape a quoted value rather than trimming its delimiters.

Every existing tolerance is deliberately preserved, because the previous
behavior is relied on by real headers: whitespace around `=`, apostrophe
delimiters, an unterminated quoted string, uppercase parameter names, a
trailing `;`, and a value with no media type. Eighteen header forms were
compared before and after; the three above change and the other fifteen
are byte-identical.

Byte indices land only on ASCII bytes, which never occur inside a
multi-byte UTF-8 sequence, so scanning the value stays on character
boundaries.

Verified on aarch64-darwin per AGENTS.md: `cargo fmt --all` and
`cargo clippy --workspace --all-targets --all-features -- -D warnings`
are clean, and `cargo test -p moli-encoding` passes 56 tests.

Closes lexmount#193
@athul-22

Copy link
Copy Markdown
Contributor Author

Superseded by #196, which fixes this alongside the same defect in Content-Disposition and Cache-Control using a shared quoting-aware helper in moli-header-field rather than a local one.

@athul-22 athul-22 closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Content-Type charset parsing ignores quoting, so a quoted parameter can displace the real charset

1 participant