Skip to content

Unfold folded content lines before decoding bytes - #2079

Open
rahul05ranjan wants to merge 3 commits into
PyCalendar:mainfrom
rahul05ranjan:fix/unfold-before-decoding
Open

rahul05ranjan wants to merge 3 commits into
PyCalendar:mainfrom
rahul05ranjan:fix/unfold-before-decoding

Conversation

@rahul05ranjan

Copy link
Copy Markdown

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 the errors="replace" fallback before they can be rejoined — the value silently comes back as U+FFFD soup:

summary = "A" * 70 + "🎉" + "B" * 20
# fold the SUMMARY line inside the 4-byte emoji, RFC 5545-style
event = Calendar.from_ical(ics).walk("VEVENT")[0]
str(event["SUMMARY"])   # 'AAAA…AA\N{REPLACEMENT CHARACTER}\N{REPLACEMENT CHARACTER}\N{REPLACEMENT CHARACTER}\N{REPLACEMENT CHARACTER}BBBB…'

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_BYTES of the existing UFOLD regex) in Contentline.from_ical() and Contentlines.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

  • I added a change log entry, following the instructions in all subsections under Change log requirements.
  • I followed icalendar's Artificial intelligence policy and disclosed my Responsible AI use in my commit messages, if applicable.
  • I added or updated tests, if applicable.
  • I ran and ensured all tests pass locally by following Run tests.
  • I added or edited documentation as necessary, both as docstrings to be rendered in the API documentation and narrative documentation, following the Style guide.

Additional information

Verification: the new test_fold_inside_multibyte_utf8_sequence_issue_2077 in src/icalendar/tests/test_encoding.py fails 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 in test_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.

@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for the pull request, @rahul05ranjan! A maintainer will review it shortly.

@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Automation signals

Activity patterns show signs of automation.

View full analysis →

Report this account →

Evidence
  • Fork spike detected: Burst of 10 fork events in a single 24-hour window
  • Issue comment and PR within minutes: Commented on an issue and opened a PR to the same repo within 5 minutes, in 2 repositories (the fastest took 101s)

Last 2 PRs:

This is an automated analysis by AgentScan

@github-actions github-actions Bot added the ai-suspicion This contribution is possibly created with lots of AI help without enough human understanding. label Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

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.

@github-actions github-actions Bot added the agentscan:automated-account AgentScan flagged this item as coming from an automated account, and it needs review label Nov 8, 2026
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>
@rahul05ranjan
rahul05ranjan force-pushed the fix/unfold-before-decoding branch from 8e3c743 to a8eb8ab Compare November 8, 2026 23:28
Comment on lines +296 to 302
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@rahul05ranjan

Copy link
Copy Markdown
Author

Good catch @angatha — you're right, it was redundant for bytes input.

I verified it before changing anything: after UFOLD_BYTES has unfolded the raw bytes, the second UFOLD pass on the decoded string can never match (folds cannot reappear during decoding — confirmed with a 20,000-case fuzz harness over folded multi-byte content: zero differences). So the double unfold was a harmless no-op in the bytes path, but still a wasted pass, and the string unfold remains necessary only when the input is a str.

Restructured in a8eb8ab: bytes input now unfolds once on bytes and skips the string pass entirely; str input keeps the single UFOLD unfold. The issue #2077 regression test passes with the restructure, and the four remaining test_encoding.py failures reproduce identically on the branch without this change (environment-related, unrelated to this PR).

…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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentscan:automated-account AgentScan flagged this item as coming from an automated account, and it needs review ai-suspicion This contribution is possibly created with lots of AI help without enough human understanding.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

from_ical(bytes) decodes before unfolding, so folds inside a UTF-8 multi-byte sequence silently corrupt the value (U+FFFD)

2 participants