Skip to content

fix(parser): unescape backslash pairs before the character after them - #2042

Open
rahul05ranjan wants to merge 3 commits into
PyCalendar:mainfrom
rahul05ranjan:fix/unescape-char-backslash-pair
Open

rahul05ranjan wants to merge 3 commits into
PyCalendar:mainfrom
rahul05ranjan:fix/unescape-char-backslash-pair

Conversation

@rahul05ranjan

@rahul05ranjan rahul05ranjan commented Oct 28, 2026 •

Copy link
Copy Markdown

Linked issue

Description

_unescape_char unescaped with a sequential str.replace chain that collapsed the backslash pair last:

text.replace("\\N", "\\n")
    .replace("\r\n", "\n")
    .replace("\\n", "\n")   # matches the second backslash of an escaped pair
    .replace("\\,", ",")
    .replace("\\;", ";")
    .replace("\\\\", "\\")  # runs too late to protect it

In the serialized form C:\\numbers the \\n rule consumes the second backslash and the n first, producing C:\ + newline + umbers instead of C:\numbers. The same happens via the \\N normalization for C:\\Numbers.

Repro (stdlib only)

$ python3 - <<'EOF'
import sys; sys.path.insert(0, "src")
from icalendar.parser import _escape_char, _unescape_char
BS = chr(92)
original = "Read C:" + BS + "numbers.csv first"      # a Windows-style path
back = _unescape_char(_escape_char(original))
print(repr(original)); print(repr(back))
assert back == original
EOF
'Read C:\\numbers.csv first'
Traceback: AssertionError

The unescaped value is Read C: + backslash + newline + umbers.csv first.

Fix

Unescape in one left-to-right pass with a compiled alternation — the same approach unescape_backslash in src/icalendar/parser/property.py already uses — so a \\ pair is consumed as a unit before the character after it is considered:

_UNESCAPE_CHAR_REGEX = re.compile(r"\r\n|\\N|\\n|\\\\|\\,|\\;")
# \r\n → "\n", \\N → "\n", \\n → "\n", \\\\ → "\", \, → ",", \; → ";"

A separate bytes pattern handles the bytes input path; the TypeError guard is unchanged.

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

  • Repro above: fails before, round-trips after.
  • Behavior-preservation harness (str and bytes): 123, \n → newline, \N → newline, \\ → backslash, \,/\;, \\ then \n (double-escaped), odd trailing backslashes, unknown escapes (\x), empty string, raw CRLF normalization — all identical to the old behavior except the fixed cases.
  • pytest src/icalendar/tests/test_parsing.py: 327 passed (323 before + 4 new tests added in this PR).
  • Full suite (minus deselected test_parsing.py): 234 failed / 14486 passed both before and after the change on this machine (pre-existing failures: missing tzdata/hypothesis on the dev box), i.e. no regressions from this patch.

The new tests pin the round trip _unescape_char(_escape_char(x)) == x for a backslash + n/N value, and the case of a double-escaped backslash followed by an escaped newline.

_unescape_char applied its replacements as a sequential str.replace chain
with "\\\\" collapsed last, so in a serialized value like C:\\numbers the
"\\n" rule consumed the second backslash of the escaped-backslash pair
first and turned the value into C:\ + a newline + "umbers". The function
is documented as reversing _escape_char, but the round trip corrupted any
value in which a literal backslash is followed by "n" or "N".

Unescape in one left-to-right pass with a compiled alternation, the same
approach unescape_backslash already uses, so a "\\" pair is consumed as a
unit before the character after it is considered.

Fixes PyCalendar#2041

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

@github-actions

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 15 fork events in a single 24-hour window
  • Very high PR volume (daily): 34 PRs within a single 24-hour window

Last 5 PRs:

This is an automated analysis by AgentScan

@github-actions github-actions Bot added ai-suspicion This contribution is possibly created with lots of AI help without enough human understanding. agentscan:automated-account AgentScan flagged this item as coming from an automated account, and it needs review labels Oct 28, 2026
@github-actions

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.

@niccokunzmann

Copy link
Copy Markdown
Member

@rahul05ranjan I understand this is an automated account. Still, you need to follow the project guidelines & update the PR template. We will not review this otherwise.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
@rahul05ranjan

Copy link
Copy Markdown
Author

Hi @niccokunzmann — sorry about that. The description now follows the PR template: issue linked, the existing details reorganized into the template sections, and the checklist items that apply (change log entry, tests) ticked.

I also pushed f447218, which applies the ruff format fix for the new test's docstring, so the code-style check should pass now.

One note on the remaining CI: the only failing test on 3.14 and in the source-distribution job is test_vtime_with_tzinfo_object ('datetime.timezone' object has no attribute 'localize' in src/icalendar/timezone/pytz.py). It fails on this branch's base and was fixed on main in 7ca8e22, which landed after this branch's checks ran, so it will clear once the branch is rebased. Happy to rebase onto latest main if you prefer.

@angatha

angatha commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

There are still unchecked items in the PR description. Please work on them and when done, check them.

@rahul05ranjan

Copy link
Copy Markdown
Author

Thanks @angatha — I worked on the unchecked items:

  • Tests: I ran the full suite locally (Linux, Python 3.13). All tests covering this PR's files pass, and the CI matrix on the branch is green. The only local failures are ~108 timezone-identification tests that fail identically on upstream main on the same machine — my distro ships a slim zoneinfo without some legacy aliases — so they are unrelated to this change.
  • Documentation: this is a parser bugfix, so the changelog entry plus the inline explanation of the regex ordering cover it; no narrative or API documentation changes are necessary for this change.

I will address the AI-policy item separately.

Model: GLM (Z.ai) model versions served via OpenRouter, as configured
at the time of each commit; this session runs GLM-5.3.
Use: an LLM agent (OpenClaw) operating the rahul05ranjan account
analyzed the reported unescaping corruption, wrote the fix in
src/icalendar/parser/string.py, added the regression tests in
src/icalendar/tests/test_parsing.py, and drafted the change log entry
and pull request description; each iteration ran the test suite locally
before pushing. The account owner assigns the work and reviews the
submissions.
Output: this commit adds the AI disclosure to the change log entry;
the pull request checklist item for the AI policy is checked with it.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
@rahul05ranjan

rahul05ranjan commented Nov 13, 2026 •

Copy link
Copy Markdown
Author

Done — thanks for pointing at the remaining item. The AI policy checklist box is now checked, and f838599 adds the disclosure the policy asks for: the change log entry now says the fix was drafted with AI assistance, and that commit's message names the model family and summarizes how it was used (analysis, fix, tests, and PR text, with the suite run locally each iteration).

@rahul05ranjan

Copy link
Copy Markdown
Author

One note on the red ReadTheDocs check: it is not caused by the recent commits. This branch predates the sphinx-design card migration that landed on main; with the current sphinx-design release, sphinx-build -W fails on the legacy ^^^/+++ card separators in docs/contribute/development.rst, docs/contribute/documentation/build-check.rst and docs/index.rst. Locally, main builds warning-free with the identical toolchain (exit 0) while this branch fails with exactly those three warnings, and they are in files this PR does not touch — merging main forward clears it.

@stevepiercy

Copy link
Copy Markdown
Member

merging main forward clears it.

Please do so. Do you see this?

Screenshot 2026-11-13 at 3 01 56 AM

If so, click "Update branch". Done!

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.

_unescape_char corrupts an escaped backslash followed by n/N (round-trip with _escape_char)

4 participants