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