diff --git a/man/rem2ps.1.in b/man/rem2ps.1.in index 01eae2f9..fef18c9b 100644 --- a/man/rem2ps.1.in +++ b/man/rem2ps.1.in @@ -516,10 +516,28 @@ MOON, etc.) If any TAG clauses are present, the \fBtags\fR key will be present and consist of a comma-separated list of tags. .TP -.B info [\fIarray\fR] +.B info \fR{ \fIhash\fR } If any INFO clauses are present, the \fBinfo\fR key will be present. Its -value will be an array of info strings, one for each INFO clause and in -the same order as the INFO clauses. +value will be a hash of info key-value pairs. Each key is the header +from an INFO string, \fIconverted to all upper-case\fR. The value is the +valu from the INFO string. +.RS +.PP +For example, the following REM command: +.PP +.nf + REM INFO "Location: Boardroom" INFO "Summary: None" MSG whatever +.fi +.PP +will produce the following \fBinfo\fR hash: +.PP +.nf + "info" : { + "LOCATION" : "Boardroom", + "SUMMARY" : "None" + }, +.fi +.RE .TP .B time \fIt\fR If an AT clause was present, this key will contain the time of the AT clause diff --git a/man/remind.1.in b/man/remind.1.in index 6ebba6f3..dd2567e1 100644 --- a/man/remind.1.in +++ b/man/remind.1.in @@ -1365,11 +1365,20 @@ to each distinct REM command. .PP The \fBINFO\fR keyword is similar to \fBTAG\fR but is intended to convey metadata about an event, such as its location. Back-ends will have their -own rules about the format of the \fIinfo_string\fRs and must ignore -\fIinfo_string\fRs they don't recognize. Note that \fBINFO\fR must +own rules about which \fIinfo_string\fRs they recognize, and must ignore +\fIinfo_string\fRs they don't recognize. Note that \fBINFO\fR must be followed by a quoted string; you can include newlines in the string by supplying them as "\\n". .PP +An INFO string \fImust\fR be of the form "Header: Value". The header +can consist of any printable character, but cannot contain whitespace. +The value can consist of any characters you like. Space may not +appear before the colon, but can appear afterwards; such space is not +considered to be part of the value. If there is more than one INFO +string for a given reminder, there cannot be any duplicate headers. +Case is ignored when determining if a header is a duplicate of an +existing one. +.PP For example, a hypothetical back-end might let you set the location and description of a reminder like this: .PP diff --git a/src/calendar.c b/src/calendar.c index c63c898a..ecbd3518 100644 --- a/src/calendar.c +++ b/src/calendar.c @@ -409,6 +409,23 @@ void PrintJSONString(char const *s) } } +void PrintJSONStringUC(char const *s) +{ + while (*s) { + switch(*s) { + case '\b': printf("\\b"); break; + case '\f': printf("\\f"); break; + case '\n': printf("\\n"); break; + case '\r': printf("\\r"); break; + case '\t': printf("\\t"); break; + case '"': printf("\\\""); break; + case '\\': printf("\\\\"); break; + default: printf("%c", toupper(*s)); + } + s++; + } +} + void PrintJSONKeyPairInt(char const *name, int val) { printf("\""); @@ -2354,6 +2371,41 @@ void WriteJSONTimeTrigger(TimeTrig const *tt) } } +static void +WriteJSONInfoChain(TrigInfo *ti) +{ + printf("\"info\":{"); + while (ti) { + /* Skanky... */ + char *colon = (char *) strchr(ti->info, ':'); + char const *value; + if (!colon) { + /* Should be impossible... */ + ti = ti->next; + continue; + } + /* Terminate the string at the colon */ + *colon = 0; + + value = colon+1; + while(*value && isspace(*value)) { + value++; + } + printf("\""); + PrintJSONStringUC(ti->info); + printf("\":\""); + PrintJSONString(value); + printf("\""); + + /* Restore the value of the colon */ + *colon = ':'; + if (ti->next) { + printf(","); + } + ti = ti->next; + } + printf("},"); +} void WriteJSONTrigger(Trigger const *t, int include_tags, int today) { /* wd is an array of days from 0=monday to 6=sunday. @@ -2446,18 +2498,7 @@ void WriteJSONTrigger(Trigger const *t, int include_tags, int today) } if (include_tags) { if (t->infos) { - TrigInfo *ti = t->infos; - printf("\"info\":["); - while (ti) { - printf("\""); - PrintJSONString(ti->info); - printf("\""); - if (ti->next) { - printf(","); - } - ti = ti->next; - } - printf("],"); + WriteJSONInfoChain(t->infos); } PrintJSONKeyPairString("tags", DBufValue(&(t->tags))); } @@ -2473,18 +2514,7 @@ static void WriteSimpleEntryProtocol2(CalEntry *e, int today) PrintJSONKeyPairString("passthru", e->passthru); PrintJSONKeyPairString("tags", DBufValue(&(e->tags))); if (e->infos) { - TrigInfo *ti = e->infos; - printf("\"info\":["); - while (ti) { - printf("\""); - PrintJSONString(ti->info); - printf("\""); - if (ti->next) { - printf(","); - } - ti = ti->next; - } - printf("],"); + WriteJSONInfoChain(e->infos); } if (e->duration != NO_TIME) { PrintJSONKeyPairInt("duration", e->duration); diff --git a/src/protos.h b/src/protos.h index d59569f6..bb463179 100644 --- a/src/protos.h +++ b/src/protos.h @@ -280,3 +280,5 @@ TrigInfo *NewTrigInfo(char const *i); void FreeTrigInfo(TrigInfo *ti); void FreeTrigInfoChain(TrigInfo *ti); int AppendTrigInfo(Trigger *t, char const *info); +int TrigInfoHeadersAreTheSame(char const *i1, char const *i2); +int TrigInfoIsValid(char const *info); diff --git a/src/trigger.c b/src/trigger.c index b097331c..4335387e 100644 --- a/src/trigger.c +++ b/src/trigger.c @@ -732,8 +732,16 @@ FreeTrigInfoChain(TrigInfo *ti) int AppendTrigInfo(Trigger *t, char const *info) { - TrigInfo *ti = NewTrigInfo(info); - TrigInfo *last = t->infos; + TrigInfo *ti; + TrigInfo *last; + + if (!TrigInfoIsValid(info)) { + Eprint("%s", tr("Invalid INFO string: Must be of the form \"Header: Value\"")); + return E_PARSE_ERR; + } + + ti = NewTrigInfo(info); + last = t->infos; if (!ti) { return E_NO_MEM; } @@ -741,10 +749,46 @@ AppendTrigInfo(Trigger *t, char const *info) t->infos = ti; return OK; } + if (TrigInfoHeadersAreTheSame(info, last->info)) { + Eprint("%s", tr("Duplicate INFO headers are not permitted")); + FreeTrigInfo(ti); + return E_PARSE_ERR; + } while (last->next) { last = last->next; + if (TrigInfoHeadersAreTheSame(info, last->info)) { + Eprint("%s", tr("Duplicate INFO headers are not permitted")); + FreeTrigInfo(ti); + return E_PARSE_ERR; + } } last->next = ti; return OK; } +int +TrigInfoHeadersAreTheSame(char const *i1, char const *i2) +{ + char const *c1 = strchr(i1, ':'); + char const *c2 = strchr(i2, ':'); + if (!c1 || !c2) return 1; + if (c1 - i1 != c2 - i2) return 0; + if (!strncasecmp(i1, i2, (c1 - i1))) return 1; + return 0; +} + +int +TrigInfoIsValid(char const *info) +{ + char const *t; + char const *s = strchr(info, ':'); + if (!s) return 0; + if (s == info) return 0; + + t = info; + while (t < s) { + if (isspace(*t) || iscntrl(*t)) return 0; + t++; + } + return 1; +} diff --git a/tests/queue1.rem b/tests/queue1.rem index b4268c98..b746441e 100644 --- a/tests/queue1.rem +++ b/tests/queue1.rem @@ -1,6 +1,6 @@ FSET msgprefix(x) "Priority: " + x + "; Filename: " + filename() + ": " REM at 23:56 MSG foo -REM PRIORITY 42 at 23:57 INFO "A piece of info" MSG bar -REM PRIORITY 999 at 23:58 INFO "info1" INFO "info2" MSG quux +REM PRIORITY 42 at 23:57 INFO "Info: yuppers" MSG bar +REM PRIORITY 999 at 23:58 INFO "Info2: Nope" INFO "Info3: heh" MSG quux DO queue2.rem diff --git a/tests/test-rem b/tests/test-rem index 530dcb69..902ea8d4 100644 --- a/tests/test-rem +++ b/tests/test-rem @@ -650,6 +650,16 @@ EOF REM Wed INFO "Location: here" INFO "Summary: Nope" MSG Meeting EOF +# Invalid info strings +../src/remind - 1 Feb 2024 <<'EOF' >> ../tests/test.out 2>&1 +REM Thu INFO "Invalid" MSG wookie +REM Fri INFO ": foo" MSG blat +REM Sun INFO "foo bar baz : blork" MSG uua + +# Duplicate info string +REM Sat INFO "Location: here" INFO "location: there" MSG blort +EOF + # Languages for i in ../include/lang/??.rem ; do ../src/remind -r -q "-ii=\"$i\"" ../tests/tstlang.rem 1 Feb 2024 13:34 >> ../tests/test.out 2>&1 diff --git a/tests/test.cmp b/tests/test.cmp index 99c8fbc9..c3fc0704 100644 --- a/tests/test.cmp +++ b/tests/test.cmp @@ -23205,12 +23205,12 @@ Enabling test mode: This is meant for the acceptance test. Do not use --test in production. In test mode, the system time is fixed at 2025-01-06@19:00 NOTE JSONQUEUE -[{"priority":2,"eventstart":"2025-01-06T23:59","time":"23:59","nexttime":"23:59","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue2.rem","lineno":1,"type":"MSG_TYPE","body":"XXXX"},{"priority":999,"eventstart":"2025-01-06T23:58","info":["info1","info2"],"time":"23:58","nexttime":"23:58","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":5,"type":"MSG_TYPE","body":"quux"},{"priority":42,"eventstart":"2025-01-06T23:57","info":["A piece of info"],"time":"23:57","nexttime":"23:57","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":4,"type":"MSG_TYPE","body":"bar"},{"priority":5000,"eventstart":"2025-01-06T23:56","time":"23:56","nexttime":"23:56","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":3,"type":"MSG_TYPE","body":"foo"}] +[{"priority":2,"eventstart":"2025-01-06T23:59","time":"23:59","nexttime":"23:59","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue2.rem","lineno":1,"type":"MSG_TYPE","body":"XXXX"},{"priority":999,"eventstart":"2025-01-06T23:58","info":{"INFO2":"Nope","INFO3":"heh"},"time":"23:58","nexttime":"23:58","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":5,"type":"MSG_TYPE","body":"quux"},{"priority":42,"eventstart":"2025-01-06T23:57","info":{"INFO":"yuppers"},"time":"23:57","nexttime":"23:57","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":4,"type":"MSG_TYPE","body":"bar"},{"priority":5000,"eventstart":"2025-01-06T23:56","time":"23:56","nexttime":"23:56","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":3,"type":"MSG_TYPE","body":"foo"}] NOTE ENDJSONQUEUE Enabling test mode: This is meant for the acceptance test. Do not use --test in production. In test mode, the system time is fixed at 2025-01-06@19:00 -{"response":"queue","queue":[{"priority":2,"eventstart":"2025-01-06T23:59","time":"23:59","nexttime":"23:59","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue2.rem","lineno":1,"type":"MSG_TYPE","body":"XXXX"},{"priority":999,"eventstart":"2025-01-06T23:58","info":["info1","info2"],"time":"23:58","nexttime":"23:58","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":5,"type":"MSG_TYPE","body":"quux"},{"priority":42,"eventstart":"2025-01-06T23:57","info":["A piece of info"],"time":"23:57","nexttime":"23:57","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":4,"type":"MSG_TYPE","body":"bar"},{"priority":5000,"eventstart":"2025-01-06T23:56","time":"23:56","nexttime":"23:56","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":3,"type":"MSG_TYPE","body":"foo"}],"command":"QUEUE"} +{"response":"queue","queue":[{"priority":2,"eventstart":"2025-01-06T23:59","time":"23:59","nexttime":"23:59","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue2.rem","lineno":1,"type":"MSG_TYPE","body":"XXXX"},{"priority":999,"eventstart":"2025-01-06T23:58","info":{"INFO2":"Nope","INFO3":"heh"},"time":"23:58","nexttime":"23:58","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":5,"type":"MSG_TYPE","body":"quux"},{"priority":42,"eventstart":"2025-01-06T23:57","info":{"INFO":"yuppers"},"time":"23:57","nexttime":"23:57","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":4,"type":"MSG_TYPE","body":"bar"},{"priority":5000,"eventstart":"2025-01-06T23:56","time":"23:56","nexttime":"23:56","tdelta":0,"trep":0,"qid":"42424242","rundisabled":0,"ntrig":1,"filename":"../tests/queue1.rem","lineno":3,"type":"MSG_TYPE","body":"foo"}],"command":"QUEUE"} BANNER % REM 29 MSG One -(2): Trig = Thursday, 29 February, 2024 @@ -24503,6 +24503,7 @@ TRANSLATE "Cannot open `%s' for writing: %s" "" TRANSLATE "Cannot stat %s - not running as daemon!" "" TRANSLATE "Cannot use AT clause in multitrig() function" "" TRANSLATE "Do not use ["["]] around expression in SET command" "" +TRANSLATE "Duplicate INFO headers are not permitted" "" TRANSLATE "Error: THROUGH date earlier than start date" "" TRANSLATE "Executing `%s' for INCLUDECMD and caching as `%s'" "" TRANSLATE "Found cached directory listing for `%s'" "" @@ -24510,6 +24511,7 @@ TRANSLATE "Function `%s' defined at %s:%d should take %d argument%s, but actuall TRANSLATE "Function `%s' redefined (previously defined at %s:%d)" "" TRANSLATE "GetValidHebDate: Bad adarbehave value %d" "" TRANSLATE "In" "" +TRANSLATE "Invalid INFO string: Must be of the form \"Header: Value\"" "" TRANSLATE "Invalid translation: Both original and translated must have the same printf-style formatting sequences in the same order." "" TRANSLATE "Missing REM type; assuming MSG" "" TRANSLATE "No Adar A in %d" "" @@ -24655,11 +24657,16 @@ February 2024 29 4 0 Sunday Monday Tuesday Wednesday Thursday Friday Saturday January 31 March 31 -{"date":"2024-02-07","filename":"-","lineno":1,"info":["Location: here","Summary: Nope"],"wd":["Wednesday"],"priority":5000,"body":"Meeting"} -{"date":"2024-02-14","filename":"-","lineno":1,"info":["Location: here","Summary: Nope"],"wd":["Wednesday"],"priority":5000,"body":"Meeting"} -{"date":"2024-02-21","filename":"-","lineno":1,"info":["Location: here","Summary: Nope"],"wd":["Wednesday"],"priority":5000,"body":"Meeting"} -{"date":"2024-02-28","filename":"-","lineno":1,"info":["Location: here","Summary: Nope"],"wd":["Wednesday"],"priority":5000,"body":"Meeting"} +{"date":"2024-02-07","filename":"-","lineno":1,"info":{"LOCATION":"here","SUMMARY":"Nope"},"wd":["Wednesday"],"priority":5000,"body":"Meeting"} +{"date":"2024-02-14","filename":"-","lineno":1,"info":{"LOCATION":"here","SUMMARY":"Nope"},"wd":["Wednesday"],"priority":5000,"body":"Meeting"} +{"date":"2024-02-21","filename":"-","lineno":1,"info":{"LOCATION":"here","SUMMARY":"Nope"},"wd":["Wednesday"],"priority":5000,"body":"Meeting"} +{"date":"2024-02-28","filename":"-","lineno":1,"info":{"LOCATION":"here","SUMMARY":"Nope"},"wd":["Wednesday"],"priority":5000,"body":"Meeting"} # rem2ps2 end +-stdin-(1): Invalid INFO string: Must be of the form "Header: Value" +-stdin-(2): Invalid INFO string: Must be of the form "Header: Value" +-stdin-(3): Invalid INFO string: Must be of the form "Header: Value" +-stdin-(6): Duplicate INFO headers are not permitted +No reminders. Agenda pel dijous, 1 de febrer de 2024: Language: ca