]> diplodocus.org Git - nmh/blob - sbr/datetime.c
Fixed mhshow part markers when displaying multiple messages.
[nmh] / sbr / datetime.c
1 /* datetime.c -- functions for manipulating RFC 5545 date-time values
2 *
3 * This code is Copyright (c) 2014, by the authors of nmh.
4 * See the COPYRIGHT file in the root directory of the nmh
5 * distribution for complete copyright information.
6 */
7
8 #include "h/mh.h"
9 #include "dtime.h"
10 #include "error.h"
11 #include "h/icalendar.h"
12 #include "datetime.h"
13 #include "h/fmt_scan.h"
14 #include "h/tws.h"
15 #include "h/utils.h"
16 #include "unquote.h"
17
18 /*
19 * This doesn't try to support all of the myriad date-time formats
20 * allowed by RFC 5545. It is only used for viewing date-times,
21 * so that shouldn't be a problem: if a particular format can't
22 * be handled by this code, just present it to the user in its
23 * original form.
24 *
25 * And, this assumes a valid iCalendar input file. E.g, it
26 * doesn't check that each BEGIN has a matching END and vice
27 * versa. That should be done in the parser, though it currently
28 * isn't.
29 */
30
31 typedef struct tzparams {
32 /* Pointers to values in parse tree.
33 * TZOFFSETFROM is used to calculate the absolute time at which
34 * the transition to a given observance takes place.
35 * TZOFFSETTO is the timezone offset from UTC. Both are in HHmm
36 * format. */
37 char *offsetfrom, *offsetto;
38 const char *dtstart;
39 const char *rrule;
40
41 /* This is only used to make sure that timezone applies. And not
42 always, because if the timezone DTSTART is before the epoch, we
43 don't try to compare to it. */
44 time_t start_dt; /* in seconds since epoch */
45 } tzparams;
46
47 struct tzdesc {
48 char *tzid;
49
50 /* The following are translations of the pieces of RRULE and DTSTART
51 into seconds from beginning of year. */
52 tzparams standard_params;
53 tzparams daylight_params;
54
55 struct tzdesc *next;
56 };
57
58 /*
59 * Parse a datetime of the form YYYYMMDDThhmmss and a string
60 * representation of the timezone in units of [+-]hhmm and load the
61 * struct tws.
62 */
63 static int
64 parse_datetime (const char *datetime, const char *zone, bool dst,
65 struct tws *tws)
66 {
67 char utc_indicator;
68 bool form_1;
69 int items_matched;
70
71 ZERO(tws);
72 items_matched =
73 sscanf (datetime, "%4d%2d%2dT%2d%2d%2d%c",
74 &tws->tw_year, &tws->tw_mon, &tws->tw_mday,
75 &tws->tw_hour, &tws->tw_min, &tws->tw_sec,
76 &utc_indicator);
77 tws->tw_flags = TW_NULL;
78
79 form_1 = false;
80 if (items_matched == 7) {
81 /* The 'Z' must be capital according to RFC 5545 Sec. 3.3.5. */
82 if (utc_indicator != 'Z') {
83 inform("%s has invalid timezone-indicator byte: %#x",
84 datetime, utc_indicator);
85 return NOTOK;
86 }
87 } else if (zone == NULL) {
88 form_1 = true;
89 }
90
91 /* items_matched of 3 is for, e.g., 20151230. Assume that means
92 the entire day. The time fields of the tws struct were
93 initialized to 0 by the memset() above. */
94 if (items_matched >= 6 || items_matched == 3) {
95 /* struct tws defines tw_mon over [0, 11]. */
96 --tws->tw_mon;
97
98 /* Fill out rest of tws, i.e., its tw_wday and tw_flags. */
99 set_dotw (tws);
100 /* set_dotw() sets TW_SIMP. Replace that with TW_SEXP so that
101 dasctime() outputs the dotw before the date instead of after. */
102 tws->tw_flags &= ~TW_SDAY;
103 tws->tw_flags |= TW_SEXP;
104
105 /* For the call to dmktime():
106 - don't need tw_yday
107 - tw_clock must be 0 on entry, and is set by dmktime()
108 - the only flag in tw_flags used is TW_DST
109 */
110 tws->tw_yday = tws->tw_clock = 0;
111 if (zone) {
112 int offset = atoi(zone);
113 tws->tw_zone = 60 * (offset / 100) + offset % 100;
114 } else
115 tws->tw_zone = 0;
116 if (dst) {
117 tws->tw_zone -= 60; /* per dlocaltime() */
118 tws->tw_flags |= TW_DST;
119 }
120 /* dmktime() just sets tws->tw_clock. */
121 (void) dmktime (tws);
122
123 if (! form_1) {
124 /* Set TW_SZEXP so that dasctime outputs timezone, except
125 with local time (Form #1). */
126 tws->tw_flags |= TW_SZEXP;
127
128 /* Convert UTC time to time in local timezone. However,
129 don't try for years before 1970 because dlocatime()
130 doesn't handle them well. dlocaltime() will succeed if
131 tws->tw_clock is nonzero. */
132 if (tws->tw_year >= 1970 && tws->tw_clock > 0) {
133 const int was_dst = tws->tw_flags & TW_DST;
134
135 *tws = *dlocaltime (&tws->tw_clock);
136 if (was_dst && ! (tws->tw_flags & TW_DST)) {
137 /* dlocaltime() changed the DST flag from 1 to 0,
138 which means the time is in the hour (assumed to
139 be one hour) that is lost in the transition to
140 DST. So per RFC 5545 Sec. 3.3.5, "the
141 DATE-TIME value is interpreted using the UTC
142 offset before the gap in local times." In
143 other words, add an hour to it.
144 No adjustment is necessary for the transition
145 from DST to standard time, because dasctime()
146 shows the first occurrence of the time. */
147 tws->tw_clock += 3600;
148 *tws = *dlocaltime (&tws->tw_clock);
149 }
150 }
151 }
152
153 return OK;
154 }
155
156 return NOTOK;
157 }
158
159 tzdesc_t
160 load_timezones (const contentline *clines)
161 {
162 tzdesc_t timezones = NULL, timezone = NULL;
163 bool in_vtimezone, in_standard, in_daylight;
164 tzparams *params = NULL;
165 const contentline *node;
166
167 /* Interpret each VTIMEZONE section. */
168 in_vtimezone = in_standard = in_daylight = false;
169 for (node = clines; node; node = node->next) {
170 /* node->name will be NULL if the line was "deleted". */
171 if (! node->name) { continue; }
172
173 if (in_daylight || in_standard) {
174 if (! strcasecmp ("END", node->name) &&
175 ((in_standard && ! strcasecmp ("STANDARD", node->value)) ||
176 (in_daylight && ! strcasecmp ("DAYLIGHT", node->value)))) {
177 struct tws tws;
178
179 if (in_standard) { in_standard = false; }
180 else if (in_daylight) { in_daylight = false; }
181
182 if (parse_datetime(params->dtstart, params->offsetfrom,
183 in_daylight, &tws) != OK) {
184 inform("failed to parse start time %s for %s",
185 params->dtstart,
186 in_daylight ? "daylight" : "standard");
187 return NULL;
188 }
189
190 if (tws.tw_year >= 1970) {
191 /* dmktime() falls apart for, e.g., the year 1601. */
192 params->start_dt = tws.tw_clock;
193 }
194 params = NULL;
195 } else if (! strcasecmp ("DTSTART", node->name)) {
196 /* Save DTSTART for use after getting TZOFFSETFROM. */
197 params->dtstart = node->value;
198 } else if (! strcasecmp ("TZOFFSETFROM", node->name)) {
199 params->offsetfrom = node->value;
200 } else if (! strcasecmp ("TZOFFSETTO", node->name)) {
201 params->offsetto = node->value;
202 } else if (! strcasecmp ("RRULE", node->name)) {
203 params->rrule = node->value;
204 }
205 } else if (in_vtimezone) {
206 if (! strcasecmp ("END", node->name) &&
207 ! strcasecmp ("VTIMEZONE", node->value)) {
208 in_vtimezone = false;
209 } else if (! strcasecmp ("BEGIN", node->name) &&
210 ! strcasecmp ("STANDARD", node->value)) {
211 in_standard = true;
212 params = &timezone->standard_params;
213 } else if (! strcasecmp ("BEGIN", node->name) &&
214 ! strcasecmp ("DAYLIGHT", node->value)) {
215 in_daylight = true;
216 params = &timezone->daylight_params;
217 } else if (! strcasecmp ("TZID", node->name)) {
218 /* See comment below in format_datetime() about removing any enclosing quotes from a
219 timezone identifier. */
220 char *buf = mh_xmalloc(strlen(node->value) + 1);
221 unquote_string(node->value, buf);
222 timezone->tzid = buf;
223 }
224 } else {
225 if (! strcasecmp ("BEGIN", node->name) &&
226 ! strcasecmp ("VTIMEZONE", node->value)) {
227
228 in_vtimezone = true;
229 NEW0(timezone);
230 if (timezones) {
231 tzdesc_t t;
232
233 for (t = timezones; t && t->next; t = t->next) { continue; }
234 /* The loop terminated at, not after, the last
235 timezones node. */
236 t->next = timezone;
237 } else {
238 timezones = timezone;
239 }
240 }
241 }
242 }
243
244 return timezones;
245 }
246
247 void
248 free_timezones (tzdesc_t timezone)
249 {
250 tzdesc_t next;
251
252 for ( ; timezone; timezone = next) {
253 free (timezone->tzid);
254 next = timezone->next;
255 free (timezone);
256 }
257 }
258
259 /*
260 * Convert time to local timezone, accounting for daylight saving time:
261 * - Detect which type of datetime the node contains:
262 * Form #1: DATE WITH LOCAL TIME
263 * Form #2: DATE WITH UTC TIME
264 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
265 * - Convert value to local time in seconds since epoch.
266 * - If there's a DST in the timezone, convert its start and end
267 * date-times to local time in seconds, also. Then determine
268 * if the value is between them, and therefore DST. Otherwise, it's
269 * not.
270 * - Format the time value.
271 */
272
273 /*
274 * Given a recurrence rule and year, calculate its time in seconds
275 * from 01 January UTC of the year.
276 */
277 static time_t
278 rrule_clock (const char *rrule, const char *starttime, const char *zone,
279 unsigned int year)
280 {
281 time_t clock = 0;
282
283 if (nmh_strcasestr (rrule, "FREQ=YEARLY;INTERVAL=1") ||
284 (nmh_strcasestr (rrule, "FREQ=YEARLY") && nmh_strcasestr(rrule, "INTERVAL") == NULL)) {
285 struct tws *tws;
286 const char *cp;
287 int wday = -1, month = -1;
288 int specific_day = 1; /* BYDAY integer (prefix) */
289 char buf[32];
290 int day;
291
292 if ((cp = nmh_strcasestr (rrule, "BYDAY="))) {
293 cp += 6;
294 /* BYDAY integers must be ASCII. */
295 if (*cp == '+') { ++cp; } /* +n specific day; don't support '-' */
296 else if (*cp == '-') { goto fail; }
297
298 if (isdigit ((unsigned char) *cp)) { specific_day = *cp++ - 0x30; }
299
300 if (! strncasecmp (cp, "SU", 2)) { wday = 0; }
301 else if (! strncasecmp (cp, "MO", 2)) { wday = 1; }
302 else if (! strncasecmp (cp, "TU", 2)) { wday = 2; }
303 else if (! strncasecmp (cp, "WE", 2)) { wday = 3; }
304 else if (! strncasecmp (cp, "TH", 2)) { wday = 4; }
305 else if (! strncasecmp (cp, "FR", 2)) { wday = 5; }
306 else if (! strncasecmp (cp, "SA", 2)) { wday = 6; }
307 }
308 if ((cp = nmh_strcasestr (rrule, "BYMONTH="))) {
309 month = atoi (cp + 8);
310 }
311
312 for (day = 1; day <= 7; ++day) {
313 /* E.g, 11-01-2014 02:00:00-0400 */
314 snprintf (buf, sizeof buf, "%02d-%02d-%04u %.2s:%.2s:%.2s%s",
315 month, day + 7 * (specific_day-1), year,
316 starttime, starttime + 2, starttime + 4,
317 zone ? zone : "0000");
318 if ((tws = dparsetime (buf))) {
319 if (! (tws->tw_flags & (TW_SEXP|TW_SIMP))) { set_dotw (tws); }
320
321 if (tws->tw_wday == wday) {
322 /* Found the day specified in the RRULE. */
323 break;
324 }
325 }
326 }
327
328 if (day <= 7) {
329 clock = tws->tw_clock;
330 }
331 }
332
333 fail:
334 if (clock == 0) {
335 inform("Unsupported RRULE format: %s, assume local timezone, continuing...",
336 rrule);
337 }
338
339 return clock;
340 }
341
342 char *
343 format_datetime (tzdesc_t timezones, const contentline *node)
344 {
345 param_list *p;
346 char *dt_timezone = NULL;
347 int dst = 0;
348 struct tws tws[2]; /* [standard, daylight] */
349 tzdesc_t tz;
350 char *tp_std, *tp_dst, *tp_dt;
351
352 /* Extract the timezone, if specified (RFC 5545 Sec. 3.3.5 Form #3). */
353 for (p = node->params; p && p->param_name; p = p->next) {
354 if (! strcasecmp (p->param_name, "TZID") && p->values) {
355 /* Remove any enclosing quotes from the timezone identifier. I don't believe that it's
356 legal for it to be quoted, according to RFC 5545 ยง 3.2.19:
357 tzidparam = "TZID" "=" [tzidprefix] paramtext
358 tzidprefix = "/"
359 where paramtext includes SAFE-CHAR, which specifically excludes DQUOTE. But we'll
360 be generous and strip quotes. */
361 char *buf = mh_xmalloc(strlen(p->values->value) + 1);
362 unquote_string(p->values->value, buf);
363 dt_timezone = buf;
364 break;
365 }
366 }
367
368 if (! dt_timezone) {
369 /* Form #1: DATE WITH LOCAL TIME, i.e., no time zone, or
370 Form #2: DATE WITH UTC TIME */
371 if (parse_datetime(node->value, NULL, false, &tws[0]) != OK) {
372 inform("unable to parse datetime %s", node->value);
373 return NULL;
374 }
375 return strdup (dasctime (&tws[0], 0));
376 }
377
378 /*
379 * must be
380 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
381 */
382
383 /* Find the corresponding tzdesc. */
384 for (tz = timezones; dt_timezone && tz; tz = tz->next) {
385 /* Property parameter values are case insensitive (RFC 5545
386 Sec. 2) and time zone identifiers are property parameters
387 (RFC 5545 Sec. 3.8.2.4), though it would seem odd to use
388 different case in the same file for identifiers that are
389 supposed to be the same. */
390 if (tz->tzid && ! strcasecmp (dt_timezone, tz->tzid)) { break; }
391 }
392
393 if (!tz) {
394 inform("did not find VTIMEZONE section for %s", dt_timezone);
395 free(dt_timezone);
396 return NULL;
397 }
398 free(dt_timezone);
399
400 /* Determine if it's Daylight Saving. */
401 tp_std = strchr (tz->standard_params.dtstart, 'T');
402 tp_dt = strchr (node->value, 'T');
403
404 if (tz->daylight_params.dtstart) {
405 tp_dst = strchr (tz->daylight_params.dtstart, 'T');
406 } else {
407 /* No DAYLIGHT section. */
408 tp_dst = NULL;
409 dst = 0;
410 }
411
412 if (tp_std && tp_dt) {
413 time_t transition[2] = { 0, 0 }; /* [standard, daylight] */
414 time_t dt[2]; /* [standard, daylight] */
415 unsigned int year;
416 char buf[5];
417
418 /* Datetime is form YYYYMMDDThhmmss. Extract year. */
419 memcpy (buf, node->value, sizeof buf - 1);
420 buf[sizeof buf - 1] = '\0';
421 year = atoi (buf);
422
423 if (tz->standard_params.rrule) {
424 /* +1 to skip the T before the time */
425 transition[0] =
426 rrule_clock (tz->standard_params.rrule, tp_std + 1,
427 tz->standard_params.offsetfrom, year);
428 }
429 if (tp_dst && tz->daylight_params.rrule) {
430 /* +1 to skip the T before the time */
431 transition[1] =
432 rrule_clock (tz->daylight_params.rrule, tp_dst + 1,
433 tz->daylight_params.offsetfrom, year);
434 }
435
436 if (transition[0] < transition[1]) {
437 inform("format_datetime() requires that daylight "
438 "saving time transition precede standard time "
439 "transition");
440 return NULL;
441 }
442
443 if (parse_datetime(node->value, tz->standard_params.offsetto,
444 false, &tws[0]) != OK) {
445 inform("unable to parse datetime %s", node->value);
446 return NULL;
447 }
448 dt[0] = tws[0].tw_clock;
449
450 if (tp_dst) {
451 if (dt[0] < transition[1]) {
452 dst = 0;
453 } else {
454 if (parse_datetime(node->value,
455 tz->daylight_params.offsetto, true, &tws[1]) != OK) {
456 inform("unable to parse datetime %s", node->value);
457 return NULL;
458 }
459 dt[1] = tws[1].tw_clock;
460 dst = dt[1] <= transition[0];
461 }
462 }
463
464 if (dst) {
465 if (tz->daylight_params.start_dt > 0 &&
466 dt[dst] < tz->daylight_params.start_dt) {
467 inform("date-time of %s is before VTIMEZONE start "
468 "of %s", node->value,
469 tz->daylight_params.dtstart);
470 return NULL;
471 }
472 } else {
473 if (tz->standard_params.start_dt > 0 &&
474 dt[dst] < tz->standard_params.start_dt) {
475 inform("date-time of %s is before VTIMEZONE start "
476 "of %s", node->value,
477 tz->standard_params.dtstart);
478 return NULL;
479 }
480 }
481 } else {
482 if (! tp_std) {
483 inform("unsupported date-time format: %s",
484 tz->standard_params.dtstart);
485 return NULL;
486 }
487 if (! tp_dt) {
488 inform("unsupported date-time format: %s", node->value);
489 return NULL;
490 }
491 }
492
493 return strdup (dasctime (&tws[dst], 0));
494 }