diff options
| author | Craig Jennings <c@cjennings.net> | 2025-11-17 02:54:02 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-11-17 02:54:02 -0600 |
| commit | 0afa3fba94157d5e18f9a086e0b67b7cfd2aedf0 (patch) | |
| tree | 08178068136b5bdf2847921cf14b65cce7f09f5a | |
| parent | cee86049043be47d82a274a1202641fc0a5c68b4 (diff) | |
fix(calendar-sync): Remove carriage return characters from synced events
Problem:
Google Calendar .ics files use CRLF line endings (RFC 5545 spec), which
resulted in 11,685 ^M (CR) characters appearing in gcal.org, particularly
at the end of org header lines.
Solution:
- Created calendar-sync--normalize-line-endings function to strip all \r
characters from .ics content
- Integrated into calendar-sync--fetch-ics immediately after curl download
- Ensures clean Unix LF-only line endings throughout parsing pipeline
Testing:
- Added comprehensive test suite: test-calendar-sync--normalize-line-endings.el
- 16 tests covering Normal, Boundary, and Error cases
- All 56 existing calendar-sync tests still pass (no regressions)
- Verified: gcal.org now has 0 CR characters (was 11,685)
Files modified:
- modules/calendar-sync.el: Added normalize function, updated fetch function
- tests/test-calendar-sync--normalize-line-endings.el: New comprehensive test suite
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
| -rw-r--r-- | modules/calendar-sync.el | 18 | ||||
| -rw-r--r-- | tests/test-calendar-sync--normalize-line-endings.el | 186 |
2 files changed, 202 insertions, 2 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index 8450b282..feb7188d 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -144,6 +144,20 @@ Example: -21600 → 'UTC-6' or 'UTC-6:00'." (error (message "calendar-sync: Error loading state: %s" (error-message-string err)))))) +;;; Line Ending Normalization + +(defun calendar-sync--normalize-line-endings (content) + "Normalize line endings in CONTENT to Unix format (LF only). +Removes all carriage return characters (\\r) from CONTENT. +The iCalendar format (RFC 5545) uses CRLF line endings, but Emacs +and org-mode expect LF only. This function ensures consistent line +endings throughout the parsing pipeline. + +Returns CONTENT with all \\r characters removed." + (if (not (stringp content)) + content + (replace-regexp-in-string "\r" "" content))) + ;;; .ics Parsing (defun calendar-sync--split-events (ics-content) @@ -293,7 +307,7 @@ Events are sorted chronologically by start time." (defun calendar-sync--fetch-ics (url) "Fetch .ics file from URL using curl. -Returns .ics content as string, or nil on error. +Returns .ics content as string with normalized Unix line endings (LF only), or nil on error. Uses curl instead of url-retrieve-synchronously to avoid daemon mode hanging." (condition-case err (with-temp-buffer @@ -303,7 +317,7 @@ Uses curl instead of url-retrieve-synchronously to avoid daemon mode hanging." "-m" "10" ; Max 10 seconds url))) (if (= exit-code 0) - (buffer-string) + (calendar-sync--normalize-line-endings (buffer-string)) (setq calendar-sync--last-error (format "curl exited with code %d" exit-code)) (message "calendar-sync: Fetch error: %s" calendar-sync--last-error) nil))) diff --git a/tests/test-calendar-sync--normalize-line-endings.el b/tests/test-calendar-sync--normalize-line-endings.el new file mode 100644 index 00000000..7f0830cc --- /dev/null +++ b/tests/test-calendar-sync--normalize-line-endings.el @@ -0,0 +1,186 @@ +;;; test-calendar-sync--normalize-line-endings.el --- Tests for calendar-sync--normalize-line-endings -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for calendar-sync--normalize-line-endings function. +;; Tests conversion of various line ending formats to Unix LF-only format. +;; Covers Normal, Boundary, and Error cases. +;; +;; The iCalendar format (RFC 5545) uses CRLF line endings (\r\n), +;; but Emacs and org-mode expect LF only (\n). This function ensures +;; consistent line endings throughout the parsing pipeline. + +;;; Code: + +(require 'ert) +(require 'calendar-sync) + +;;; Normal Cases + +(ert-deftest test-calendar-sync--normalize-line-endings-normal-crlf-to-lf () + "Test that CRLF line endings are converted to LF only. + +Input: String with \\r\\n (Windows/DOS line endings) +Expected: String with \\n only (Unix line endings)" + (let* ((input "line1\r\nline2\r\nline3\r\n") + (expected "line1\nline2\nline3\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-normal-lf-unchanged () + "Test that LF-only content is returned unchanged. + +Input: String with \\n only (already Unix format) +Expected: Same string (no modification needed)" + (let* ((input "line1\nline2\nline3\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= input result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-normal-mixed-endings () + "Test that mixed line endings are normalized to LF only. + +Input: String with both \\r\\n (CRLF) and \\n (LF) +Expected: String with \\n only everywhere" + (let* ((input "line1\r\nline2\nline3\r\nline4\n") + (expected "line1\nline2\nline3\nline4\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-normal-ics-vevent-block () + "Test normalization of realistic iCalendar VEVENT block with CRLF. + +Input: VEVENT block with CRLF line endings (as per RFC 5545) +Expected: Same structure with LF only" + (let* ((input "BEGIN:VEVENT\r\nSUMMARY:Test Event\r\nDTSTART:20251116T140000Z\r\nEND:VEVENT\r\n") + (expected "BEGIN:VEVENT\nSUMMARY:Test Event\nDTSTART:20251116T140000Z\nEND:VEVENT\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +;;; Boundary Cases + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-empty-string () + "Test that empty string is handled correctly. + +Input: Empty string +Expected: Empty string (no crash)" + (let ((result (calendar-sync--normalize-line-endings ""))) + (should (string= "" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-no-line-endings () + "Test that string with no line endings is unchanged. + +Input: Plain text with no \\r or \\n +Expected: Same string unchanged" + (let* ((input "no line endings here") + (result (calendar-sync--normalize-line-endings input))) + (should (string= input result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-only-cr () + "Test that bare CR characters (old Mac format) are removed. + +Input: String with \\r only (classic Mac OS line endings) +Expected: String with \\r removed (results in run-together text)" + (let* ((input "line1\rline2\rline3\r") + (expected "line1line2line3") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-cr-in-middle () + "Test that CR characters in middle of content are removed. + +Input: String with \\r not followed by \\n (unusual but possible) +Expected: All \\r removed regardless of position" + (let* ((input "line1\r\ntext\rwith\rmiddle\r\nline2") + (expected "line1\ntextwithmiddle\nline2") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-multiple-cr () + "Test that multiple consecutive CR characters are all removed. + +Input: String with \\r\\r or \\r\\r\\n sequences +Expected: All \\r characters removed" + (let* ((input "line1\r\r\nline2\r\r\r\nline3") + (expected "line1\nline2\nline3") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-single-line () + "Test normalization of single line with trailing CRLF. + +Input: Single line of text ending with \\r\\n +Expected: Single line ending with \\n" + (let* ((input "single line\r\n") + (expected "single line\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-only-line-endings () + "Test string containing only line ending characters. + +Input: String of only \\r\\n sequences +Expected: String of only \\n (CR stripped)" + (let* ((input "\r\n\r\n\r\n") + (expected "\n\n\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-unicode-content () + "Test normalization preserves Unicode characters. + +Input: String with Unicode and CRLF line endings +Expected: Unicode preserved, only CR removed" + (let* ((input "emoji 🎉\r\nchinese 䏿–‡\r\narabic العربية\r\n") + (expected "emoji 🎉\nchinese 䏿–‡\narabic العربية\n") + (result (calendar-sync--normalize-line-endings input))) + (should (string= expected result)) + (should-not (string-match-p "\r" result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-boundary-very-long-string () + "Test normalization of large string with many line endings. + +Input: String with 1000 lines with CRLF +Expected: Same content with LF only, performance acceptable" + (let* ((line "This is line content with some text\r\n") + (input (apply #'concat (make-list 1000 line))) + (result (calendar-sync--normalize-line-endings input))) + (should (= (length input) (+ (length result) 1000))) ; 1000 \r removed + (should-not (string-match-p "\r" result)) + (should (string-match-p "^This is line content" result)))) + +;;; Error Cases + +(ert-deftest test-calendar-sync--normalize-line-endings-error-nil-input () + "Test that nil input is handled gracefully. + +Input: nil +Expected: nil (defensive programming, no crash)" + (let ((result (calendar-sync--normalize-line-endings nil))) + (should (null result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-error-non-string-input () + "Test that non-string input is returned unchanged. + +Input: Integer (wrong type) +Expected: Same value returned (defensive, don't crash)" + (let ((result (calendar-sync--normalize-line-endings 42))) + (should (= 42 result)))) + +(ert-deftest test-calendar-sync--normalize-line-endings-error-symbol-input () + "Test that symbol input is handled gracefully. + +Input: Symbol (wrong type) +Expected: Symbol returned unchanged" + (let ((result (calendar-sync--normalize-line-endings 'some-symbol))) + (should (eq 'some-symbol result)))) + +(provide 'test-calendar-sync--normalize-line-endings) +;;; test-calendar-sync--normalize-line-endings.el ends here |
