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