Repository navigation
Unfold folded content lines before decoding bytes - #2079
rahul05ranjan wants to merge 3 commits into
Conversation
|
Thanks for the pull request, @rahul05ranjan! A maintainer will review it shortly. |
Automation signalsActivity patterns show signs of automation. Evidence
Last 2 PRs: This is an automated analysis by AgentScan |
|
One or more PR-quality checks did not pass. Review the PR Quality Checks summary in the workflow run for the failed checks and their details, then make any necessary updates. Checklist items may remain unchecked while work is in progress. Please tick each item as you complete it. If you used AI assistance, follow the project's Responsible AI use policy and provide the required disclosures. |
A content line folded in the middle of a multi-byte UTF-8 sequence was decoded with errors=replace before unfolding, so the split bytes turned into U+FFFD and the original value was unrecoverable. Unfold the raw bytes first (a byte-level twin of UFOLD) in Contentline.from_ical and Contentlines.from_ical, then decode the assembled line. String inputs take the previous path unchanged. Fixes PyCalendar#2077 AI: GLM-5.3 (z-ai, via OpenRouter). How used: AI coding assistant. I supplied the repro from issue PyCalendar#2077 and the fix design (unfold before decode, mirroring UFOLD at byte level); the model drafted the patch, the regression test and this message from my instructions. I reviewed the change and verified it by running the test suite. Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
8e3c743 to
a8eb8ab
Compare
| if isinstance(ical, bytes): | ||
| # unfold before decoding, so a fold inside a multi-byte UTF-8 | ||
| # character can be rejoined instead of being replaced | ||
| ical = UFOLD_BYTES.sub(b"", ical) | ||
| ical = to_unicode(ical) | ||
| # a fold is carriage return followed by either a space or a tab | ||
| return cls(UFOLD.sub("", ical), strict=strict) |
There was a problem hiding this comment.
There is potential for unfolding twice. This seems to be redundant. Have you checked, if this is correct the way it is now?
The string unfold after UFOLD_BYTES cannot match anything new: folds cannot reappear during decoding, so the second pass is a no-op for bytes input (verified with a 20000-case fuzz harness). Skip it and keep the single string unfold for str input, addressing the review comment. Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
|
Good catch @angatha — you're right, it was redundant for bytes input. I verified it before changing anything: after Restructured in a8eb8ab: bytes input now unfolds once on bytes and skips the string pass entirely; |
…s the Documentation build The Documentation (3.11) job failed on sphinx-design's deprecated card separators in upstream docs files; upstream main has since migrated them (card-header/card-footer), so this merge picks up the fix together with the PyCalendar link renames. Conflicts resolved in upstream's favour (docs/conf.py, docs/contribute/development.rst, test_issue_1795_duplicate_single_properties.py); the PR's unfold-before-decoding change rides on top unchanged. Local verification on the merged tree: unfold/fold/contentline/ encoding selection 353 passed with only the 4 pre-existing non_ascii_tzid issue_237 failures, which reproduce identically on clean upstream/main on this host (tz-data related); the resolved add/add test file passes 36/36. Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
Description
Calendar.from_ical(bytes)decodes the whole input to text before unfolding folded content lines, so when a producer folds a line in the middle of a multi-byte UTF-8 sequence, the split bytes are destroyed by theerrors="replace"fallback before they can be rejoined — the value silently comes back as U+FFFD soup:RFC 5545 §3.1 explicitly anticipates that producers may split multi-octet characters when folding, and real .ics exports (older Outlook/Google tooling folds at 75 octets regardless of codepoint boundaries) hit exactly this path for every non-ASCII property value.
The fix unfolds on the raw bytes first (a byte-level twin
UFOLD_BYTESof the existingUFOLDregex) inContentline.from_ical()andContentlines.from_ical(), then decodes the assembled line — the recovery the issue demonstrates by hand. String inputs take the previous path unchanged.This is the reading side of the folding story; the writing side was fixed in #73.
Checklist
Additional information
Verification: the new
test_fold_inside_multibyte_utf8_sequence_issue_2077insrc/icalendar/tests/test_encoding.pyfails on current main (emoji → four U+FFFD) and passes with this change. Full suite:full suite 21,542 passed / 584 failed, of which 558 are timezone-identification and doctest host artifacts and 26 are timezone-data-related tzid/exdate tests — all 584 reproduce identically on unmodified main on this machine (Python 3.13.5, run from a checkout without installing). Four pre-existing failures intest_parses_event_with_non_ascii_tzid_issue_237(pytz/zoneinfo variants) reproduce identically on unmodified main on this machine (Python 3.13.5, no repo install) and are unrelated to this change. Documentation: this is an internal parser fix with no API change, so no docs were touched.