From 76fa32da3979d6b033e5a366239490538e8d2533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84min=20Baumeler?= Date: Mon, 16 Jun 2025 13:04:01 +0200 Subject: [PATCH] bugfix: escaping --- src/awk/export.awk | 55 +++++++++++---------- src/awk/get.awk | 40 +++++++++++----- src/awk/list.awk | 117 +++++++++++++++++++++++++-------------------- src/awk/new.awk | 55 +++++++++++++-------- src/awk/update.awk | 59 +++++++++++++---------- 5 files changed, 197 insertions(+), 129 deletions(-) diff --git a/src/awk/export.awk b/src/awk/export.awk index f81f74f..86f7c19 100644 --- a/src/awk/export.awk +++ b/src/awk/export.awk @@ -1,34 +1,41 @@ -function getcontent(content_line, prop) -{ - return substr(content_line[prop], index(content_line[prop], ":") + 1); +# unescape +# Isolate and unescape the content part of an iCalendar line. +# +# @local variables: tmp +# @input str: String +# @return: Unescaped string +function unescape(str) { + gsub("\\\\n", "\n", str) + gsub("\\\\N", "\n", str) + gsub("\\\\,", ",", str) + gsub("\\\\;", ";", str) + gsub("\\\\\\\\", "\\", str) + return str } -function storetext_line(content_line, c, prop) -{ - c[prop] = getcontent(content_line, prop); - gsub("\\\\n", "\n", c[prop]); - gsub("\\\\N", "\n", c[prop]); - gsub("\\\\,", ",", c[prop]); - gsub("\\\\;", ";", c[prop]); - gsub("\\\\\\\\", "\\", c[prop]); +# getcontent +# Isolate content part of an iCalendar line, and unescape. +# +# @input str: String +# @return: Unescaped content part +function getcontent(str) { + return unescape(substr(str, index(str, ":") + 1)) } -BEGIN { FS = "[:;]"; } -/^BEGIN:(VJOURNAL|VTODO)/ { type = $2 } -/^END:/ && $2 == type { exit } -/^(CATEGORIES|DESCRIPTION|SUMMARY|DUE)/ { prop = $1; content_line[prop] = $0; next; } -/^[^ ]/ && prop { prop = ""; next; } -/^ / && prop { content_line[prop] = content_line[prop] substr($0, 2); next; } - +BEGIN { FS = "[:;]"; } +/^BEGIN:(VJOURNAL|VTODO)/ { type = $2 } +/^END:/ && $2 == type { exit } +/^(CATEGORIES|DESCRIPTION|SUMMARY|DUE)/ { prop = $1; c[prop] = $0; next; } +/^[^ ]/ && prop { prop = ""; next; } +/^ / && prop { c[prop] = c[prop] substr($0, 2); next; } END { - if (!type) { + if (!type) exit - } # Process content lines - storetext_line(content_line, c, "CATEGORIES" ); - storetext_line(content_line, c, "DESCRIPTION"); - storetext_line(content_line, c, "SUMMARY" ); - storetext_line(content_line, c, "DUE" ); + c["CATEGORIES"] = getcontent(c["CATEGORIES"]) + c["DESCRIPTION"] = getcontent(c["DESCRIPTION"]) + c["SUMMARY"] = getcontent(c["SUMMARY"]) + c["DUE"] = getcontent(c["DUE"]) # Print if (c["DUE"]) print "::: <| " substr(c["DUE"], 1, 4) "-" substr(c["DUE"], 5, 2) "-" substr(c["DUE"], 7, 2); diff --git a/src/awk/get.awk b/src/awk/get.awk index a013736..ef9319b 100644 --- a/src/awk/get.awk +++ b/src/awk/get.awk @@ -1,18 +1,36 @@ +# unescape +# Isolate and unescape the content part of an iCalendar line. +# +# @local variables: tmp +# @input str: String +# @return: Unescaped string +function unescape(str) { + gsub("\\\\n", "\n", str) + gsub("\\\\N", "\n", str) + gsub("\\\\,", ",", str) + gsub("\\\\;", ";", str) + gsub("\\\\\\\\", "\\", str) + return str +} + +# getcontent +# Isolate content part of an iCalendar line, and unescape. +# +# @input str: String +# @return: Unescaped content part +function getcontent(str) { + return unescape(substr(str, index(str, ":") + 1)) +} + # print content of field `field` BEGIN { FS = ":"; regex = "^" field; } /^BEGIN:(VJOURNAL|VTODO)/ { type = $2 } /^END:/ && $2 == type { exit } -$0 ~ field { content = $0; next; } -/^ / && content { content = content substr($0, 2); next; } -/^[^ ]/ && content { exit } +$0 ~ field { line = $0; next; } +/^ / && line { line = line substr($0, 2); next; } +/^[^ ]/ && line { exit } END { if (!type) { exit } - # Process content line - content = substr(content, index(content, ":") + 1); - gsub("\\\\n", "\n", content); - gsub("\\\\N", "\n", content); - gsub("\\\\,", ",", content); - gsub("\\\\;", ";", content); - gsub("\\\\\\\\", "\\", content); - print content; + # Process line + print getcontent(line) } diff --git a/src/awk/list.awk b/src/awk/list.awk index 0a05324..7aea268 100644 --- a/src/awk/list.awk +++ b/src/awk/list.awk @@ -3,9 +3,28 @@ # See https://datatracker.ietf.org/doc/html/rfc5545 for the RFC 5545 that # describes iCalendar, and its syntax -function getcontent(content_line, prop) -{ - return substr(content_line[prop], index(content_line[prop], ":") + 1); +# unescape +# Isolate and unescape the content part of an iCalendar line. +# +# @local variables: tmp +# @input str: String +# @return: Unescaped string +function unescape(str) { + gsub("\\\\n", "\n", str) + gsub("\\\\N", "\n", str) + gsub("\\\\,", ",", str) + gsub("\\\\;", ";", str) + gsub("\\\\\\\\", "\\", str) + return str +} + +# getcontent +# Isolate content part of an iCalendar line, and unescape. +# +# @input str: String +# @return: Unescaped content part +function getcontent(str) { + return unescape(substr(str, index(str, ":") + 1)) } function storetext_line(content_line, c, prop) @@ -19,23 +38,14 @@ function storetext_line(content_line, c, prop) #gsub(" ", "_", c[prop]); } -function storeinteger(content_line, c, prop) -{ - c[prop] = getcontent(content_line, prop); - c[prop] = c[prop] ? c[prop] : 0; -} - -function storedatetime(content_line, c, prop) -{ - c[prop] = getcontent(content_line, prop); -} - -function storedate(content_line, c, prop) -{ - c[prop] = substr(getcontent(content_line, prop), 1, 8); -} - -function formatdate(date, today, todaystamp, ts, ts_y, ts_m, ts_d, delta) +# formatdate +# Generate kind-of-pretty date strings. +# +# @local variables: ts, ts_y, ts_m, ts_d, delta +# @input date: Date in the format YYYYMMDD +# @input todaystamp: Today, seconds since epoch +# @return: string +function formatdate(date, todaystamp, ts, ts_y, ts_m, ts_d, delta) { ts_y = substr(date, 1, 4); ts_m = substr(date, 5, 2); @@ -87,7 +97,6 @@ BEGIN { RED = "\033[1;31m"; WHITE = "\033[1;97m"; CYAN = "\033[1;36m"; - FAINT = "\033[2m"; OFF = "\033[m"; # For date comparision @@ -99,7 +108,6 @@ BEGIN { BEGINFILE { type = ""; prop = ""; - delete content_line; delete c; } @@ -114,7 +122,7 @@ BEGINFILE { /^(CATEGORIES|DESCRIPTION|PRIORITY|STATUS|SUMMARY|COMPLETED|DUE|DTSTART|DURATION|CREATED|DTSTAMP|LAST-MODIFIED)/ { prop = $1; - content_line[prop] = $0; + c[prop] = $0; next; } /^[^ ]/ && prop { @@ -122,7 +130,7 @@ BEGINFILE { next; } /^ / && prop { - content_line[prop] = content_line[prop] substr($0, 2); + c[prop] = c[prop] substr($0, 2); next; } @@ -146,26 +154,33 @@ ENDFILE { collection = collection in collection2label ? collection2label[collection] : collection; # Process content lines - storetext_line(content_line, c, "CATEGORIES" ); - storetext_line(content_line, c, "DESCRIPTION" ); - storeinteger( content_line, c, "PRIORITY" ); - storetext_line(content_line, c, "STATUS" ); - storetext_line(content_line, c, "SUMMARY" ); - storedatetime( content_line, c, "COMPLETED" ); - storedate( content_line, c, "DUE" ); - storedate( content_line, c, "DTSTART" ); - storedatetime( content_line, c, "DURATION" ); - storedatetime( content_line, c, "CREATED" ); - storedatetime( content_line, c, "DTSTAMP" ); - storedatetime( content_line, c, "LAST-MODIFIED"); + # strings + cat = unescape(getcontent(c["CATEGORIES"])) + des = unescape(getcontent(c["DESCRIPTION"])) + sta = unescape(getcontent(c["STATUS"])) + sum = unescape(getcontent(c["SUMMARY"])) + + # integers + pri = unescape(getcontent(c["PRIORITY"])) + pri = pri ? pri + 0 : 0 + + # dates + due = substr(unescape(getcontent(c["DUE"])), 1, 8) + dts = substr(unescape(getcontent(c["DTSTART"])), 1, 8) + + # date-times + com = unescape(getcontent(c["COMPLETED"])) + dur = unescape(getcontent(c["DURATION"])) + cre = unescape(getcontent(c["CREATED"])) + stp = unescape(getcontent(c["DTSTAMP"])) + lmd = unescape(getcontent(c["LAST-MODIFIED"])) # Priority field, primarly used for sorting - priotext = ""; - prio = 0; - if (c["PRIORITY"] > 0) + psort = 0; + if (pri > 0) { - priotext = "❗(" c["PRIORITY"] ") "; - prio = 10 - c["PRIORITY"]; + priotext = "❗(" pri ") " + psort = 10 - pri } # Last modification/creation time stamp, used for sorting @@ -173,7 +188,7 @@ ENDFILE { # UTC time format # DTSTAMP: mandatory field in VTODO and VJOURNAL, date-time in UTC time # format - mod = c["LAST-MODIFIED"] ? c["LAST-MODIFIED"] : c["DTSTAMP"]; + mod = lmd ? lmd : stp # Date field. For VTODO entries, we show the due date, for journal entries, # the associated date. @@ -183,9 +198,9 @@ ENDFILE { if (type == "VTODO") { # Either DUE or DURATION may appear. If DURATION appears, then also DTSTART - d = c["DUE"] ? c["DUE"] : - (c["DURATION"] ? c["DTSTART"] " for " c["DURATION"] : ""); - if (d && d <= today && c["STATUS"] != "COMPLETED") + d = due ? due : + (dur ? dts " for " dur : ""); + if (d && d <= today && sta != "COMPLETED") { datecolor = RED; summarycolor = RED; @@ -193,28 +208,28 @@ ENDFILE { } else { d = c["DTSTART"]; } - d = d ? formatdate(d, today, todaystamp ts, ts_y, ts_m, ts_d, delta) : " "; + d = d ? formatdate(d, todaystamp) : " "; # flag: - "journal" for VJOURNAL with DTSTART # - "note" for VJOURNAL without DTSTART # - "completed" for VTODO with c["STATUS"] == COMPLETED # - "open" for VTODO with c["STATUS"] != COMPLETED if (type == "VTODO") - flag = c["STATUS"] == "COMPLETED" ? flag_completed : flag_open; + flag = sta == "COMPLETED" ? flag_completed : flag_open; else - flag = c["DTSTART"] ? flag_journal : flag_note; + flag = dts ? flag_journal : flag_note; # summary # c["SUMMARY"] - summary = c["SUMMARY"] ? c["SUMMARY"] : " " + summary = sum ? sum : " " # categories - categories = c["CATEGORIES"] ? c["CATEGORIES"] : " " + categories = cat ? cat : " " # filename # FILENAME - print prio, + print psort, mod, fpath, collection, diff --git a/src/awk/new.awk b/src/awk/new.awk index 407da56..cb362c0 100644 --- a/src/awk/new.awk +++ b/src/awk/new.awk @@ -1,27 +1,44 @@ -function escape_categories(str) -{ - gsub("\\\\", "\\\\", str); - gsub(";", "\\\\;", str); -} - +# Escape string to be used as content in iCalendar files. +# +# @input str: String to escape +# @return: Escaped string function escape(str) { - escape_categories(str) - gsub(",", "\\\\,", str); + gsub("\\\\", "\\", str) + gsub(";", "\\;", str) + gsub(",", "\\,", str) + return str } +# Escape string to be used as content in iCalendar files. +# +# @input str: String to escape +# @return: Escaped string +function escape_categories(str) +{ + gsub("\\\\", "\\", str) + gsub(";", "\\;", str) + return str +} + +# Print property with its content and fold according to the iCalendar +# specification. +# +# @local variables: i, s +# @input nameparam: Property name with optional parameters +# @input content: Escaped content function print_fold(nameparam, content, i, s) { - i = 74 - length(nameparam); - s = substr(content, 1, i); - print nameparam s; - s = substr(content, i+1, 73); - i = i + 73; + i = 74 - length(nameparam) + s = substr(content, 1, i) + print nameparam s + s = substr(content, i+1, 73) + i = i + 73 while (s) { - print " " s; - s = substr(content, i+1, 73); - i = i + 73; + print " " s + s = substr(content, i+1, 73) + i = i + 73 } } @@ -88,9 +105,9 @@ END { if (start) print "DTSTART;VALUE=DATE:" start; } - if (summary) print_fold("SUMMARY:", summary, i, s); - if (categories) print_fold("CATEGORIES:", categories, i, s); - if (desc) print_fold("DESCRIPTION:", desc, i, s); + if (summary) print_fold("SUMMARY:", summary); + if (categories) print_fold("CATEGORIES:", categories); + if (desc) print_fold("DESCRIPTION:", desc); print "END:" type; print "END:VCALENDAR" } diff --git a/src/awk/update.awk b/src/awk/update.awk index 7939bd3..1786329 100644 --- a/src/awk/update.awk +++ b/src/awk/update.awk @@ -1,33 +1,44 @@ -function getcontent(content_line, prop) -{ - return substr(content_line[prop], index(content_line[prop], ":") + 1); -} - -function escape_categories(str) -{ - gsub("\\\\", "\\\\", str); - gsub(";", "\\;", str); -} - +# Escape string to be used as content in iCalendar files. +# +# @input str: String to escape +# @return: Escaped string function escape(str) { - escape_categories(str) - gsub(",", "\\,", str); + gsub("\\\\", "\\", str) + gsub(";", "\\;", str) + gsub(",", "\\,", str) return str } +# Escape string to be used as content in iCalendar files. +# +# @input str: String to escape +# @return: Escaped string +function escape_categories(str) +{ + gsub("\\\\", "\\", str) + gsub(";", "\\;", str) + return str +} + +# Print property with its content and fold according to the iCalendar +# specification. +# +# @local variables: i, s +# @input nameparam: Property name with optional parameters +# @input content: Escaped content function print_fold(nameparam, content, i, s) { - i = 74 - length(nameparam); - s = substr(content, 1, i); - print nameparam s; - s = substr(content, i+1, 73); - i = i + 73; + i = 74 - length(nameparam) + s = substr(content, 1, i) + print nameparam s + s = substr(content, i+1, 73) + i = i + 73 while (s) { - print " " s; - s = substr(content, i+1, 73); - i = i + 73; + print " " s + s = substr(content, i+1, 73) + i = i + 73 } } @@ -78,9 +89,9 @@ NR == FNR { print "SEQUENCE:" seq; print "LAST-MODIFIED:" zulu; if (due) print "DUE;VALUE=DATE:" due; - print_fold("SUMMARY:", summary, i, s); - print_fold("CATEGORIES:", categories, i, s); - print_fold("DESCRIPTION:", desc, i, s); + print_fold("SUMMARY:", summary); + print_fold("CATEGORIES:", categories); + print_fold("DESCRIPTION:", desc); type = ""; } { print }