Repository navigation
Conversation
write() keeps the last two bytes for the next call when they contain '='. A chunk ending with a complete escape followed by a lone '=' (e.g. b"=41=") keeps b"1=" and decodes b"=4" as literal text, so the decoded part depends on where the caller's reads fall: b"=41=" then b"42" gives b"=41B" instead of b"AB". The multipart parser uses this decoder for a quoted-printable part. Keep the bytes from the last '=' within the final two, not always the final two, so a complete escape is decoded at once and only an unfinished one waits for the next write. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
1 issue found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="python_multipart/decoders.py">
<violation number="1" location="python_multipart/decoders.py:145">
P2: `rfind` can split adjacent `=` bytes and decode them separately, changing accepted malformed input. For `b"=="`, `write()` followed by `finalize()` now emits `b""` instead of `b"="`; preserve the entire trailing run of `=` bytes (or reject it consistently).</violation>
</file>
Reply to a comment to ask cubic a question or push back. It learns from your replies.
View guided diff | Re-trigger cubic
| # next decoding step. | ||
| if data[-2:].find(b"=") != -1: | ||
| enc, rest = data[:-2], data[-2:] | ||
| idx = data.rfind(b"=", max(0, len(data) - 2)) |
There was a problem hiding this comment.
P2: rfind can split adjacent = bytes and decode them separately, changing accepted malformed input. For b"==", write() followed by finalize() now emits b"" instead of b"="; preserve the entire trailing run of = bytes (or reject it consistently).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At python_multipart/decoders.py, line 145:
<comment>`rfind` can split adjacent `=` bytes and decode them separately, changing accepted malformed input. For `b"=="`, `write()` followed by `finalize()` now emits `b""` instead of `b"="`; preserve the entire trailing run of `=` bytes (or reject it consistently).</comment>
<file context>
@@ -142,8 +142,9 @@ def write(self, data: bytes) -> int:
# next decoding step.
- if data[-2:].find(b"=") != -1:
- enc, rest = data[:-2], data[-2:]
+ idx = data.rfind(b"=", max(0, len(data) - 2))
+ if idx != -1:
+ enc, rest = data[:idx], data[idx:]
</file context>
| idx = data.rfind(b"=", max(0, len(data) - 2)) | |
| idx = data.rfind(b"=", max(0, len(data) - 2)) | |
| while idx > 0 and data[idx - 1 : idx] == b"=": | |
| idx -= 1 |
|
Thanks for the report and the fix! Superseded by #355, which uses |
What happens
QuotedPrintableDecoder.write()keeps the last two bytes for the next call when they contain=. A chunk that ends with a complete escape followed by a lone=(for example=41=) keeps1=and decodes=4as literal text:The multipart parser uses this decoder for a part sent with
Content-Transfer-Encoding: quoted-printable, and the chunks follow the caller's reads, so the decoded upload depends on where the reads fall.The fix
Keep the bytes from the last
=within the final two, not always the final two:data.rfind(b"=", max(0, len(data) - 2)). A complete escape is then decoded at once and only an unfinished one (=,=4) waits for the next write.Tests
tests/test_quoted_printable_chunks.pyadded: every two-way split of four inputs must decode like the whole input. It fails on the current code (4 failures) and passes with the fix. Full suite: 163 passed before, 167 after.🤖 Generated with Claude Code