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