1 /* date.c: date parsing for Subversion
3 * ====================================================================
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
20 * ====================================================================
24 #include "svn_error.h"
25 #include "svn_string.h"
27 #include "svn_private_config.h"
28 #include "private/svn_token.h"
30 /* Valid rule actions */
32 ACCUM, /* Accumulate a decimal value */
33 MICRO, /* Accumulate microseconds */
34 TZIND, /* Handle +, -, Z */
35 NOOP, /* Do nothing */
36 SKIPFROM, /* If at end-of-value, accept the match. Otherwise,
37 if the next template character matches the current
38 value character, continue processing as normal.
39 Otherwise, attempt to complete matching starting
40 immediately after the first subsequent occurrance of
41 ']' in the template. */
42 SKIP, /* Ignore this template character */
43 ACCEPT /* Accept the value */
46 /* How to handle a particular character in a template */
49 char key; /* The template char that this rule matches */
50 const char *valid; /* String of valid chars for this rule */
51 enum rule_action action; /* What action to take when the rule is matched */
52 int offset; /* Where to store the any results of the action,
53 expressed in terms of bytes relative to the
54 base of a match_state object. */
57 /* The parsed values, before localtime/gmt processing */
58 typedef struct match_state
62 apr_int32_t offminutes;
65 #define DIGITS "0123456789"
67 /* A declarative specification of how each template character
68 should be processed, using a rule for each valid symbol. */
72 { 'Y', DIGITS, ACCUM, APR_OFFSETOF(match_state, base.tm_year) },
73 { 'M', DIGITS, ACCUM, APR_OFFSETOF(match_state, base.tm_mon) },
74 { 'D', DIGITS, ACCUM, APR_OFFSETOF(match_state, base.tm_mday) },
75 { 'h', DIGITS, ACCUM, APR_OFFSETOF(match_state, base.tm_hour) },
76 { 'm', DIGITS, ACCUM, APR_OFFSETOF(match_state, base.tm_min) },
77 { 's', DIGITS, ACCUM, APR_OFFSETOF(match_state, base.tm_sec) },
78 { 'u', DIGITS, MICRO, APR_OFFSETOF(match_state, base.tm_usec) },
79 { 'O', DIGITS, ACCUM, APR_OFFSETOF(match_state, offhours) },
80 { 'o', DIGITS, ACCUM, APR_OFFSETOF(match_state, offminutes) },
81 { '+', "-+", TZIND, 0 },
82 { 'Z', "Z", TZIND, 0 },
83 { ':', ":", NOOP, 0 },
84 { '-', "-", NOOP, 0 },
85 { 'T', "T", NOOP, 0 },
86 { ' ', " ", NOOP, 0 },
87 { '.', ".,", NOOP, 0 },
88 { '[', NULL, SKIPFROM, 0 },
89 { ']', NULL, SKIP, 0 },
90 { '\0', NULL, ACCEPT, 0 },
93 /* Return the rule associated with TCHAR, or NULL if there
98 int i = sizeof(rules)/sizeof(rules[0]);
100 if (rules[i].key == tchar)
105 /* Attempt to match the date-string in VALUE to the provided TEMPLATE,
106 using the rules defined above. Return TRUE on successful match,
107 FALSE otherwise. On successful match, fill in *EXP with the
108 matched values and set *LOCALTZ to TRUE if the local time zone
109 should be used to interpret the match (i.e. if no time zone
110 information was provided), or FALSE if not. */
112 template_match(apr_time_exp_t *expt, svn_boolean_t *localtz,
113 const char *template, const char *value)
115 int multiplier = 100000;
118 char *base = (char *)&ms;
120 memset(&ms, 0, sizeof(ms));
124 const rule *match = find_rule(*template++);
125 char vchar = *value++;
128 if (!match || (match->valid
129 && (!vchar || !strchr(match->valid, vchar))))
132 /* Compute the address of memory location affected by this
133 rule by adding match->offset bytes to the address of ms.
134 Because this is a byte-quantity, it is necessary to cast
136 place = (apr_int32_t *)(base + match->offset);
137 switch (match->action)
140 *place = *place * 10 + vchar - '0';
143 *place += (vchar - '0') * multiplier;
157 match = find_rule(*template);
158 if (!strchr(match->valid, vchar))
159 template = strchr(template, ']') + 1;
171 /* Validate gmt offset here, since we can't reliably do it later. */
172 if (ms.offhours > 23 || ms.offminutes > 59)
175 /* tzind will be '+' or '-' for an explicit time zone, 'Z' to
176 indicate UTC, or 0 to indicate local time. */
180 ms.base.tm_gmtoff = ms.offhours * 3600 + ms.offminutes * 60;
183 ms.base.tm_gmtoff = -(ms.offhours * 3600 + ms.offminutes * 60);
188 *localtz = (tzind == 0);
192 static struct unit_words_table {
195 } unit_words_table[] = {
196 /* Word matching does not concern itself with exact days of the month
197 * or leap years so these amounts are always fixed. */
198 { "years", apr_time_from_sec(60 * 60 * 24 * 365) },
199 { "months", apr_time_from_sec(60 * 60 * 24 * 30) },
200 { "weeks", apr_time_from_sec(60 * 60 * 24 * 7) },
201 { "days", apr_time_from_sec(60 * 60 * 24) },
202 { "hours", apr_time_from_sec(60 * 60) },
203 { "minutes", apr_time_from_sec(60) },
204 { "mins", apr_time_from_sec(60) },
208 static svn_token_map_t number_words_map[] = {
209 { "zero", 0 }, { "one", 1 }, { "two", 2 }, { "three", 3 }, { "four", 4 },
210 { "five", 5 }, { "six", 6 }, { "seven", 7 }, { "eight", 8 }, { "nine", 9 },
211 { "ten", 10 }, { "eleven", 11 }, { "twelve", 12 }, { NULL, 0 }
214 /* Attempt to match the date-string in TEXT according to the following rules:
216 * "N years|months|weeks|days|hours|minutes ago" resolve to the most recent
217 * revision prior to the specified time. N may either be a word from
218 * NUMBER_WORDS_TABLE defined above, or a non-negative digit.
220 * Return TRUE on successful match, FALSE otherwise. On successful match,
221 * fill in *EXP with the matched value and set *LOCALTZ to TRUE (this
222 * function always uses local time). Use POOL for temporary allocations. */
224 words_match(apr_time_exp_t *expt, svn_boolean_t *localtz,
225 apr_time_t now, const char *text, apr_pool_t *pool)
229 apr_array_header_t *words;
232 const char *unit_str;
234 words = svn_cstring_split(text, " ", TRUE /* chop_whitespace */, pool);
236 if (words->nelts != 3)
239 word = APR_ARRAY_IDX(words, 0, const char *);
241 /* Try to parse a number word. */
242 n = svn_token__from_word(number_words_map, word);
244 if (n == SVN_TOKEN_UNKNOWN)
248 /* Try to parse a digit. */
249 err = svn_cstring_atoi(&n, word);
252 svn_error_clear(err);
259 /* Try to parse a unit. */
260 word = APR_ARRAY_IDX(words, 1, const char *);
261 for (i = 0, unit_str = unit_words_table[i].word;
262 unit_str = unit_words_table[i].word, unit_str != NULL; i++)
264 /* Tolerate missing trailing 's' from unit. */
265 if (!strcmp(word, unit_str) ||
266 !strncmp(word, unit_str, strlen(unit_str) - 1))
268 t = now - (n * unit_words_table[i].value);
276 /* Require trailing "ago". */
277 word = APR_ARRAY_IDX(words, 2, const char *);
278 if (strcmp(word, "ago"))
281 if (apr_time_exp_lt(expt, t) != APR_SUCCESS)
289 valid_days_by_month[] = {
296 svn_parse_date(svn_boolean_t *matched, apr_time_t *result, const char *text,
297 apr_time_t now, apr_pool_t *pool)
299 apr_time_exp_t expt, expnow;
300 apr_status_t apr_err;
301 svn_boolean_t localtz;
305 apr_err = apr_time_exp_lt(&expnow, now);
306 if (apr_err != APR_SUCCESS)
307 return svn_error_wrap_apr(apr_err, _("Can't manipulate current date"));
309 if (template_match(&expt, &localtz, /* ISO-8601 extended, date only */
312 || template_match(&expt, &localtz, /* ISO-8601 extended, UTC */
313 "YYYY-M[M]-D[D]Th[h]:mm[:ss[.u[u[u[u[u[u][Z]",
315 || template_match(&expt, &localtz, /* ISO-8601 extended, with offset */
316 "YYYY-M[M]-D[D]Th[h]:mm[:ss[.u[u[u[u[u[u]+OO[:oo]",
318 || template_match(&expt, &localtz, /* ISO-8601 basic, date only */
321 || template_match(&expt, &localtz, /* ISO-8601 basic, UTC */
322 "YYYYMMDDThhmm[ss[.u[u[u[u[u[u][Z]",
324 || template_match(&expt, &localtz, /* ISO-8601 basic, with offset */
325 "YYYYMMDDThhmm[ss[.u[u[u[u[u[u]+OO[oo]",
327 || template_match(&expt, &localtz, /* "svn log" format */
328 "YYYY-M[M]-D[D] h[h]:mm[:ss[.u[u[u[u[u[u][ +OO[oo]",
330 || template_match(&expt, &localtz, /* GNU date's iso-8601 */
331 "YYYY-M[M]-D[D]Th[h]:mm[:ss[.u[u[u[u[u[u]+OO[oo]",
334 expt.tm_year -= 1900;
337 else if (template_match(&expt, &localtz, /* Just a time */
338 "h[h]:mm[:ss[.u[u[u[u[u[u]",
341 expt.tm_year = expnow.tm_year;
342 expt.tm_mon = expnow.tm_mon;
343 expt.tm_mday = expnow.tm_mday;
345 else if (!words_match(&expt, &localtz, now, text, pool))
348 /* Range validation, allowing for leap seconds */
349 if (expt.tm_mon < 0 || expt.tm_mon > 11
350 || expt.tm_mday > valid_days_by_month[expt.tm_mon]
357 /* february/leap-year day checking. tm_year is bias-1900, so centuries
358 that equal 100 (mod 400) are multiples of 400. */
360 && expt.tm_mday == 29
361 && (expt.tm_year % 4 != 0
362 || (expt.tm_year % 100 == 0 && expt.tm_year % 400 != 100)))
367 apr_time_t candidate;
368 apr_time_exp_t expthen;
370 /* We need to know the GMT offset of the requested time, not the
371 current time. In some cases, that quantity is ambiguous,
372 since at the end of daylight saving's time, an hour's worth
373 of local time happens twice. For those cases, we should
374 prefer DST if we are currently in DST, and standard time if
375 not. So, calculate the time value using the current time's
376 GMT offset and use the GMT offset of the resulting time. */
377 expt.tm_gmtoff = expnow.tm_gmtoff;
378 apr_err = apr_time_exp_gmt_get(&candidate, &expt);
379 if (apr_err != APR_SUCCESS)
380 return svn_error_wrap_apr(apr_err,
381 _("Can't calculate requested date"));
382 apr_err = apr_time_exp_lt(&expthen, candidate);
383 if (apr_err != APR_SUCCESS)
384 return svn_error_wrap_apr(apr_err, _("Can't expand time"));
385 expt.tm_gmtoff = expthen.tm_gmtoff;
387 apr_err = apr_time_exp_gmt_get(result, &expt);
388 if (apr_err != APR_SUCCESS)
389 return svn_error_wrap_apr(apr_err, _("Can't calculate requested date"));