diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-06 10:15:08 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-06 10:15:08 -0600 |
| commit | a980541c7300001181a25c2ed80401766d3abcaa (patch) | |
| tree | 1418a96c09172efb9d7b8b8c79bf483f38000326 | |
| parent | 00ddf74b232b0762baa7826e62f6765d087041fb (diff) | |
fix(calendar-sync): sanitize description text to prevent org heading corruption
Event descriptions containing lines starting with * were being interpreted
as org headings, breaking file structure. Replace leading asterisks with
dashes before writing descriptions.
| -rw-r--r-- | modules/calendar-sync.el | 15 | ||||
| -rw-r--r-- | tests/test-calendar-sync--event-to-org.el | 11 | ||||
| -rw-r--r-- | tests/test-calendar-sync--sanitize-org-body.el | 73 |
3 files changed, 94 insertions, 5 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index 06248531..022aff80 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -275,6 +275,17 @@ Returns nil for nil input. Returns empty string for whitespace-only input." (when text (string-trim (calendar-sync--strip-html (calendar-sync--unescape-ics-text text))))) +(defun calendar-sync--sanitize-org-body (text) + "Sanitize TEXT for safe inclusion as org body content. +Replaces leading asterisks with dashes to prevent lines from being +parsed as org headings. Handles multiple levels (e.g. ** becomes --)." + (when text + (replace-regexp-in-string + "^\\(\\*+\\) " + (lambda (match) + (concat (make-string (length (match-string 1 match)) ?-) " ")) + text))) + ;;; Date Utilities (defun calendar-sync--add-months (date months) @@ -1175,9 +1186,9 @@ Description appears as body text after the drawer." (dolist (prop props) (push prop parts)) (push ":END:" parts)) - ;; Add description as body text + ;; Add description as body text (sanitized to prevent org heading conflicts) (when (and description (not (string-empty-p description))) - (push description parts)) + (push (calendar-sync--sanitize-org-body description) parts)) (string-join (nreverse parts) "\n")))) (defun calendar-sync--event-start-time (event) diff --git a/tests/test-calendar-sync--event-to-org.el b/tests/test-calendar-sync--event-to-org.el index 96ce49d5..a3dc0106 100644 --- a/tests/test-calendar-sync--event-to-org.el +++ b/tests/test-calendar-sync--event-to-org.el @@ -92,7 +92,7 @@ (should (string-match-p ":END:" result))))) (ert-deftest test-calendar-sync--event-to-org-boundary-description-with-asterisks () - "Test event description containing org-special asterisks." + "Test event description containing org-special asterisks are sanitized." (let* ((start (test-calendar-sync-time-days-from-now 5 14 0)) (end (test-calendar-sync-time-days-from-now 5 15 0)) (event (list :summary "Meeting" @@ -100,8 +100,13 @@ :end end :description "* agenda item 1\n** sub-item"))) (let ((result (calendar-sync--event-to-org event))) - ;; Description should be present - (should (string-match-p "agenda item" result))))) + ;; Description content should be present + (should (string-match-p "agenda item" result)) + ;; Leading asterisks replaced with dashes to prevent org heading conflicts + (should (string-match-p "^- agenda item 1" result)) + (should (string-match-p "^-- sub-item" result)) + ;; Only the event heading should use * + (should (= 1 (length (split-string result "^\\* " t))))))) (ert-deftest test-calendar-sync--event-to-org-boundary-organizer-email-only () "Test organizer without CN shows email." diff --git a/tests/test-calendar-sync--sanitize-org-body.el b/tests/test-calendar-sync--sanitize-org-body.el new file mode 100644 index 00000000..c85e763c --- /dev/null +++ b/tests/test-calendar-sync--sanitize-org-body.el @@ -0,0 +1,73 @@ +;;; test-calendar-sync--sanitize-org-body.el --- Tests for org body sanitization -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for calendar-sync--sanitize-org-body. +;; Ensures description text with org-special syntax (leading asterisks) +;; is escaped to prevent corruption of the org file structure. + +;;; Code: + +(require 'ert) +(require 'calendar-sync) + +;;; Normal Cases + +(ert-deftest test-calendar-sync--sanitize-org-body-normal-single-asterisk () + "Single leading asterisk replaced with dash." + (should (equal "- item one" (calendar-sync--sanitize-org-body "* item one")))) + +(ert-deftest test-calendar-sync--sanitize-org-body-normal-double-asterisk () + "Double leading asterisks replaced with double dashes." + (should (equal "-- sub-item" (calendar-sync--sanitize-org-body "** sub-item")))) + +(ert-deftest test-calendar-sync--sanitize-org-body-normal-triple-asterisk () + "Triple leading asterisks replaced with triple dashes." + (should (equal "--- deep item" (calendar-sync--sanitize-org-body "*** deep item")))) + +(ert-deftest test-calendar-sync--sanitize-org-body-normal-multiline () + "Multiple lines with asterisks all get sanitized." + (let ((input "Format:\n* What did you do yesterday?\n* What are you doing today?\n* Is anything in your way?") + (expected "Format:\n- What did you do yesterday?\n- What are you doing today?\n- Is anything in your way?")) + (should (equal expected (calendar-sync--sanitize-org-body input))))) + +(ert-deftest test-calendar-sync--sanitize-org-body-normal-mixed-lines () + "Only lines starting with asterisks are changed." + (let ((input "Normal line\n* Bullet line\nAnother normal line")) + (should (equal "Normal line\n- Bullet line\nAnother normal line" + (calendar-sync--sanitize-org-body input))))) + +(ert-deftest test-calendar-sync--sanitize-org-body-normal-mixed-levels () + "Lines with different asterisk counts are each handled." + (let ((input "* Top\n** Middle\n*** Bottom")) + (should (equal "- Top\n-- Middle\n--- Bottom" + (calendar-sync--sanitize-org-body input))))) + +;;; Boundary Cases + +(ert-deftest test-calendar-sync--sanitize-org-body-boundary-nil-input () + "Nil input returns nil." + (should (null (calendar-sync--sanitize-org-body nil)))) + +(ert-deftest test-calendar-sync--sanitize-org-body-boundary-empty-string () + "Empty string returns empty string." + (should (equal "" (calendar-sync--sanitize-org-body "")))) + +(ert-deftest test-calendar-sync--sanitize-org-body-boundary-no-asterisks () + "Text without leading asterisks is returned unchanged." + (let ((input "Just a normal description\nwith multiple lines")) + (should (equal input (calendar-sync--sanitize-org-body input))))) + +(ert-deftest test-calendar-sync--sanitize-org-body-boundary-asterisk-mid-line () + "Asterisks not at line start are left alone." + (should (equal "Use * for emphasis" (calendar-sync--sanitize-org-body "Use * for emphasis")))) + +(ert-deftest test-calendar-sync--sanitize-org-body-boundary-asterisk-no-space () + "Asterisk at line start without trailing space is not a heading — left alone." + (should (equal "*bold text*" (calendar-sync--sanitize-org-body "*bold text*")))) + +(ert-deftest test-calendar-sync--sanitize-org-body-boundary-asterisk-only () + "Lone asterisk with space at start of line is sanitized." + (should (equal "- " (calendar-sync--sanitize-org-body "* ")))) + +(provide 'test-calendar-sync--sanitize-org-body) +;;; test-calendar-sync--sanitize-org-body.el ends here |
