diff options
author | Karel Zak | 2017-03-23 12:36:47 +0100 |
---|---|---|
committer | Karel Zak | 2017-03-23 12:36:47 +0100 |
commit | 7e26832d706ee73af7f85f36581c5f8ba089cbb1 (patch) | |
tree | d3abdd331599bfaed19187b9b9350cbbba65e981 | |
parent | Fix menu spelling in cfdisk debug messages (diff) | |
parent | lib: add parse-date documentation (diff) | |
download | kernel-qcow2-util-linux-7e26832d706ee73af7f85f36581c5f8ba089cbb1.tar.gz kernel-qcow2-util-linux-7e26832d706ee73af7f85f36581c5f8ba089cbb1.tar.xz kernel-qcow2-util-linux-7e26832d706ee73af7f85f36581c5f8ba089cbb1.zip |
Merge remote-tracking branch 'jwpi/hwclock-date7'
* jwpi/hwclock-date7:
lib: add parse-date documentation
hwclock: use parse_date function
build-sys: add parse-date.y
lib: add parse-date.y
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Documentation/parse-date.txt | 468 | ||||
-rwxr-xr-x | autogen.sh | 20 | ||||
-rw-r--r-- | configure.ac | 2 | ||||
-rw-r--r-- | include/cctype.h | 325 | ||||
-rw-r--r-- | include/timeutils.h | 2 | ||||
-rw-r--r-- | lib/.gitignore | 1 | ||||
-rw-r--r-- | lib/Makemodule.am | 1 | ||||
-rw-r--r-- | lib/parse-date.y | 1645 | ||||
-rw-r--r-- | sys-utils/hwclock.8.in | 22 | ||||
-rw-r--r-- | sys-utils/hwclock.c | 104 |
11 files changed, 2480 insertions, 111 deletions
diff --git a/.gitignore b/.gitignore index 8630feacf..6c87c6be4 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ test-suite.log tests/run.sh.log tests/run.sh.trs update.log +ylwrap # # binaries diff --git a/Documentation/parse-date.txt b/Documentation/parse-date.txt new file mode 100644 index 000000000..cb06a4722 --- /dev/null +++ b/Documentation/parse-date.txt @@ -0,0 +1,468 @@ + +NAME + parse_date - parses a date string into a timespec struct. + +SYNOPSIS + #include "timeutils.h" + + int parse_date(struct timespec *result, char const *p, + struct timespec const *now) + + LDADD libcommon.la + +DESCRIPTION + Parse a date/time string, storing the resulting time value into *result. + The string itself is pointed to by *p. Return 1 if successful. + *p can be an incomplete or relative time specification; if so, use + *now as the basis for the returned time. + + +This function is based upon gnulib's parse-datetime.y-dd7a871. + +Below is a plain text version of the gnulib parse-datetime.texi-dd7a871 manual +describing the input strings that are recognized. + +Any future modifications to the util-linux parser that affect input strings +should be noted below. + + +1 Date input formats +******************** + +First, a quote: + + Our units of temporal measurement, from seconds on up to months, + are so complicated, asymmetrical and disjunctive so as to make + coherent mental reckoning in time all but impossible. Indeed, had + some tyrannical god contrived to enslave our minds to time, to + make it all but impossible for us to escape subjection to sodden + routines and unpleasant surprises, he could hardly have done + better than handing down our present system. It is like a set of + trapezoidal building blocks, with no vertical or horizontal + surfaces, like a language in which the simplest thought demands + ornate constructions, useless particles and lengthy + circumlocutions. Unlike the more successful patterns of language + and science, which enable us to face experience boldly or at least + level-headedly, our system of temporal calculation silently and + persistently encourages our terror of time. + + ... It is as though architects had to measure length in feet, + width in meters and height in ells; as though basic instruction + manuals demanded a knowledge of five different languages. It is + no wonder then that we often look into our own immediate past or + future, last Tuesday or a week from Sunday, with feelings of + helpless confusion. ... + + --Robert Grudin, `Time and the Art of Living'. + + This section describes the textual date representations that GNU +programs accept. These are the strings you, as a user, can supply as +arguments to the various programs. The C interface (via the +`parse_datetime' function) is not described here. + +1.1 General date syntax +======================= + +A "date" is a string, possibly empty, containing many items separated +by whitespace. The whitespace may be omitted when no ambiguity arises. +The empty string means the beginning of today (i.e., midnight). Order +of the items is immaterial. A date string may contain many flavors of +items: + + * calendar date items + + * time of day items + + * time zone items + + * combined date and time of day items + + * day of the week items + + * relative items + + * pure numbers. + +We describe each of these item types in turn, below. + + A few ordinal numbers may be written out in words in some contexts. +This is most useful for specifying day of the week items or relative +items (see below). Among the most commonly used ordinal numbers, the +word `last' stands for -1, `this' stands for 0, and `first' and `next' +both stand for 1. Because the word `second' stands for the unit of +time there is no way to write the ordinal number 2, but for convenience +`third' stands for 3, `fourth' for 4, `fifth' for 5, `sixth' for 6, +`seventh' for 7, `eighth' for 8, `ninth' for 9, `tenth' for 10, +`eleventh' for 11 and `twelfth' for 12. + + When a month is written this way, it is still considered to be +written numerically, instead of being "spelled in full"; this changes +the allowed strings. + + In the current implementation, only English is supported for words +and abbreviations like `AM', `DST', `EST', `first', `January', +`Sunday', `tomorrow', and `year'. + + The output of the `date' command is not always acceptable as a date +string, not only because of the language problem, but also because +there is no standard meaning for time zone items like `IST'. When using +`date' to generate a date string intended to be parsed later, specify a +date format that is independent of language and that does not use time +zone items other than `UTC' and `Z'. Here are some ways to do this: + + $ LC_ALL=C TZ=UTC0 date + Mon Mar 1 00:21:42 UTC 2004 + $ TZ=UTC0 date +'%Y-%m-%d %H:%M:%SZ' + 2004-03-01 00:21:42Z + $ date --rfc-3339=ns # --rfc-3339 is a GNU extension. + 2004-02-29 16:21:42.692722128-08:00 + $ date --rfc-2822 # a GNU extension + Sun, 29 Feb 2004 16:21:42 -0800 + $ date +'%Y-%m-%d %H:%M:%S %z' # %z is a GNU extension. + 2004-02-29 16:21:42 -0800 + $ date +'@%s.%N' # %s and %N are GNU extensions. + @1078100502.692722128 + + Alphabetic case is completely ignored in dates. Comments may be +introduced between round parentheses, as long as included parentheses +are properly nested. Hyphens not followed by a digit are currently +ignored. Leading zeros on numbers are ignored. + + Invalid dates like `2005-02-29' or times like `24:00' are rejected. +In the typical case of a host that does not support leap seconds, a +time like `23:59:60' is rejected even if it corresponds to a valid leap +second. + +1.2 Calendar date items +======================= + +A "calendar date item" specifies a day of the year. It is specified +differently, depending on whether the month is specified numerically or +literally. All these strings specify the same calendar date: + + 1972-09-24 # ISO 8601. + 72-9-24 # Assume 19xx for 69 through 99, + # 20xx for 00 through 68. + 72-09-24 # Leading zeros are ignored. + 9/24/72 # Common U.S. writing. + 24 September 1972 + 24 Sept 72 # September has a special abbreviation. + 24 Sep 72 # Three-letter abbreviations always allowed. + Sep 24, 1972 + 24-sep-72 + 24sep72 + + The year can also be omitted. In this case, the last specified year +is used, or the current year if none. For example: + + 9/24 + sep 24 + + Here are the rules. + + For numeric months, the ISO 8601 format `YEAR-MONTH-DAY' is allowed, +where YEAR is any positive number, MONTH is a number between 01 and 12, +and DAY is a number between 01 and 31. A leading zero must be present +if a number is less than ten. If YEAR is 68 or smaller, then 2000 is +added to it; otherwise, if YEAR is less than 100, then 1900 is added to +it. The construct `MONTH/DAY/YEAR', popular in the United States, is +accepted. Also `MONTH/DAY', omitting the year. + + Literal months may be spelled out in full: `January', `February', +`March', `April', `May', `June', `July', `August', `September', +`October', `November' or `December'. Literal months may be abbreviated +to their first three letters, possibly followed by an abbreviating dot. +It is also permitted to write `Sept' instead of `September'. + + When months are written literally, the calendar date may be given as +any of the following: + + DAY MONTH YEAR + DAY MONTH + MONTH DAY YEAR + DAY-MONTH-YEAR + + Or, omitting the year: + + MONTH DAY + +1.3 Time of day items +===================== + +A "time of day item" in date strings specifies the time on a given day. +Here are some examples, all of which represent the same time: + + 20:02:00.000000 + 20:02 + 8:02pm + 20:02-0500 # In EST (U.S. Eastern Standard Time). + + More generally, the time of day may be given as +`HOUR:MINUTE:SECOND', where HOUR is a number between 0 and 23, MINUTE +is a number between 0 and 59, and SECOND is a number between 0 and 59 +possibly followed by `.' or `,' and a fraction containing one or more +digits. Alternatively, `:SECOND' can be omitted, in which case it is +taken to be zero. On the rare hosts that support leap seconds, SECOND +may be 60. + + If the time is followed by `am' or `pm' (or `a.m.' or `p.m.'), HOUR +is restricted to run from 1 to 12, and `:MINUTE' may be omitted (taken +to be zero). `am' indicates the first half of the day, `pm' indicates +the second half of the day. In this notation, 12 is the predecessor of +1: midnight is `12am' while noon is `12pm'. (This is the zero-oriented +interpretation of `12am' and `12pm', as opposed to the old tradition +derived from Latin which uses `12m' for noon and `12pm' for midnight.) + + The time may alternatively be followed by a time zone correction, +expressed as `SHHMM', where S is `+' or `-', HH is a number of zone +hours and MM is a number of zone minutes. The zone minutes term, MM, +may be omitted, in which case the one- or two-digit correction is +interpreted as a number of hours. You can also separate HH from MM +with a colon. When a time zone correction is given this way, it forces +interpretation of the time relative to Coordinated Universal Time +(UTC), overriding any previous specification for the time zone or the +local time zone. For example, `+0530' and `+05:30' both stand for the +time zone 5.5 hours ahead of UTC (e.g., India). This is the best way to +specify a time zone correction by fractional parts of an hour. The +maximum zone correction is 24 hours. + + Either `am'/`pm' or a time zone correction may be specified, but not +both. + +1.4 Time zone items +=================== + +A "time zone item" specifies an international time zone, indicated by a +small set of letters, e.g., `UTC' or `Z' for Coordinated Universal +Time. Any included periods are ignored. By following a +non-daylight-saving time zone by the string `DST' in a separate word +(that is, separated by some white space), the corresponding daylight +saving time zone may be specified. Alternatively, a +non-daylight-saving time zone can be followed by a time zone +correction, to add the two values. This is normally done only for +`UTC'; for example, `UTC+05:30' is equivalent to `+05:30'. + + Time zone items other than `UTC' and `Z' are obsolescent and are not +recommended, because they are ambiguous; for example, `EST' has a +different meaning in Australia than in the United States. Instead, +it's better to use unambiguous numeric time zone corrections like +`-0500', as described in the previous section. + + If neither a time zone item nor a time zone correction is supplied, +timestamps are interpreted using the rules of the default time zone +(*note Specifying time zone rules::). + +1.5 Combined date and time of day items +======================================= + +The ISO 8601 date and time of day extended format consists of an ISO +8601 date, a `T' character separator, and an ISO 8601 time of day. +This format is also recognized if the `T' is replaced by a space. + + In this format, the time of day should use 24-hour notation. +Fractional seconds are allowed, with either comma or period preceding +the fraction. ISO 8601 fractional minutes and hours are not supported. +Typically, hosts support nanosecond timestamp resolution; excess +precision is silently discarded. + + Here are some examples: + + 2012-09-24T20:02:00.052-05:00 + 2012-12-31T23:59:59,999999999+11:00 + 1970-01-01 00:00Z + +1.6 Day of week items +===================== + +The explicit mention of a day of the week will forward the date (only +if necessary) to reach that day of the week in the future. + + Days of the week may be spelled out in full: `Sunday', `Monday', +`Tuesday', `Wednesday', `Thursday', `Friday' or `Saturday'. Days may +be abbreviated to their first three letters, optionally followed by a +period. The special abbreviations `Tues' for `Tuesday', `Wednes' for +`Wednesday' and `Thur' or `Thurs' for `Thursday' are also allowed. + + A number may precede a day of the week item to move forward +supplementary weeks. It is best used in expression like `third +monday'. In this context, `last DAY' or `next DAY' is also acceptable; +they move one week before or after the day that DAY by itself would +represent. + + A comma following a day of the week item is ignored. + +1.7 Relative items in date strings +================================== + +"Relative items" adjust a date (or the current date if none) forward or +backward. The effects of relative items accumulate. Here are some +examples: + + 1 year + 1 year ago + 3 years + 2 days + + The unit of time displacement may be selected by the string `year' +or `month' for moving by whole years or months. These are fuzzy units, +as years and months are not all of equal duration. More precise units +are `fortnight' which is worth 14 days, `week' worth 7 days, `day' +worth 24 hours, `hour' worth 60 minutes, `minute' or `min' worth 60 +seconds, and `second' or `sec' worth one second. An `s' suffix on +these units is accepted and ignored. + + The unit of time may be preceded by a multiplier, given as an +optionally signed number. Unsigned numbers are taken as positively +signed. No number at all implies 1 for a multiplier. Following a +relative item by the string `ago' is equivalent to preceding the unit +by a multiplier with value -1. + + The string `tomorrow' is worth one day in the future (equivalent to +`day'), the string `yesterday' is worth one day in the past (equivalent +to `day ago'). + + The strings `now' or `today' are relative items corresponding to +zero-valued time displacement, these strings come from the fact a +zero-valued time displacement represents the current time when not +otherwise changed by previous items. They may be used to stress other +items, like in `12:00 today'. The string `this' also has the meaning +of a zero-valued time displacement, but is preferred in date strings +like `this thursday'. + + When a relative item causes the resulting date to cross a boundary +where the clocks were adjusted, typically for daylight saving time, the +resulting date and time are adjusted accordingly. + + The fuzz in units can cause problems with relative items. For +example, `2003-07-31 -1 month' might evaluate to 2003-07-01, because +2003-06-31 is an invalid date. To determine the previous month more +reliably, you can ask for the month before the 15th of the current +month. For example: + + $ date -R + Thu, 31 Jul 2003 13:02:39 -0700 + $ date --date='-1 month' +'Last month was %B?' + Last month was July? + $ date --date="$(date +%Y-%m-15) -1 month" +'Last month was %B!' + Last month was June! + + Also, take care when manipulating dates around clock changes such as +daylight saving leaps. In a few cases these have added or subtracted +as much as 24 hours from the clock, so it is often wise to adopt +universal time by setting the `TZ' environment variable to `UTC0' +before embarking on calendrical calculations. + +1.8 Pure numbers in date strings +================================ + +The precise interpretation of a pure decimal number depends on the +context in the date string. + + If the decimal number is of the form YYYYMMDD and no other calendar +date item (*note Calendar date items::) appears before it in the date +string, then YYYY is read as the year, MM as the month number and DD as +the day of the month, for the specified calendar date. + + If the decimal number is of the form HHMM and no other time of day +item appears before it in the date string, then HH is read as the hour +of the day and MM as the minute of the hour, for the specified time of +day. MM can also be omitted. + + If both a calendar date and a time of day appear to the left of a +number in the date string, but no relative item, then the number +overrides the year. + +1.9 Seconds since the Epoch +=========================== + +If you precede a number with `@', it represents an internal timestamp +as a count of seconds. The number can contain an internal decimal +point (either `.' or `,'); any excess precision not supported by the +internal representation is truncated toward minus infinity. Such a +number cannot be combined with any other date item, as it specifies a +complete timestamp. + + Internally, computer times are represented as a count of seconds +since an epoch--a well-defined point of time. On GNU and POSIX +systems, the epoch is 1970-01-01 00:00:00 UTC, so `@0' represents this +time, `@1' represents 1970-01-01 00:00:01 UTC, and so forth. GNU and +most other POSIX-compliant systems support such times as an extension +to POSIX, using negative counts, so that `@-1' represents 1969-12-31 +23:59:59 UTC. + + Traditional Unix systems count seconds with 32-bit two's-complement +integers and can represent times from 1901-12-13 20:45:52 through +2038-01-19 03:14:07 UTC. More modern systems use 64-bit counts of +seconds with nanosecond subcounts, and can represent all the times in +the known lifetime of the universe to a resolution of 1 nanosecond. + + On most hosts, these counts ignore the presence of leap seconds. +For example, on most hosts `@915148799' represents 1998-12-31 23:59:59 +UTC, `@915148800' represents 1999-01-01 00:00:00 UTC, and there is no +way to represent the intervening leap second 1998-12-31 23:59:60 UTC. + +1.10 Specifying time zone rules +=============================== + +Normally, dates are interpreted using the rules of the current time +zone, which in turn are specified by the `TZ' environment variable, or +by a system default if `TZ' is not set. To specify a different set of +default time zone rules that apply just to one date, start the date +with a string of the form `TZ="RULE"'. The two quote characters (`"') +must be present in the date, and any quotes or backslashes within RULE +must be escaped by a backslash. + + For example, with the GNU `date' command you can answer the question +"What time is it in New York when a Paris clock shows 6:30am on October +31, 2004?" by using a date beginning with `TZ="Europe/Paris"' as shown +in the following shell transcript: + + $ export TZ="America/New_York" + $ date --date='TZ="Europe/Paris" 2004-10-31 06:30' + Sun Oct 31 01:30:00 EDT 2004 + + In this example, the `--date' operand begins with its own `TZ' +setting, so the rest of that operand is processed according to +`Europe/Paris' rules, treating the string `2004-10-31 06:30' as if it +were in Paris. However, since the output of the `date' command is +processed according to the overall time zone rules, it uses New York +time. (Paris was normally six hours ahead of New York in 2004, but +this example refers to a brief Halloween period when the gap was five +hours.) + + A `TZ' value is a rule that typically names a location in the `tz' +database (http://www.twinsun.com/tz/tz-link.htm). A recent catalog of +location names appears in the TWiki Date and Time Gateway +(http://twiki.org/cgi-bin/xtra/tzdate). A few non-GNU hosts require a +colon before a location name in a `TZ' setting, e.g., +`TZ=":America/New_York"'. + + The `tz' database includes a wide variety of locations ranging from +`Arctic/Longyearbyen' to `Antarctica/South_Pole', but if you are at sea +and have your own private time zone, or if you are using a non-GNU host +that does not support the `tz' database, you may need to use a POSIX +rule instead. Simple POSIX rules like `UTC0' specify a time zone +without daylight saving time; other rules can specify simple daylight +saving regimes. *Note Specifying the Time Zone with `TZ': (libc)TZ +Variable. + +1.11 Authors of `parse_datetime' +================================ + +`parse_datetime' started life as `getdate', as originally implemented +by Steven M. Bellovin (<smb@research.att.com>) while at the University +of North Carolina at Chapel Hill. The code was later tweaked by a +couple of people on Usenet, then completely overhauled by Rich $alz +(<rsalz@bbn.com>) and Jim Berets (<jberets@bbn.com>) in August, 1990. +Various revisions for the GNU system were made by David MacKenzie, Jim +Meyering, Paul Eggert and others, including renaming it to `get_date' to +avoid a conflict with the alternative Posix function `getdate', and a +later rename to `parse_datetime'. The Posix function `getdate' can +parse more locale-specific dates using `strptime', but relies on an +environment variable and external file, and lacks the thread-safety of +`parse_datetime'. + + This chapter was originally produced by Franc,ois Pinard +(<pinard@iro.umontreal.ca>) from the `parse_datetime.y' source code, +and then edited by K. Berry (<kb@cs.umb.edu>). + diff --git a/autogen.sh b/autogen.sh index 116885bc7..04b6cb459 100755 --- a/autogen.sh +++ b/autogen.sh @@ -67,6 +67,25 @@ test -f sys-utils/mount.c || { DIE=1 } +if ! (bison --version) < /dev/null > /dev/null 2>&1; then + echo + echo "You must have bison installed to build the util-linux." + echo + DIE=1 +else + lexver=$(bison --version | awk '/bison \(GNU Bison\)/ { print $4 }') + case "$lexver" in + [2-9].*) + ;; + *) + echo + echo "You must have bison version >= 2.x, but you have $lexver." + echo + DIE=1 + ;; + esac +fi + LIBTOOLIZE=libtoolize case `uname` in Darwin*) LIBTOOLIZE=glibtoolize ;; esac if ! ($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1; then @@ -104,6 +123,7 @@ echo " autoconf: $(autoconf --version | head -1)" echo " autoheader: $(autoheader --version | head -1)" echo " automake: $(automake --version | head -1)" echo " libtoolize: $($LIBTOOLIZE --version | head -1)" +echo " bison: $(bison --version | head -1)" rm -rf autom4te.cache diff --git a/configure.ac b/configure.ac index 8933afa6f..17c3b23e7 100644 --- a/configure.ac +++ b/configure.ac @@ -99,6 +99,7 @@ AC_SUBST([usrlib_execdir]) AM_PROG_CC_C_O AC_PROG_MKDIR_P AC_PROG_CC_STDC +AC_PROG_YACC AC_CANONICAL_HOST AC_C_CONST AC_C_VOLATILE @@ -153,7 +154,6 @@ PKG_PROG_PKG_CONFIG GTK_DOC_CHECK([1.10]) AC_PATH_PROG([XSLTPROC], [xsltproc]) - linux_os=no bsd_os=no AS_CASE([${host_os}], diff --git a/include/cctype.h b/include/cctype.h new file mode 100644 index 000000000..6ab644c6e --- /dev/null +++ b/include/cctype.h @@ -0,0 +1,325 @@ +/** + * Character handling in C locale. + * + * This file is based on gnulib c-ctype.h-dd7a871 with the + * other gnulib dependencies removed for use in util-linux. + * + * These functions work like the corresponding functions in <ctype.h>, + * except that they have the C (POSIX) locale hardwired, whereas the + * <ctype.h> functions' behaviour depends on the current locale set via + * setlocale. + * + * Copyright (C) 2000-2003, 2006, 2008-2017 Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef UTIL_LINUX_CCTYPE_H +#define UTIL_LINUX_CCTYPE_H + +/** + * The functions defined in this file assume the "C" locale and a character + * set without diacritics (ASCII-US or EBCDIC-US or something like that). + * Even if the "C" locale on a particular system is an extension of the ASCII + * character set (like on BeOS, where it is UTF-8, or on AmigaOS, where it + * is ISO-8859-1), the functions in this file recognize only the ASCII + * characters. + */ + +#if (' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ + && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ + && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ + && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ + && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ + && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ + && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ + && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ + && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ + && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ + && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ + && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ + && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ + && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ + && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ + && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ + && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ + && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ + && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ + && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ + && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ + && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ + && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126) + +/* + * The character set is ASCII or one of its variants or extensions, not EBCDIC. + * Testing the value of '\n' and '\r' is not relevant. + */ +# define C_CTYPE_ASCII 1 +#elif ! (' ' == '\x40' && '0' == '\xf0' \ + && 'A' == '\xc1' && 'J' == '\xd1' && 'S' == '\xe2' \ + && 'a' == '\x81' && 'j' == '\x91' && 's' == '\xa2') +# error "Only ASCII and EBCDIC are supported" +#endif + +#if 'A' < 0 +# error "EBCDIC and char is signed -- not supported" +#endif + +/* Cases for control characters. */ +#define _C_CTYPE_CNTRL \ + case '\a': case '\b': case '\f': case '\n': \ + case '\r': case '\t': case '\v': \ + _C_CTYPE_OTHER_CNTRL + +/* ASCII control characters other than those with \-letter escapes. */ +#if C_CTYPE_ASCII +# define _C_CTYPE_OTHER_CNTRL \ + case '\x00': case '\x01': case '\x02': case '\x03': \ + case '\x04': case '\x05': case '\x06': case '\x0e': \ + case '\x0f': case '\x10': case '\x11': case '\x12': \ + case '\x13': case '\x14': case '\x15': case '\x16': \ + case '\x17': case '\x18': case '\x19': case '\x1a': \ + case '\x1b': case '\x1c': case '\x1d': case '\x1e': \ + case '\x1f': case '\x7f' +#else + +/* + * Use EBCDIC code page 1047's assignments for ASCII control chars; + * assume all EBCDIC code pages agree about these assignments. + */ +# define _C_CTYPE_OTHER_CNTRL \ + case '\x00': case '\x01': case '\x02': case '\x03': \ + case '\x07': case '\x0e': case '\x0f': case '\x10': \ + case '\x11': case '\x12': case '\x13': case '\x18': \ + case '\x19': case '\x1c': case '\x1d': case '\x1e': \ + case '\x1f': case '\x26': case '\x27': case '\x2d': \ + case '\x2e': case '\x32': case '\x37': case '\x3c': \ + case '\x3d': case '\x3f' +#endif + +/* Cases for lowercase hex letters, and lowercase letters, all offset by N. */ +#define _C_CTYPE_LOWER_A_THRU_F_N(N) \ + case 'a' + (N): case 'b' + (N): case 'c' + (N): case 'd' + (N): \ + case 'e' + (N): case 'f' + (N) +#define _C_CTYPE_LOWER_N(N) \ + _C_CTYPE_LOWER_A_THRU_F_N(N): \ + case 'g' + (N): case 'h' + (N): case 'i' + (N): case 'j' + (N): \ + case 'k' + (N): case 'l' + (N): case 'm' + (N): case 'n' + (N): \ + case 'o' + (N): case 'p' + (N): case 'q' + (N): case 'r' + (N): \ + case 's' + (N): case 't' + (N): case 'u' + (N): case 'v' + (N): \ + case 'w' + (N): case 'x' + (N): case 'y' + (N): case 'z' + (N) + +/* Cases for hex letters, digits, lower, punct, and upper. */ +#define _C_CTYPE_A_THRU_F \ + _C_CTYPE_LOWER_A_THRU_F_N (0): \ + _C_CTYPE_LOWER_A_THRU_F_N ('A' - 'a') +#define _C_CTYPE_DIGIT \ + case '0': case '1': case '2': case '3': \ + case '4': case '5': case '6': case '7': \ + case '8': case '9' +#define _C_CTYPE_LOWER _C_CTYPE_LOWER_N (0) +#define _C_CTYPE_PUNCT \ + case '!': case '"': case '#': case '$': \ + case '%': case '&': case '\'': case '(': \ + case ')': case '*': case '+': case ',': \ + case '-': case '.': case '/': case ':': \ + case ';': case '<': case '=': case '>': \ + case '?': case '@': case '[': case '\\': \ + case ']': case '^': case '_': case '`': \ + case '{': case '|': case '}': case '~' +#define _C_CTYPE_UPPER _C_CTYPE_LOWER_N ('A' - 'a') + +/** + * Function definitions. + * + * Unlike the functions in <ctype.h>, which require an argument in the range + * of the 'unsigned char' type, the functions here operate on values that are + * in the 'unsigned char' range or in the 'char' range. In other words, + * when you have a 'char' value, you need to cast it before using it as + * argument to a <ctype.h> function: + * + * const char *s = ...; + * if (isalpha ((unsigned char) *s)) ... + * + * but you don't need to cast it for the functions defined in this file: + * + * const char *s = ...; + * if (c_isalpha (*s)) ... + */ + +static inline int c_isalnum (int c) +{ + switch (c) { + _C_CTYPE_DIGIT: + _C_CTYPE_LOWER: + _C_CTYPE_UPPER: + return 1; + default: + return 0; + } +} + +static inline int c_isalpha (int c) +{ + switch (c) { + _C_CTYPE_LOWER: + _C_CTYPE_UPPER: + return 1; + default: + return 0; + } +} + +/* The function isascii is not locale dependent. + * Its use in EBCDIC is questionable. + */ +static inline int c_isascii (int c) +{ + switch (c) { + case ' ': + _C_CTYPE_CNTRL: + _C_CTYPE_DIGIT: + _C_CTYPE_LOWER: + _C_CTYPE_PUNCT: + _C_CTYPE_UPPER: + return 1; + default: + return 0; + } +} + +static inline int c_isblank (int c) +{ + return c == ' ' || c == '\t'; +} + +static inline int c_iscntrl (int c) +{ + switch (c) { + _C_CTYPE_CNTRL: + return 1; + default: + return 0; + } +} + +static inline int c_isdigit (int c) +{ + switch (c) { + _C_CTYPE_DIGIT: + return 1; + default: + return 0; + } +} + +static inline int c_isgraph (int c) +{ + switch (c) { + _C_CTYPE_DIGIT: + _C_CTYPE_LOWER: + _C_CTYPE_PUNCT: + _C_CTYPE_UPPER: + return 1; + default: + return 0; + } +} + +static inline int c_islower (int c) +{ + switch (c) { + _C_CTYPE_LOWER: + return 1; + default: + return 0; + } +} + +static inline int c_isprint (int c) +{ + switch (c) { + case ' ': + _C_CTYPE_DIGIT: + _C_CTYPE_LOWER: + _C_CTYPE_PUNCT: + _C_CTYPE_UPPER: + return 1; + default: + return 0; + } +} + +static inline int c_ispunct (int c) +{ + switch (c) { + _C_CTYPE_PUNCT: + return 1; + default: + return 0; + } +} + +static inline int c_isspace (int c) +{ + switch (c) { + case ' ': case '\t': case '\n': case '\v': case '\f': case '\r': + return 1; + default: + return 0; + } +} + +static inline int c_isupper (int c) +{ + switch (c) { + _C_CTYPE_UPPER: + return 1; + default: + return 0; + } +} + +static inline int c_isxdigit (int c) +{ + switch (c) { + _C_CTYPE_DIGIT: + _C_CTYPE_A_THRU_F: + return 1; + default: + return 0; + } +} + +static inline int c_tolower (int c) +{ + switch (c) { + _C_CTYPE_UPPER: + return c - 'A' + 'a'; + default: + return c; + } +} + +static inline int c_toupper (int c) +{ + switch (c) { + _C_CTYPE_LOWER: + return c - 'a' + 'A'; + default: + return c; + } +} + +#endif /* UTIL_LINUX_CCTYPE_H */ diff --git a/include/timeutils.h b/include/timeutils.h index 85fc228db..874f853b7 100644 --- a/include/timeutils.h +++ b/include/timeutils.h @@ -82,4 +82,6 @@ int strtime_short(const time_t *t, struct timeval *now, int flags, char *buf, si extern time_t timegm(struct tm *tm); #endif +int parse_date(struct timespec *, char const *, struct timespec const *); + #endif /* UTIL_LINUX_TIME_UTIL_H */ diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 000000000..070c244be --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +parse-date.c diff --git a/lib/Makemodule.am b/lib/Makemodule.am index d20a9aca2..358b85aee 100644 --- a/lib/Makemodule.am +++ b/lib/Makemodule.am @@ -16,6 +16,7 @@ libcommon_la_SOURCES = \ lib/mbsedit.c\ lib/md5.c \ lib/pager.c \ + lib/parse-date.y \ lib/path.c \ lib/randutils.c \ lib/setproctitle.c \ diff --git a/lib/parse-date.y b/lib/parse-date.y new file mode 100644 index 000000000..7bfdb367a --- /dev/null +++ b/lib/parse-date.y @@ -0,0 +1,1645 @@ +%{ +/** + * Parse a string into an internal timestamp. + * + * This file is based on gnulib parse-datetime.y-dd7a871 with + * the other gnulib dependencies removed for use in util-linux. + * + * Copyright (C) 1999-2000, 2002-2017 Free Software Foundation, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Originally written by Steven M. Bellovin <smb@research.att.com> while + * at the University of North Carolina at Chapel Hill. Later tweaked by + * a couple of people on Usenet. Completely overhauled by Rich $alz + * <rsalz@bbn.com> and Jim Berets <jberets@bbn.com> in August, 1990. + * + * Modified by Paul Eggert <eggert@twinsun.com> in August 1999 to do + * the right thing about local DST. Also modified by Paul Eggert + * <eggert@cs.ucla.edu> in February 2004 to support + * nanosecond-resolution timestamps, and in October 2004 to support + * TZ strings in dates. + */ + +/** + * FIXME: Check for arithmetic overflow in all cases, not just + * some of them. + */ + +#include <sys/time.h> +#include <time.h> + +#include "c.h" +#include "timeutils.h" + +/** + * There's no need to extend the stack, so there's no need to involve + * alloca. + */ +#define YYSTACK_USE_ALLOCA 0 + +/** + * Tell Bison how much stack space is needed. 20 should be plenty for + * this grammar, which is not right recursive. Beware setting it too + * high, since that might cause problems on machines whose + * implementations have lame stack-overflow checking. + */ +#define YYMAXDEPTH 20 +#define YYINITDEPTH YYMAXDEPTH + +/** + * Since the code of parse-datetime.y is not included in the Emacs executable + * itself, there is no need to #define static in this file. Even if + * the code were included in the Emacs executable, it probably + * wouldn't do any harm to #undef it here; this will only cause + * problems if we try to write to a static variable, which I don't + * think this code needs to do. + */ +#ifdef emacs +# undef static +#endif + +#include <inttypes.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + + +#include <stdarg.h> +#include "cctype.h" +#include "nls.h" +#include "xalloc.h" + +/** + * Bison's skeleton tests _STDLIB_H, while some stdlib.h headers + * use _STDLIB_H_ as witness. Map the latter to the one bison uses. + * FIXME: this is temporary. Remove when we have a mechanism to ensure + * that the version we're using is fixed, too. + */ +#ifdef _STDLIB_H_ +# undef _STDLIB_H +# define _STDLIB_H 1 +#endif + +/** + * ISDIGIT differs from isdigit, as follows: + * - Its arg may be any int or unsigned int; it need not be an unsigned char + * or EOF. + * - It's typically faster. + * POSIX says that only '0' through '9' are digits. Prefer ISDIGIT to + * isdigit unless it's important to use the locale's definition + * of "digit" even when the host does not conform to POSIX. + */ +#define ISDIGIT(c) ((unsigned int) (c) - '0' <= 9) + +/** + * Shift A right by B bits portably, by dividing A by 2**B and + * truncating towards minus infinity. A and B should be free of side + * effects, and B should be in the range 0 <= B <= INT_BITS - 2, where + * INT_BITS is the number of useful bits in an int. GNU code can + * assume that INT_BITS is at least 32. + * + * ISO C99 says that A >> B is implementation-defined if A < 0. Some + * implementations (e.g., UNICOS 9.0 on a Cray Y-MP EL) don't shift + * right in the usual way when A < 0, so SHR falls back on division if + * ordinary A >> B doesn't seem to be the usual signed shift. + */ +#define SHR(a, b) \ + (-1 >> 1 == -1 \ + ? (a) >> (b) \ + : (a) / (1 << (b)) - ((a) % (1 << (b)) < 0)) + +#define EPOCH_YEAR 1970 +#define TM_YEAR_BASE 1900 + +#define HOUR(x) ((x) * 60) + +#define STREQ(a, b) (strcmp (a, b) == 0) + +/** + * Convert a possibly-signed character to an unsigned character. This is + * a bit safer than casting to unsigned char, since it catches some type + * errors that the cast doesn't. + */ +static unsigned char to_uchar (char ch) { return ch; } + +/** + * FIXME: It also assumes that signed integer overflow silently wraps around, + * but this is not true any more with recent versions of GCC 4. + */ + +/** + * An integer value, and the number of digits in its textual + * representation. + */ +typedef struct { + int negative; + long int value; + size_t digits; +} textint; + +/* An entry in the lexical lookup table. */ +typedef struct { + char const *name; + int type; + int value; +} table; + +/* Meridian: am, pm, or 24-hour style. */ +enum { MERam, MERpm, MER24 }; + +enum { BILLION = 1000000000, LOG10_BILLION = 9 }; + +/* Relative year, month, day, hour, minutes, seconds, and nanoseconds. */ +typedef struct { + long int year; + long int month; + long int day; + long int hour; + long int minutes; + time_t seconds; + long int ns; +} relative_time; + +#if HAVE_COMPOUND_LITERALS +# define RELATIVE_TIME_0 ((relative_time) { 0, 0, 0, 0, 0, 0, 0 }) +#else +static relative_time const RELATIVE_TIME_0; +#endif + +/* Information passed to and from the parser. */ +typedef struct { + /* The input string remaining to be parsed. */ + const char *input; + + /* N, if this is the Nth Tuesday. */ + long int day_ordinal; + + /* Day of week; Sunday is 0. */ + int day_number; + + /* tm_isdst flag for the local zone. */ + int local_isdst; + + /* Time zone, in minutes east of UTC. */ + long int time_zone; + + /* Style used for time. */ + int meridian; + + /* Gregorian year, month, day, hour, minutes, seconds, and ns. */ + textint year; + long int month; + long int day; + long int hour; + long int minutes; + struct timespec seconds; /* includes nanoseconds */ + + /* Relative year, month, day, hour, minutes, seconds, and ns. */ + relative_time rel; + + /* Presence or counts of some nonterminals parsed so far. */ + int timespec_seen; + int rels_seen; + size_t dates_seen; + size_t days_seen; + size_t local_zones_seen; + size_t dsts_seen; + size_t times_seen; + size_t zones_seen; + size_t year_seen; + + /* 1 if the user specified explicit ordinal day value, */ + int ordinal_day_seen; + + /* Table of local time zone abbreviations, null terminated. */ + table local_time_zone_table[3]; +} parser_control; + +union YYSTYPE; +static int yylex (union YYSTYPE *, parser_control *); +static int yyerror (parser_control const *, char const *); +static long int time_zone_hhmm (parser_control *, textint, long int); + +/** + * Extract into *PC any date and time info from a string of digits + * of the form e.g., YYYYMMDD, YYMMDD, HHMM, HH (and sometimes YYY, + * YYYY, ...). + */ +static void digits_to_date_time(parser_control *pc, textint text_int) +{ + if (pc->dates_seen && ! pc->year.digits + && ! pc->rels_seen && (pc->times_seen || 2 < text_int.digits)) { + pc->year_seen++; + pc->year = text_int; + } else { + if (4 < text_int.digits) { + pc->dates_seen++; + pc->day = text_int.value % 100; + pc->month = (text_int.value / 100) % 100; + pc->year.value = text_int.value / 10000; + pc->year.digits = text_int.digits - 4; + } else { + pc->times_seen++; + if (text_int.digits <= 2) { + pc->hour = text_int.value; + pc->minutes = 0; + } + else { + pc->hour = text_int.value / 100; + pc->minutes = text_int.value % 100; + } + pc->seconds.tv_sec = 0; + pc->seconds.tv_nsec = 0; + pc->meridian = MER24; + } + } +} + +/* Increment PC->rel by FACTOR * REL (FACTOR is 1 or -1). */ +static void apply_relative_time(parser_control *pc, relative_time rel, + int factor) +{ + pc->rel.ns += factor * rel.ns; + pc->rel.seconds += factor * rel.seconds; + pc->rel.minutes += factor * rel.minutes; + pc->rel.hour += factor * rel.hour; + pc->rel.day += factor * rel.day; + pc->rel.month += factor * rel.month; + pc->rel.year += factor * rel.year; + pc->rels_seen = 1; +} + +/* Set PC-> hour, minutes, seconds and nanoseconds members from arguments. */ +static void +set_hhmmss(parser_control *pc, long int hour, long int minutes, + time_t sec, long int nsec) +{ + pc->hour = hour; + pc->minutes = minutes; + pc->seconds.tv_sec = sec; + pc->seconds.tv_nsec = nsec; +} + +%} + +/** + * We want a reentrant parser, even if the TZ manipulation and the calls to + * localtime and gmtime are not reentrant. + */ +%pure-parser +%parse-param { parser_control *pc } +%lex-param { parser_control *pc } + +/* This grammar has 31 shift/reduce conflicts. */ +%expect 31 + +%union { + long int intval; + textint textintval; + struct timespec timespec; + relative_time rel; +} + +%token <intval> tAGO +%token tDST + +%token tYEAR_UNIT tMONTH_UNIT tHOUR_UNIT tMINUTE_UNIT tSEC_UNIT +%token <intval> tDAY_UNIT tDAY_SHIFT + +%token <intval> tDAY tDAYZONE tLOCAL_ZONE tMERIDIAN +%token <intval> tMONTH tORDINAL tZONE + +%token <textintval> tSNUMBER tUNUMBER +%token <timespec> tSDECIMAL_NUMBER tUDECIMAL_NUMBER + +%type <intval> o_colon_minutes +%type <timespec> seconds signed_seconds unsigned_seconds + +%type <rel> relunit relunit_snumber dayshift + +%% + +spec: + timespec + | items +; + +timespec: + '@' seconds { + pc->seconds = $2; + pc->timespec_seen = 1; + } +; + +items: + /* empty */ + | items item +; + +item: + datetime { + pc->times_seen++; pc->dates_seen++; + } + | time { + pc->times_seen++; + } + | local_zone { + pc->local_zones_seen++; + } + | zone { + pc->zones_seen++; + } + | date { + pc->dates_seen++; + } + | day { + pc->days_seen++; + } + | rel + | number + | hybrid +; + +datetime: + iso_8601_datetime +; + +iso_8601_datetime: + iso_8601_date 'T' iso_8601_time +; + +time: + tUNUMBER tMERIDIAN { + set_hhmmss (pc, $1.value, 0, 0, 0); + pc->meridian = $2; + } + | tUNUMBER ':' tUNUMBER tMERIDIAN { + set_hhmmss (pc, $1.value, $3.value, 0, 0); + pc->meridian = $4; + } + | tUNUMBER ':' tUNUMBER ':' unsigned_seconds tMERIDIAN { + set_hhmmss (pc, $1.value, $3.value, $5.tv_sec, $5.tv_nsec); + pc->meridian = $6; + } + | iso_8601_time +; + +iso_8601_time: + tUNUMBER zone_offset { + set_hhmmss (pc, $1.value, 0, 0, 0); + pc->meridian = MER24; + } + | tUNUMBER ':' tUNUMBER o_zone_offset { + set_hhmmss (pc, $1.value, $3.value, 0, 0); + pc->meridian = MER24; + } + | tUNUMBER ':' tUNUMBER ':' unsigned_seconds o_zone_offset { + set_hhmmss (pc, $1.value, $3.value, $5.tv_sec, $5.tv_nsec); + pc->meridian = MER24; + } +; + +o_zone_offset: + /* empty */ + | zone_offset +; + +zone_offset: + tSNUMBER o_colon_minutes { + pc->zones_seen++; + pc->time_zone = time_zone_hhmm (pc, $1, $2); + } +; + +/** + * Local zone strings only affect DST setting, + * and only take affect if the current TZ setting is relevant. + * + * Example 1: + * 'EEST' is parsed as tLOCAL_ZONE, as it relates to the effective TZ: + * TZ=Europe/Helsinki date -d '2016-12-30 EEST' + * + * Example 2: + * 'EEST' is parsed as 'zone' (TZ=+03:00): + * TZ=Asia/Tokyo ./src/date --debug -d '2011-06-11 EEST' + * + * This is implemented by probing the next three calendar quarters + * of the effective timezone and looking for DST changes - + * if found, the timezone name (EEST) is inserted into + * the lexical lookup table with type tLOCAL_ZONE. + * (Search for 'quarter' comment in 'parse_date'). + */ +local_zone: + tLOCAL_ZONE { + pc->local_isdst = $1; + pc->dsts_seen += (0 < $1); + } + | tLOCAL_ZONE tDST { + pc->local_isdst = 1; + pc->dsts_seen += (0 < $1) + 1; + } +; + +/** + * Note 'T' is a special case, as it is used as the separator in ISO + * 8601 date and time of day representation. + */ +zone: + tZONE { + pc->time_zone = $1; + } + | 'T' { + pc->time_zone = HOUR(7); + } + | tZONE relunit_snumber { + pc->time_zone = $1; + apply_relative_time (pc, $2, 1); + } + | 'T' relunit_snumber { + pc->time_zone = HOUR(7); + apply_relative_time (pc, $2, 1); + } + | tZONE tSNUMBER o_colon_minutes { + pc->time_zone = $1 + time_zone_hhmm (pc, $2, $3); + } + | tDAYZONE { + pc->time_zone = $1 + 60; + } + | tZONE tDST { + pc->time_zone = $1 + 60; + } +; + +day: + tDAY { + pc->day_ordinal = 0; + pc->day_number = $1; + } + | tDAY ',' { + pc->day_ordinal = 0; + pc->day_number = $1; + } + | tORDINAL tDAY { + pc->day_ordinal = $1; + pc->day_number = $2; + pc->ordinal_day_seen = 1; + } + | tUNUMBER tDAY { + pc->day_ordinal = $1.value; + pc->day_number = $2; + pc->ordinal_day_seen = 1; + } +; + +date: + tUNUMBER '/' tUNUMBER { + pc->month = $1.value; + pc->day = $3.value; + } + | tUNUMBER '/' tUNUMBER '/' tUNUMBER { + /** + * Interpret as YYYY/MM/DD if the first value has 4 or more digits, + * otherwise as MM/DD/YY. + * The goal in recognizing YYYY/MM/DD is solely to support legacy + * machine-generated dates like those in an RCS log listing. If + * you want portability, use the ISO 8601 format. + */ + if (4 <= $1.digits) { + pc->year = $1; + pc->month = $3.value; + pc->day = $5.value; + } else { + pc->month = $1.value; + pc->day = $3.value; + pc->year = $5; + } + } + | tUNUMBER tMONTH tSNUMBER { + /* e.g. 17-JUN-1992. */ + pc->day = $1.value; + pc->month = $2; + pc->year.value = -$3.value; + pc->year.digits = $3.digits; + } + | tMONTH tSNUMBER tSNUMBER { + /* e.g. JUN-17-1992. */ + pc->month = $1; + pc->day = -$2.value; + pc->year.value = -$3.value; + pc->year.digits = $3.digits; + } + | tMONTH tUNUMBER { + pc->month = $1; + pc->day = $2.value; + } + | tMONTH tUNUMBER ',' tUNUMBER { + pc->month = $1; + pc->day = $2.value; + pc->year = $4; + } + | tUNUMBER tMONTH { + pc->day = $1.value; + pc->month = $2; + } + | tUNUMBER tMONTH tUNUMBER { + pc->day = $1.value; + pc->month = $2; + pc->year = $3; + } + | iso_8601_date +; + +iso_8601_date: + tUNUMBER tSNUMBER tSNUMBER { + /* ISO 8601 format.YYYY-MM-DD. */ + pc->year = $1; + pc->month = -$2.value; + pc->day = -$3.value; + } +; + +rel: + relunit tAGO + { apply_relative_time (pc, $1, $2); } + | relunit + { apply_relative_time (pc, $1, 1); } + | dayshift + { apply_relative_time (pc, $1, 1); } +; + +relunit: + tORDINAL tYEAR_UNIT + { $$ = RELATIVE_TIME_0; $$.year = $1; } + | tUNUMBER tYEAR_UNIT + { $$ = RELATIVE_TIME_0; $$.year = $1.value; } + | tYEAR_UNIT + { $$ = RELATIVE_TIME_0; $$.year = 1; } + | tORDINAL tMONTH_UNIT + { $$ = RELATIVE_TIME_0; $$.month = $1; } + | tUNUMBER tMONTH_UNIT + { $$ = RELATIVE_TIME_0; $$.month = $1.value; } + | tMONTH_UNIT + { $$ = RELATIVE_TIME_0; $$.month = 1; } + | tORDINAL tDAY_UNIT + { $$ = RELATIVE_TIME_0; $$.day = $1 * $2; } + | tUNUMBER tDAY_UNIT + { $$ = RELATIVE_TIME_0; $$.day = $1.value * $2; } + | tDAY_UNIT + { $$ = RELATIVE_TIME_0; $$.day = $1; } + | tORDINAL tHOUR_UNIT + { $$ = RELATIVE_TIME_0; $$.hour = $1; } + | tUNUMBER tHOUR_UNIT + { $$ = RELATIVE_TIME_0; $$.hour = $1.value; } + | tHOUR_UNIT + { $$ = RELATIVE_TIME_0; $$.hour = 1; } + | tORDINAL tMINUTE_UNIT + { $$ = RELATIVE_TIME_0; $$.minutes = $1; } + | tUNUMBER tMINUTE_UNIT + { $$ = RELATIVE_TIME_0; $$.minutes = $1.value; } + | tMINUTE_UNIT + { $$ = RELATIVE_TIME_0; $$.minutes = 1; } + | tORDINAL tSEC_UNIT + { $$ = RELATIVE_TIME_0; $$.seconds = $1; } + | tUNUMBER tSEC_UNIT + { $$ = RELATIVE_TIME_0; $$.seconds = $1.value; } + | tSDECIMAL_NUMBER tSEC_UNIT { + $$ = RELATIVE_TIME_0; + $$.seconds = $1.tv_sec; + $$.ns = $1.tv_nsec; + } + | tUDECIMAL_NUMBER tSEC_UNIT { + $$ = RELATIVE_TIME_0; + $$.seconds = $1.tv_sec; + $$.ns = $1.tv_nsec; + } + | tSEC_UNIT + { $$ = RELATIVE_TIME_0; $$.seconds = 1; } + | relunit_snumber +; + +relunit_snumber: + tSNUMBER tYEAR_UNIT + { $$ = RELATIVE_TIME_0; $$.year = $1.value; } + | tSNUMBER tMONTH_UNIT + { $$ = RELATIVE_TIME_0; $$.month = $1.value; } + | tSNUMBER tDAY_UNIT + { $$ = RELATIVE_TIME_0; $$.day = $1.value * $2; } + | tSNUMBER tHOUR_UNIT + { $$ = RELATIVE_TIME_0; $$.hour = $1.value; } + | tSNUMBER tMINUTE_UNIT + { $$ = RELATIVE_TIME_0; $$.minutes = $1.value; } + | tSNUMBER tSEC_UNIT + { $$ = RELATIVE_TIME_0; $$.seconds = $1.value; } +; + +dayshift: + tDAY_SHIFT + { $$ = RELATIVE_TIME_0; $$.day = $1; } +; + +seconds: signed_seconds | unsigned_seconds; + +signed_seconds: + tSDECIMAL_NUMBER + | tSNUMBER + { $$.tv_sec = $1.value; $$.tv_nsec = 0; } +; + +unsigned_seconds: + tUDECIMAL_NUMBER + | tUNUMBER + { $$.tv_sec = $1.value; $$.tv_nsec = 0; } +; + +number: + tUNUMBER + { digits_to_date_time (pc, $1); } +; + +hybrid: + tUNUMBER relunit_snumber { + /** + * Hybrid all-digit and relative offset, so that we accept e.g., + * "YYYYMMDD +N days" as well as "YYYYMMDD N days". + */ + digits_to_date_time (pc, $1); + apply_relative_time (pc, $2, 1); + } +; + +o_colon_minutes: + /* empty */ + { $$ = -1; } + | ':' tUNUMBER + { $$ = $2.value; } +; + +%% + +static table const meridian_table[] = { + { "AM", tMERIDIAN, MERam }, + { "A.M.", tMERIDIAN, MERam }, + { "PM", tMERIDIAN, MERpm }, + { "P.M.", tMERIDIAN, MERpm }, + { NULL, 0, 0 } +}; + +static table const dst_table[] = { + { "DST", tDST, 0 } +}; + +static table const month_and_day_table[] = { + { "JANUARY", tMONTH, 1 }, + { "FEBRUARY", tMONTH, 2 }, + { "MARCH", tMONTH, 3 }, + { "APRIL", tMONTH, 4 }, + { "MAY", tMONTH, 5 }, + { "JUNE", tMONTH, 6 }, + { "JULY", tMONTH, 7 }, + { "AUGUST", tMONTH, 8 }, + { "SEPTEMBER",tMONTH, 9 }, + { "SEPT", tMONTH, 9 }, + { "OCTOBER", tMONTH, 10 }, + { "NOVEMBER", tMONTH, 11 }, + { "DECEMBER", tMONTH, 12 }, + { "SUNDAY", tDAY, 0 }, + { "MONDAY", tDAY, 1 }, + { "TUESDAY", tDAY, 2 }, + { "TUES", tDAY, 2 }, + { "WEDNESDAY",tDAY, 3 }, + { "WEDNES", tDAY, 3 }, + { "THURSDAY", tDAY, 4 }, + { "THUR", tDAY, 4 }, + { "THURS", tDAY, 4 }, + { "FRIDAY", tDAY, 5 }, + { "SATURDAY", tDAY, 6 }, + { NULL, 0, 0 } +}; + +static table const time_units_table[] = { + { "YEAR", tYEAR_UNIT, 1 }, + { "MONTH", tMONTH_UNIT, 1 }, + { "FORTNIGHT",tDAY_UNIT, 14 }, + { "WEEK", tDAY_UNIT, 7 }, + { "DAY", tDAY_UNIT, 1 }, + { "HOUR", tHOUR_UNIT, 1 }, + { "MINUTE", tMINUTE_UNIT, 1 }, + { "MIN", tMINUTE_UNIT, 1 }, + { "SECOND", tSEC_UNIT, 1 }, + { "SEC", tSEC_UNIT, 1 }, + { NULL, 0, 0 } +}; + +/* Assorted relative-time words. */ +static table const relative_time_table[] = { + { "TOMORROW", tDAY_SHIFT, 1 }, + { "YESTERDAY",tDAY_SHIFT, -1 }, + { "TODAY", tDAY_SHIFT, 0 }, + { "NOW", tDAY_SHIFT, 0 }, + { "LAST", tORDINAL, -1 }, + { "THIS", tORDINAL, 0 }, + { "NEXT", tORDINAL, 1 }, + { "FIRST", tORDINAL, 1 }, + /*{ "SECOND", tORDINAL, 2 }, */ + { "THIRD", tORDINAL, 3 }, + { "FOURTH", tORDINAL, 4 }, + { "FIFTH", tORDINAL, 5 }, + { "SIXTH", tORDINAL, 6 }, + { "SEVENTH", tORDINAL, 7 }, + { "EIGHTH", tORDINAL, 8 }, + { "NINTH", tORDINAL, 9 }, + { "TENTH", tORDINAL, 10 }, + { "ELEVENTH", tORDINAL, 11 }, + { "TWELFTH", tORDINAL, 12 }, + { "AGO", tAGO, -1 }, + { "HENCE", tAGO, 1 }, + { NULL, 0, 0 } +}; + +/** + * The universal time zone table. These labels can be used even for + * timestamps that would not otherwise be valid, e.g., GMT timestamps + * in London during summer. + */ +static table const universal_time_zone_table[] = { + { "GMT", tZONE, HOUR ( 0) }, /* Greenwich Mean */ + { "UT", tZONE, HOUR ( 0) }, /* Universal (Coordinated) */ + { "UTC", tZONE, HOUR ( 0) }, + { NULL, 0, 0 } +}; + +/** + * The time zone table. This table is necessarily incomplete, as time + * zone abbreviations are ambiguous; e.g. Australians interpret "EST" + * as Eastern time in Australia, not as US Eastern Standard Time. + * You cannot rely on parse_date to handle arbitrary time zone + * abbreviations; use numeric abbreviations like "-0500" instead. + */ +static table const time_zone_table[] = { + { "WET", tZONE, HOUR ( 0) }, /* Western European */ + { "WEST", tDAYZONE, HOUR ( 0) }, /* Western European Summer */ + { "BST", tDAYZONE, HOUR ( 0) }, /* British Summer */ + { "ART", tZONE, -HOUR ( 3) }, /* Argentina */ + { "BRT", tZONE, -HOUR ( 3) }, /* Brazil */ + { "BRST", tDAYZONE, -HOUR ( 3) }, /* Brazil Summer */ + { "NST", tZONE, -(HOUR ( 3) + 30) }, /* Newfoundland Standard */ + { "NDT", tDAYZONE,-(HOUR ( 3) + 30) }, /* Newfoundland Daylight */ + { "AST", tZONE, -HOUR ( 4) }, /* Atlantic Standard */ + { "ADT", tDAYZONE, -HOUR ( 4) }, /* Atlantic Daylight */ + { "CLT", tZONE, -HOUR ( 4) }, /* Chile */ + { "CLST", tDAYZONE, -HOUR ( 4) }, /* Chile Summer */ + { "EST", tZONE, -HOUR ( 5) }, /* Eastern Standard */ + { "EDT", tDAYZONE, -HOUR ( 5) }, /* Eastern Daylight */ + { "CST", tZONE, -HOUR ( 6) }, /* Central Standard */ + { "CDT", tDAYZONE, -HOUR ( 6) }, /* Central Daylight */ + { "MST", tZONE, -HOUR ( 7) }, /* Mountain Standard */ + { "MDT", tDAYZONE, -HOUR ( 7) }, /* Mountain Daylight */ + { "PST", tZONE, -HOUR ( 8) }, /* Pacific Standard */ + { "PDT", tDAYZONE, -HOUR ( 8) }, /* Pacific Daylight */ + { "AKST", tZONE, -HOUR ( 9) }, /* Alaska Standard */ + { "AKDT", tDAYZONE, -HOUR ( 9) }, /* Alaska Daylight */ + { "HST", tZONE, -HOUR (10) }, /* Hawaii Standard */ + { "HAST", tZONE, -HOUR (10) }, /* Hawaii-Aleutian Standard */ + { "HADT", tDAYZONE, -HOUR (10) }, /* Hawaii-Aleutian Daylight */ + { "SST", tZONE, -HOUR (12) }, /* Samoa Standard */ + { "WAT", tZONE, HOUR ( 1) }, /* West Africa */ + { "CET", tZONE, HOUR ( 1) }, /* Central European */ + { "CEST", tDAYZONE, HOUR ( 1) }, /* Central European Summer */ + { "MET", tZONE, HOUR ( 1) }, /* Middle European */ + { "MEZ", tZONE, HOUR ( 1) }, /* Middle European */ + { "MEST", tDAYZONE, HOUR ( 1) }, /* Middle European Summer */ + { "MESZ", tDAYZONE, HOUR ( 1) }, /* Middle European Summer */ + { "EET", tZONE, HOUR ( 2) }, /* Eastern European */ + { "EEST", tDAYZONE, HOUR ( 2) }, /* Eastern European Summer */ + { "CAT", tZONE, HOUR ( 2) }, /* Central Africa */ + { "SAST", tZONE, HOUR ( 2) }, /* South Africa Standard */ + { "EAT", tZONE, HOUR ( 3) }, /* East Africa */ + { "MSK", tZONE, HOUR ( 3) }, /* Moscow */ + { "MSD", tDAYZONE, HOUR ( 3) }, /* Moscow Daylight */ + { "IST", tZONE, (HOUR ( 5) + 30) }, /* India Standard */ + { "SGT", tZONE, HOUR ( 8) }, /* Singapore */ + { "KST", tZONE, HOUR ( 9) }, /* Korea Standard */ + { "JST", tZONE, HOUR ( 9) }, /* Japan Standard */ + { "GST", tZONE, HOUR (10) }, /* Guam Standard */ + { "NZST", tZONE, HOUR (12) }, /* New Zealand Standard */ + { "NZDT", tDAYZONE, HOUR (12) }, /* New Zealand Daylight */ + { NULL, 0, 0 } +}; + +/** + * Military time zone table. + * + * Note 'T' is a special case, as it is used as the separator in ISO + * 8601 date and time of day representation. + */ +static table const military_table[] = { + { "A", tZONE, -HOUR ( 1) }, + { "B", tZONE, -HOUR ( 2) }, + { "C", tZONE, -HOUR ( 3) }, + { "D", tZONE, -HOUR ( 4) }, + { "E", tZONE, -HOUR ( 5) }, + { "F", tZONE, -HOUR ( 6) }, + { "G", tZONE, -HOUR ( 7) }, + { "H", tZONE, -HOUR ( 8) }, + { "I", tZONE, -HOUR ( 9) }, + { "K", tZONE, -HOUR (10) }, + { "L", tZONE, -HOUR (11) }, + { "M", tZONE, -HOUR (12) }, + { "N", tZONE, HOUR ( 1) }, + { "O", tZONE, HOUR ( 2) }, + { "P", tZONE, HOUR ( 3) }, + { "Q", tZONE, HOUR ( 4) }, + { "R", tZONE, HOUR ( 5) }, + { "S", tZONE, HOUR ( 6) }, + { "T", 'T', 0 }, + { "U", tZONE, HOUR ( 8) }, + { "V", tZONE, HOUR ( 9) }, + { "W", tZONE, HOUR (10) }, + { "X", tZONE, HOUR (11) }, + { "Y", tZONE, HOUR (12) }, + { "Z", tZONE, HOUR ( 0) }, + { NULL, 0, 0 } +}; + + + +/** + * Convert a time zone expressed as HH:MM into an integer count of + * minutes. If MM is negative, then S is of the form HHMM and needs + * to be picked apart; otherwise, S is of the form HH. As specified in + * http://www.opengroup.org/susv3xbd/xbd_chap08.html#tag_08_03, allow + * only valid TZ range, and consider first two digits as hours, if no + * minutes specified. + */ + +static long int time_zone_hhmm(parser_control *pc, textint s, long int mm) +{ + long int n_minutes; + + /** + * If the length of S is 1 or 2 and no minutes are specified, + * interpret it as a number of hours. + */ + if (s.digits <= 2 && mm < 0) + s.value *= 100; + + if (mm < 0) + n_minutes = (s.value / 100) * 60 + s.value % 100; + else + n_minutes = s.value * 60 + (s.negative ? -mm : mm); + + /** + * If the absolute number of minutes is larger than 24 hours, + * arrange to reject it by incrementing pc->zones_seen. Thus, + * we allow only values in the range UTC-24:00 to UTC+24:00. + */ + if (24 * 60 < abs (n_minutes)) + pc->zones_seen++; + + return n_minutes; +} + +static int to_hour(long int hours, int meridian) +{ + switch (meridian) { + default: /* Pacify GCC. */ + case MER24: + return 0 <= hours && hours < 24 ? hours : -1; + case MERam: + return 0 < hours && hours < 12 ? hours : hours == 12 ? 0 : -1; + case MERpm: + return 0 < hours && hours < 12 ? hours + 12 : hours == 12 ? 12 : -1; + } +} + +static long int to_year(textint textyear) +{ + long int year = textyear.value; + + if (year < 0) + year = -year; + + /** + * XPG4 suggests that years 00-68 map to 2000-2068, and + * years 69-99 map to 1969-1999. + */ + else if (textyear.digits == 2) + year += year < 69 ? 2000 : 1900; + + return year; +} + +static table const * lookup_zone(parser_control const *pc, char const *name) +{ + table const *tp; + + for (tp = universal_time_zone_table; tp->name; tp++) + if (strcmp (name, tp->name) == 0) + return tp; + + /** + * Try local zone abbreviations before those in time_zone_table, as + * the local ones are more likely to be right. + */ + for (tp = pc->local_time_zone_table; tp->name; tp++) + if (strcmp (name, tp->name) == 0) + return tp; + + for (tp = time_zone_table; tp->name; tp++) + if (strcmp (name, tp->name) == 0) + return tp; + + return NULL; +} + +#if ! HAVE_TM_GMTOFF +/** + * Yield the difference between *A and *B, + * measured in seconds, ignoring leap seconds. + * The body of this function is taken directly from the GNU C Library; + * see src/strftime.c. + */ +static long int tm_diff(struct tm const *a, struct tm const *b) +{ + /** + * Compute intervening leap days correctly even if year is negative. + * Take care to avoid int overflow in leap day calculations. + */ + int a4 = SHR (a->tm_year, 2) + SHR (TM_YEAR_BASE, 2) - ! (a->tm_year & 3); + int b4 = SHR (b->tm_year, 2) + SHR (TM_YEAR_BASE, 2) - ! (b->tm_year & 3); + int a100 = a4 / 25 - (a4 % 25 < 0); + int b100 = b4 / 25 - (b4 % 25 < 0); + int a400 = SHR (a100, 2); + int b400 = SHR (b100, 2); + int intervening_leap_days = (a4 - b4) - (a100 - b100) + (a400 - b400); + long int ayear = a->tm_year; + long int years = ayear - b->tm_year; + long int days = (365 * years + intervening_leap_days + + (a->tm_yday - b->tm_yday)); + return (60 * (60 * (24 * days + (a->tm_hour - b->tm_hour)) + + (a->tm_min - b->tm_min)) + + (a->tm_sec - b->tm_sec)); +} +#endif /* ! HAVE_TM_GMTOFF */ + +static table const * lookup_word(parser_control const *pc, char *word) +{ + char *p; + char *q; + size_t wordlen; + table const *tp; + int period_found; + int abbrev; + + /* Make it uppercase. */ + for (p = word; *p; p++) { + unsigned char ch = *p; + *p = c_toupper (ch); + } + + for (tp = meridian_table; tp->name; tp++) + if (strcmp (word, tp->name) == 0) + return tp; + + /* See if we have an abbreviation for a month. */ + wordlen = strlen (word); + abbrev = wordlen == 3 || (wordlen == 4 && word[3] == '.'); + + for (tp = month_and_day_table; tp->name; tp++) + if ((abbrev ? strncmp (word, tp->name, 3) : + strcmp (word, tp->name)) == 0) + return tp; + + if ((tp = lookup_zone (pc, word))) + return tp; + + if (strcmp (word, dst_table[0].name) == 0) + return dst_table; + + for (tp = time_units_table; tp->name; tp++) + if (strcmp (word, tp->name) == 0) + return tp; + + /* Strip off any plural and try the units table again. */ + if (word[wordlen - 1] == 'S') { + word[wordlen - 1] = '\0'; + for (tp = time_units_table; tp->name; tp++) + if (strcmp (word, tp->name) == 0) + return tp; + word[wordlen - 1] = 'S'; /* For "this" in relative_time_table. */ + } + + for (tp = relative_time_table; tp->name; tp++) + if (strcmp (word, tp->name) == 0) + return tp; + + /* Military time zones. */ + if (wordlen == 1) + for (tp = military_table; tp->name; tp++) + if (word[0] == tp->name[0]) + return tp; + + /* Drop out any periods and try the time zone table again. */ + for (period_found = 0, p = q = word; (*p = *q); q++) + if (*q == '.') + period_found = 1; + else + p++; + if (period_found && (tp = lookup_zone (pc, word))) + return tp; + + return NULL; +} + +static int yylex (union YYSTYPE *lvalp, parser_control *pc) +{ + unsigned char c; + size_t count; + + for (;;) { + while (c = *pc->input, c_isspace (c)) + pc->input++; + + if (ISDIGIT (c) || c == '-' || c == '+') { + char const *p; + int sign; + unsigned long int value; + if (c == '-' || c == '+') { + sign = c == '-' ? -1 : 1; + while (c = *++pc->input, c_isspace (c)) + continue; + if (! ISDIGIT (c)) + /* skip the '-' sign */ + continue; + } else + sign = 0; + p = pc->input; + for (value = 0; ; value *= 10) { + unsigned long int value1 = value + (c - '0'); + if (value1 < value) + return '?'; + value = value1; + c = *++p; + if (! ISDIGIT (c)) + break; + if (ULONG_MAX / 10 < value) + return '?'; + } + if ((c == '.' || c == ',') && ISDIGIT (p[1])) { + time_t s; + int ns; + int digits; + unsigned long int value1; + + /* Check for overflow when converting value to + * time_t. + */ + if (sign < 0) { + s = - value; + if (0 < s) + return '?'; + value1 = -s; + } else { + s = value; + if (s < 0) + return '?'; + value1 = s; + } + if (value != value1) + return '?'; + + /* Accumulate fraction, to ns precision. */ + p++; + ns = *p++ - '0'; + for (digits = 2; + digits <= LOG10_BILLION; digits++) { + ns *= 10; + if (ISDIGIT (*p)) + ns += *p++ - '0'; + } + + /* Skip excess digits, truncating toward + * -Infinity. + */ + if (sign < 0) + for (; ISDIGIT (*p); p++) + if (*p != '0') { + ns++; + break; + } + while (ISDIGIT (*p)) + p++; + + /* Adjust to the timespec convention, which is + * that tv_nsec is always a positive offset even + * if tv_sec is negative. + */ + if (sign < 0 && ns) { + s--; + if (! (s < 0)) + return '?'; + ns = BILLION - ns; + } + + lvalp->timespec.tv_sec = s; + lvalp->timespec.tv_nsec = ns; + pc->input = p; + return + sign ? tSDECIMAL_NUMBER : tUDECIMAL_NUMBER; + } else { + lvalp->textintval.negative = sign < 0; + if (sign < 0) { + lvalp->textintval.value = - value; + if (0 < lvalp->textintval.value) + return '?'; + } else { + lvalp->textintval.value = value; + if (lvalp->textintval.value < 0) + return '?'; + } + lvalp->textintval.digits = p - pc->input; + pc->input = p; + return sign ? tSNUMBER : tUNUMBER; + } + } + + if (c_isalpha (c)) { + char buff[20]; + char *p = buff; + table const *tp; + + do { + if (p < buff + sizeof buff - 1) + *p++ = c; + c = *++pc->input; + } + while (c_isalpha (c) || c == '.'); + + *p = '\0'; + tp = lookup_word (pc, buff); + if (! tp) { + return '?'; + } + lvalp->intval = tp->value; + return tp->type; + } + + if (c != '(') + return to_uchar (*pc->input++); + + count = 0; + do { + c = *pc->input++; + if (c == '\0') + return c; + if (c == '(') + count++; + else if (c == ')') + count--; + } + while (count != 0); + } +} + +/* Do nothing if the parser reports an error. */ +static int yyerror(parser_control const *pc __attribute__((__unused__)), + char const *s __attribute__((__unused__))) +{ + return 0; +} + +/** + * If *TM0 is the old and *TM1 is the new value of a struct tm after + * passing it to mktime, return 1 if it's OK that mktime returned T. + * It's not OK if *TM0 has out-of-range members. + */ + +static int mktime_ok(struct tm const *tm0, struct tm const *tm1, time_t t) +{ + if (t == (time_t) -1) { + /** + * Guard against falsely reporting an error when parsing a + * timestamp that happens to equal (time_t) -1, on a host that + * supports such a timestamp. + */ + tm1 = localtime (&t); + if (!tm1) + return 0; + } + + return ! ((tm0->tm_sec ^ tm1->tm_sec) + | (tm0->tm_min ^ tm1->tm_min) + | (tm0->tm_hour ^ tm1->tm_hour) + | (tm0->tm_mday ^ tm1->tm_mday) + | (tm0->tm_mon ^ tm1->tm_mon) + | (tm0->tm_year ^ tm1->tm_year)); +} + +/** + * A reasonable upper bound for the size of ordinary TZ strings. + * Use heap allocation if TZ's length exceeds this. + */ +enum { TZBUFSIZE = 100 }; + +/** + * Return a copy of TZ, stored in TZBUF if it fits, and heap-allocated + * otherwise. + */ +static char * get_tz(char tzbuf[TZBUFSIZE]) +{ + char *tz = getenv ("TZ"); + if (tz) { + size_t tzsize = strlen (tz) + 1; + tz = (tzsize <= TZBUFSIZE + ? memcpy (tzbuf, tz, tzsize) + : xstrdup (tz)); + } + return tz; +} + +/** + * Parse a date/time string, storing the resulting time value into *result. + * The string itself is pointed to by *p. Return 1 if successful. + * *p can be an incomplete or relative time specification; if so, use + * *now as the basis for the returned time. + */ +int parse_date(struct timespec *result, char const *p, + struct timespec const *now) +{ + time_t Start; + long int Start_ns; + struct tm const *tmp; + struct tm tm; + struct tm tm0; + parser_control pc; + struct timespec gettime_buffer; + unsigned char c; + int tz_was_altered = 0; + char *tz0 = NULL; + char tz0buf[TZBUFSIZE]; + int ok = 1; + struct timeval tv; + + if (! now) { + gettimeofday (&tv, NULL); + gettime_buffer.tv_sec = tv.tv_sec; + gettime_buffer.tv_nsec = tv.tv_usec * 1000; + now = &gettime_buffer; + } + + Start = now->tv_sec; + Start_ns = now->tv_nsec; + + tmp = localtime (&now->tv_sec); + if (! tmp) + return 0; + + while (c = *p, c_isspace (c)) + p++; + + if (strncmp (p, "TZ=\"", 4) == 0) { + char const *tzbase = p + 4; + size_t tzsize = 1; + char const *s; + + for (s = tzbase; *s; s++, tzsize++) + if (*s == '\\') { + s++; + if (! (*s == '\\' || *s == '"')) + break; + } else if (*s == '"') { + char *z; + char *tz1; + char tz1buf[TZBUFSIZE]; + int large_tz = TZBUFSIZE < tzsize; + int setenv_ok; + tz0 = get_tz (tz0buf); + z = tz1 = large_tz ? malloc (tzsize) : tz1buf; + for (s = tzbase; *s != '"'; s++) + *z++ = *(s += *s == '\\'); + *z = '\0'; + setenv_ok = setenv ("TZ", tz1, 1) == 0; + if (large_tz) + free (tz1); + if (!setenv_ok) + goto fail; + tz_was_altered = 1; + + p = s + 1; + while (c = *p, c_isspace (c)) + p++; + + break; + } + } + + /** + * As documented, be careful to treat the empty string just like + * a date string of "0". Without this, an empty string would be + * declared invalid when parsed during a DST transition. + */ + if (*p == '\0') + p = "0"; + + pc.input = p; + pc.year.value = tmp->tm_year; + pc.year.value += TM_YEAR_BASE; + pc.year.digits = 0; + pc.month = tmp->tm_mon + 1; + pc.day = tmp->tm_mday; + pc.hour = tmp->tm_hour; + pc.minutes = tmp->tm_min; + pc.seconds.tv_sec = tmp->tm_sec; + pc.seconds.tv_nsec = Start_ns; + tm.tm_isdst = tmp->tm_isdst; + + pc.meridian = MER24; + pc.rel = RELATIVE_TIME_0; + pc.timespec_seen = 0; + pc.rels_seen = 0; + pc.dates_seen = 0; + pc.days_seen = 0; + pc.times_seen = 0; + pc.local_zones_seen = 0; + pc.dsts_seen = 0; + pc.zones_seen = 0; + pc.year_seen = 0; + pc.ordinal_day_seen = 0; + +#if HAVE_STRUCT_TM_TM_ZONE + pc.local_time_zone_table[0].name = tmp->tm_zone; + pc.local_time_zone_table[0].type = tLOCAL_ZONE; + pc.local_time_zone_table[0].value = tmp->tm_isdst; + pc.local_time_zone_table[1].name = NULL; + + /** + * Probe the names used in the next three calendar quarters, looking + * for a tm_isdst different from the one we already have. + */ + { + int quarter; + for (quarter = 1; quarter <= 3; quarter++) { + time_t probe = Start + quarter * (90 * 24 * 60 * 60); + struct tm const *probe_tm = localtime (&probe); + if (probe_tm && probe_tm->tm_zone + && probe_tm->tm_isdst + != pc.local_time_zone_table[0].value) { + { + pc.local_time_zone_table[1].name + = probe_tm->tm_zone; + pc.local_time_zone_table[1].type + = tLOCAL_ZONE; + pc.local_time_zone_table[1].value + = probe_tm->tm_isdst; + pc.local_time_zone_table[2].name + = NULL; + } + break; + } + } + } +#else +#if HAVE_TZNAME + { +# if !HAVE_DECL_TZNAME + extern char *tzname[]; +# endif + int i; + for (i = 0; i < 2; i++) { + pc.local_time_zone_table[i].name = tzname[i]; + pc.local_time_zone_table[i].type = tLOCAL_ZONE; + pc.local_time_zone_table[i].value = i; + } + pc.local_time_zone_table[i].name = NULL; + } +#else + pc.local_time_zone_table[0].name = NULL; +#endif +#endif + + if (pc.local_time_zone_table[0].name && pc.local_time_zone_table[1].name + && ! strcmp (pc.local_time_zone_table[0].name, + pc.local_time_zone_table[1].name)) { + /** + * This locale uses the same abbreviation for standard and + * daylight times. So if we see that abbreviation, we don't + * know whether it's daylight time. + */ + pc.local_time_zone_table[0].value = -1; + pc.local_time_zone_table[1].name = NULL; + } + + if (yyparse (&pc) != 0) { + goto fail; + } + + if (pc.timespec_seen) + *result = pc.seconds; + else { + if (1 < (pc.times_seen | pc.dates_seen | pc.days_seen + | pc.dsts_seen + | (pc.local_zones_seen + pc.zones_seen))) { + goto fail; + } + + tm.tm_year = to_year (pc.year) - TM_YEAR_BASE; + tm.tm_mon = pc.month - 1; + tm.tm_mday = pc.day; + if (pc.times_seen || (pc.rels_seen && + ! pc.dates_seen && ! pc.days_seen)) { + tm.tm_hour = to_hour (pc.hour, pc.meridian); + if (tm.tm_hour < 0) { + goto fail; + } + tm.tm_min = pc.minutes; + tm.tm_sec = pc.seconds.tv_sec; + } else { + tm.tm_hour = tm.tm_min = tm.tm_sec = 0; + pc.seconds.tv_nsec = 0; + } + + /** + * Let mktime deduce tm_isdst if we have an absolute timestamp. + */ + if (pc.dates_seen | pc.days_seen | pc.times_seen) + tm.tm_isdst = -1; + + /** + * But if the input explicitly specifies local time with or + * without DST, give mktime that information. + */ + if (pc.local_zones_seen) + tm.tm_isdst = pc.local_isdst; + + tm0 = tm; + + Start = mktime (&tm); + + if (! mktime_ok (&tm0, &tm, Start)) { + if (! pc.zones_seen) { + goto fail; + } else { + /** Guard against falsely reporting errors near + * the time_t boundaries when parsing times in + * other time zones. For example, suppose the + * input string "1969-12-31 23:00:00 -0100", the + * current time zone is 8 hours ahead of UTC, + * and the min time_t value is 1970-01-01 + * 00:00:00 UTC. Then the min localtime value + * is 1970-01-01 08:00:00, and mktime will + * therefore fail on 1969-12-31 23:00:00. To + * work around the problem, set the time zone to + * 1 hour behind UTC temporarily by setting + * TZ="XXX1:00" and try mktime again. + */ + + long int time_zone = pc.time_zone; + + long int abs_time_zone = time_zone < 0 + ? - time_zone : time_zone; + + long int abs_time_zone_hour + = abs_time_zone / 60; + + int abs_time_zone_min = abs_time_zone % 60; + + char tz1buf[sizeof "XXX+0:00" + + sizeof pc.time_zone + * CHAR_BIT / 3]; + + if (!tz_was_altered) + tz0 = get_tz (tz0buf); + sprintf (tz1buf, "XXX%s%ld:%02d", + &"-"[time_zone < 0], + abs_time_zone_hour, + abs_time_zone_min); + if (setenv ("TZ", tz1buf, 1) != 0) { + goto fail; + } + tz_was_altered = 1; + tm = tm0; + Start = mktime (&tm); + if (! mktime_ok (&tm0, &tm, Start)) { + goto fail; + } + } + } + + if (pc.days_seen && ! pc.dates_seen) { + tm.tm_mday += ((pc.day_number - tm.tm_wday + 7) % 7 + 7 + * (pc.day_ordinal + - (0 < pc.day_ordinal + && tm.tm_wday != pc.day_number))); + tm.tm_isdst = -1; + Start = mktime (&tm); + if (Start == (time_t) -1) { + goto fail; + } + } + /* Add relative date. */ + if (pc.rel.year | pc.rel.month | pc.rel.day) { + int year = tm.tm_year + pc.rel.year; + int month = tm.tm_mon + pc.rel.month; + int day = tm.tm_mday + pc.rel.day; + if (((year < tm.tm_year) ^ (pc.rel.year < 0)) + | ((month < tm.tm_mon) ^ (pc.rel.month < 0)) + | ((day < tm.tm_mday) ^ (pc.rel.day < 0))) { + goto fail; + } + tm.tm_year = year; + tm.tm_mon = month; + tm.tm_mday = day; + tm.tm_hour = tm0.tm_hour; + tm.tm_min = tm0.tm_min; + tm.tm_sec = tm0.tm_sec; + tm.tm_isdst = tm0.tm_isdst; + Start = mktime (&tm); + if (Start == (time_t) -1) { + goto fail; + } + } + + /** + * The only "output" of this if-block is an updated Start value, + * so this block must follow others that clobber Start. + */ + if (pc.zones_seen) { + long int delta = pc.time_zone * 60; + time_t t1; +#ifdef HAVE_TM_GMTOFF + delta -= tm.tm_gmtoff; +#else + time_t t = Start; + struct tm const *gmt = gmtime (&t); + if (! gmt) { + goto fail; + } + delta -= tm_diff (&tm, gmt); +#endif + t1 = Start - delta; + if ((Start < t1) != (delta < 0)) { + goto fail; /* time_t overflow */ + } + Start = t1; + } + + /** + * Add relative hours, minutes, and seconds. On hosts that + * support leap seconds, ignore the possibility of leap seconds; + * e.g., "+ 10 minutes" adds 600 seconds, even if one of them is + * a leap second. Typically this is not what the user wants, + * but it's too hard to do it the other way, because the time + * zone indicator must be applied before relative times, and if + * mktime is applied again the time zone will be lost. + */ + long int sum_ns = pc.seconds.tv_nsec + pc.rel.ns; + long int normalized_ns = (sum_ns % BILLION + BILLION) % BILLION; + time_t t0 = Start; + long int d1 = 60 * 60 * pc.rel.hour; + time_t t1 = t0 + d1; + long int d2 = 60 * pc.rel.minutes; + time_t t2 = t1 + d2; + time_t d3 = pc.rel.seconds; + time_t t3 = t2 + d3; + long int d4 = (sum_ns - normalized_ns) / BILLION; + time_t t4 = t3 + d4; + time_t t5 = t4; + + if ((d1 / (60 * 60) ^ pc.rel.hour) + | (d2 / 60 ^ pc.rel.minutes) + | ((t1 < t0) ^ (d1 < 0)) + | ((t2 < t1) ^ (d2 < 0)) + | ((t3 < t2) ^ (d3 < 0)) + | ((t4 < t3) ^ (d4 < 0)) + | (t5 != t4)) { + goto fail; + } + result->tv_sec = t5; + result->tv_nsec = normalized_ns; + } + + goto done; + + fail: + ok = 0; + done: + if (tz_was_altered) + ok &= (tz0 ? setenv ("TZ", tz0, 1) + : unsetenv ("TZ")) == 0; + if (tz0 != tz0buf) + free (tz0); + return ok; +} diff --git a/sys-utils/hwclock.8.in b/sys-utils/hwclock.8.in index 2dec7823e..78e53aeb1 100644 --- a/sys-utils/hwclock.8.in +++ b/sys-utils/hwclock.8.in @@ -282,27 +282,27 @@ end up losing a day. . .TP .BI \%\-\-date= date_string -You need this option if you specify the +This option must be used with the .B \-\-set or .B \%\-\-predict functions, otherwise it is ignored. -It specifies the time to which to set the Hardware Clock, or the -time for which to predict the Hardware Clock reading. -The value of this option is used as an argument to the -.BR date "(1) program's " \-\-date -option. For example: .RS .IP "" 4 -.B "hwclock\ \-\-set\ \-\-date='2011-08-14\ 16:45:05'" +.B "hwclock\ \-\-set\ \-\-date='16:45'" +.IP "" 4 +.B "hwclock\ \-\-predict\ \-\-date='2525-08-14\ 07:11:05'" .PP The argument must be in local time, even if you keep your Hardware Clock in -UTC. See the +UTC. See the .B \%\-\-localtime -option. The argument must not be a relative time like "+5 minutes", because +option. Therefore, the argument should not include any timezone information. +It also should not be a relative time like "+5 minutes", because .BR \%hwclock 's -precision depends upon correlation between the argument's value and when -the enter key is pressed. +precision depends upon correlation between the argument's value and when the +enter key is pressed. Fractional seconds are silently dropped. This option is +capable of understanding many time and date formats, but the previous +parameters should be observed. .RE . .TP diff --git a/sys-utils/hwclock.c b/sys-utils/hwclock.c index 21d1304e1..21559cf41 100644 --- a/sys-utils/hwclock.c +++ b/sys-utils/hwclock.c @@ -639,100 +639,6 @@ display_time(const bool hclock_valid, struct timeval hwctime) } /* - * Interpret the value of the --date option, which is something like - * "13:05:01". In fact, it can be any of the myriad ASCII strings that - * specify a time which the "date" program can understand. The date option - * value in question is our "dateopt" argument. - * - * The specified time is in the local time zone. - * - * Our output, "*time_p", is a seconds-into-epoch time. - * - * We use the "date" program to interpret the date string. "date" must be - * runnable by issuing the command "date" to the /bin/sh shell. That means - * in must be in the current PATH. - * - * If anything goes wrong (and many things can), we return code 10 - * and arbitrary *time_p. Otherwise, return code is 0 and *time_p is valid. - */ -static int interpret_date_string(const struct hwclock_control *ctl, - time_t *const time_p) -{ - FILE *date_child_fp = NULL; - char *date_command = NULL; - char *date_resp = NULL; - size_t len = 0; - const char magic[] = "seconds-into-epoch="; - int retcode = 1; - long seconds_since_epoch; - - if (!ctl->date_opt) { - warnx(_("No --date option specified.")); - return retcode; - } - - /* Quotes in date_opt would ruin the date command we construct. */ - if (strchr(ctl->date_opt, '"') != NULL || - strchr(ctl->date_opt, '`') != NULL || - strchr(ctl->date_opt, '$') != NULL) { - warnx(_ - ("The value of the --date option is not a valid date.\n" - "In particular, it contains illegal character(s).")); - return retcode; - } - - xasprintf(&date_command, "date --date=\"%s\" +%s%%s", - ctl->date_opt, magic); - if (ctl->debug) - printf(_("Issuing date command: %s\n"), date_command); - - date_child_fp = popen(date_command, "r"); - if (date_child_fp == NULL) { - warn(_("Unable to run 'date' program in /bin/sh shell. " - "popen() failed")); - goto out; - } - - if (getline(&date_resp, &len, date_child_fp) < 0) { - warn(_("getline() failed")); - goto out; - } - if (ctl->debug) - printf(_("response from date command = %s\n"), date_resp); - if (strncmp(date_resp, magic, sizeof(magic) - 1) != 0) { - warnx(_("The date command issued by %s returned " - "unexpected results.\n" - "The command was:\n %s\n" - "The response was:\n %s"), - program_invocation_short_name, date_command, date_resp); - goto out; - } - - if (sscanf(date_resp + sizeof(magic) - 1, "%ld", &seconds_since_epoch) < 1) { - warnx(_("The date command issued by %s returned " - "something other than an integer where the " - "converted time value was expected.\n" - "The command was:\n %s\n" - "The response was:\n %s\n"), - program_invocation_short_name, date_command, date_resp); - } else { - retcode = 0; - *time_p = seconds_since_epoch; - if (ctl->debug) - printf(_("date string %s equates to " - "%ld seconds since 1969.\n"), - ctl->date_opt, *time_p); - } - out: - free(date_command); - free(date_resp); - if (date_child_fp) - pclose(date_child_fp); - - return retcode; -} - -/* * Set the System Clock to time 'newtime'. * * Also set the kernel time zone value to the value indicated by the TZ @@ -1460,6 +1366,7 @@ int main(int argc, char **argv) struct hwclock_control ctl = { NULL }; struct timeval startup_time; struct adjtime adjtime = { 0 }; + struct timespec when = { 0 }; /* * The time we started up, in seconds into the epoch, including * fractions. @@ -1699,11 +1606,10 @@ int main(int argc, char **argv) #endif if (ctl.set || ctl.predict) { - rc = interpret_date_string(&ctl, &set_time); - /* (time-consuming) */ - if (rc != 0) { - warnx(_("No usable set-to time. " - "Cannot set clock.")); + if (parse_date(&when, ctl.date_opt, NULL)) + set_time = when.tv_sec; + else { + warnx(_("invalid date '%s'"), ctl.date_opt); hwclock_exit(&ctl, EX_USAGE); } } |