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