author: kx <kx@radix.pro> 2023-03-24 03:55:33 +0300
committer: kx <kx@radix.pro> 2023-03-24 03:55:33 +0300
commit: bfc1508d26c89c9a36d2d9a827fe2c4ed128884d
parent: c836ae3775cf72f17e0b7e3792d156fdb389bee3
Commit Summary:
Diffstat:
1 file changed, 525 insertions, 0 deletions
diff --git a/csvncgi/date.c b/csvncgi/date.c new file mode 100644 index 0000000..a452ab1 --- /dev/null +++ b/csvncgi/date.c @@ -0,0 +1,596 @@ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <sys/sysinfo.h> +#include <sys/types.h> +#ifdef HAVE_INTTYPES_H +#include <inttypes.h> +#else +#include <stdint.h> +#endif +#include <stddef.h> /* offsetof(3) */ +#include <dirent.h> +#include <sys/stat.h> /* chmod(2) */ +#include <sys/file.h> +#include <sys/mman.h> +#include <fcntl.h> +#include <limits.h> +#include <string.h> /* strdup(3) */ +#include <libgen.h> /* basename(3) */ +#include <ctype.h> /* tolower(3) */ +#include <errno.h> +#include <time.h> +#include <sys/time.h> +#include <pwd.h> +#include <grp.h> +#include <stdarg.h> +#include <unistd.h> + +#include <nls.h> + +#include <defs.h> + +#include <strbuf.h> +#include <date.h> + + +/* Valid rule actions */ +enum rule_action +{ + ACCUM, /* Accumulate a decimal value */ + MICRO, /* Accumulate microseconds */ + TZIND, /* Handle +, -, Z */ + NOOP, /* Do nothing */ + SKIPFROM, /* If at end-of-value, accept the match. Otherwise, + if the next template character matches the current + value character, continue processing as normal. + Otherwise, attempt to complete matching starting + immediately after the first subsequent occurrance of + ']' in the template. */ + SKIP, /* Ignore this template character */ + ACCEPT /* Accept the value */ +}; + +/* How to handle a particular character in a template */ +struct rule +{ + char key; /* The template char that this rule matches */ + const char *valid; /* String of valid chars for this rule */ + enum rule_action action; /* What action to take when the rule is matched */ + int offset; /* Where to store the any results of the action, + expressed in terms of bytes relative to the + base of a match_state object. */ +}; + +struct match_state +{ + struct tm base; + struct timeval tv; + int gmtoff; + int gmtoff_hours; + int gmtoff_minutes; +}; + + +#define DIGITS "0123456789" + +/* + A declarative specification of how each template character + should be processed, using a rule for each valid symbol. + */ +static const struct rule rules[] = +{ + { 'Y', DIGITS, ACCUM, offsetof(struct match_state, base.tm_year) }, + { 'M', DIGITS, ACCUM, offsetof(struct match_state, base.tm_mon) }, + { 'D', DIGITS, ACCUM, offsetof(struct match_state, base.tm_mday) }, + { 'h', DIGITS, ACCUM, offsetof(struct match_state, base.tm_hour) }, + { 'm', DIGITS, ACCUM, offsetof(struct match_state, base.tm_min) }, + { 's', DIGITS, ACCUM, offsetof(struct match_state, base.tm_sec) }, + { 'u', DIGITS, MICRO, offsetof(struct match_state, tv.tv_usec) }, + { 'O', DIGITS, ACCUM, offsetof(struct match_state, gmtoff_hours) }, + { 'o', DIGITS, ACCUM, offsetof(struct match_state, gmtoff_minutes) }, + { '+', "-+", TZIND, 0 }, + { 'Z', "Z", TZIND, 0 }, + { ':', ":", NOOP, 0 }, + { '-', "-", NOOP, 0 }, + { 'T', "T", NOOP, 0 }, + { ' ', " ", NOOP, 0 }, + { '.', ".,", NOOP, 0 }, + { '[', NULL, SKIPFROM, 0 }, + { ']', NULL, SKIP, 0 }, + { '\0', NULL, ACCEPT, 0 }, +}; + +/* Return the rule associated with TCHAR, or NULL if there is no such rule. */ +static const struct rule *find_rule( char tchar ) +{ + int i = sizeof(rules)/sizeof(rules[0]); + while( i-- ) + if( rules[i].key == tchar ) + return &rules[i]; + return NULL; +} + +/* + Attempt to match the date-string in VALUE to the provided TEMPLATE, + using the rules defined above. Return TRUE on successful match, + FALSE otherwise. On successful match, fill in *TM with the + matched values and set *LOCALTZ to GMT-offset if the local time zone + should be used to interpret the match. + */ +static int template_match( struct tm *tm, int *localtz, const char *template, const char *value ) +{ + int multiplier = 100000; + int tzind = 0; + struct match_state ms; + char *base = (char *)&ms; + + memset( &ms, 0, sizeof(ms) ); + + for( ;; ) + { + const struct rule *match = find_rule(*template++); + char vchar = *value++; + int *place; + + if( !match || (match->valid && (!vchar || !strchr(match->valid, vchar))) ) + return FALSE; + + /* Compute the address of memory location affected by this + rule by adding match->offset bytes to the address of ms. + Because this is a byte-quantity, it is necessary to cast + &ms to char *. */ + place = (int *)(base + match->offset); + switch( match->action ) + { + case ACCUM: + *place = *place * 10 + vchar - '0'; + continue; + case MICRO: + *place += (vchar - '0') * multiplier; + multiplier /= 10; + continue; + case TZIND: + tzind = vchar; + continue; + case SKIP: + value--; + continue; + case NOOP: + continue; + case SKIPFROM: + if( !vchar ) + break; + match = find_rule(*template); + if (!strchr(match->valid, vchar)) + template = strchr(template, ']') + 1; + value--; + continue; + case ACCEPT: + if( vchar ) + return FALSE; + break; + } + + break; + } + + /* Validate gmt offset here, since we can't reliably do it later. */ + if( ms.gmtoff_hours > 23 || ms.gmtoff_minutes > 59 ) + return FALSE; + + /* + tzind will be '+' or '-' for an explicit time zone, + 'Z' to indicate UTC, or 0 to indicate local time. + */ + switch( tzind ) + { + case '+': + ms.gmtoff = ms.gmtoff_hours * 3600 + ms.gmtoff_minutes * 60; + break; + case '-': + ms.gmtoff = -(ms.gmtoff_hours * 3600 + ms.gmtoff_minutes * 60); + break; + } + + *tm = ms.base; + *localtz = ms.gmtoff; + return TRUE; +} + +static int valid_days_by_month[] = +{ + 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 +}; + +/* + Returns -1 on error, + time_t as the number of seconds since Epoch, 1970-01-01 00:00:00 +0000 (UTC) + on success. + */ +time_t parse_date( struct tm *tm, const char *text ) +{ + time_t n, ret = (time_t)-1; + struct tm pt, *now; + int localtz; + + n = time( NULL ); /* current UTC time */ + now = gmtime( &n ); + + + if( /* ISO-8601 extended, date only: */ + template_match( &pt, &localtz, "YYYY-M[M]-D[D]", text ) || + /* ISO-8601 extended, UTC: */ + template_match( &pt, &localtz, "YYYY-M[M]-D[D]Th[h]:mm[:ss[.u[u[u[u[u[u][Z]", text ) || + /* ISO-8601 extended, with offset: */ + template_match( &pt, &localtz, "YYYY-M[M]-D[D]Th[h]:mm[:ss[.u[u[u[u[u[u]+OO[:oo]", text ) || + /* ISO-8601 basic, date only */ + template_match( &pt, &localtz, "YYYYMMDD", text ) || + /* ISO-8601 basic, UTC: */ + template_match( &pt, &localtz, "YYYYMMDDThhmm[ss[.u[u[u[u[u[u][Z]", text ) || + /* ISO-8601 basic, with offset: */ + template_match( &pt, &localtz, "YYYYMMDDThhmm[ss[.u[u[u[u[u[u]+OO[oo]", text ) || + /* "svn log" format: */ + template_match( &pt, &localtz, "YYYY-M[M]-D[D] h[h]:mm[:ss[.u[u[u[u[u[u][ +OO[oo]", text ) || + /* GNU date's iso-8601: */ + template_match( &pt, &localtz, "YYYY-M[M]-D[D]Th[h]:mm[:ss[.u[u[u[u[u[u]+OO[oo]", text ) ) + { + pt.tm_year -= 1900; + pt.tm_mon -= 1; + } + else if( template_match( &pt, &localtz, "h[h]:mm[:ss[.u[u[u[u[u[u]", text) ) /* Just a time */ + { + pt.tm_year = now->tm_year; + pt.tm_mon = now->tm_mon; + pt.tm_mday = now->tm_mday; + } + + /* Range validation, allowing for leap seconds */ + if( pt.tm_mon < 0 || + pt.tm_mon > 11 || + pt.tm_mday > valid_days_by_month[pt.tm_mon] || + pt.tm_mday < 1 || + pt.tm_hour > 23 || + pt.tm_min > 59 || + pt.tm_sec > 60 ) + return ret; + + /* + february/leap-year day checking. tm_year is bias-1900, so + centuries that equal 100 (mod 400) are multiples of 400. + */ + if( pt.tm_mon == 1 && + pt.tm_mday == 29 && + (pt.tm_year % 4 != 0 || (pt.tm_year % 100 == 0 && pt.tm_year % 400 != 100)) ) + return ret; + + if( localtz ) + { + struct tm *gmt = NULL; + time_t time; + + time = mktime( &pt ); /* brocken-down tm asumed as localtime */ + if( time == -1 ) + return ret; + + time -= (time_t)localtz; + + gmt = localtime( &time ); + if( !gmt ) + return ret; + + memcpy( (void *)&pt, (const void *)gmt, sizeof(struct tm) ); + } + + memcpy( (void *)tm, (const void *)&pt, sizeof(struct tm) ); + + return mktime( &pt ); +} + + +void show_date_relative( struct strbuf *sb, time_t t ) +{ + time_t now, diff; + + if( !sb || !t ) return; + + now = time( NULL ); + if( now < t ) + { + strbuf_addstr( sb, _("in the future") ); + return; + } + diff = now - t; + if( diff < 90 ) + { + strbuf_addf( sb, Q_("%"PRIdMAX" second ago", "%"PRIdMAX" seconds ago", diff), diff ); + return; + } + /* Turn it into minutes */ + diff = (diff + 30) / 60; + if( diff < 90 ) + { + strbuf_addf( sb, Q_("%"PRIdMAX" minute ago", "%"PRIdMAX" minutes ago", diff), diff ); + return; + } + /* Turn it into hours */ + diff = (diff + 30) / 60; + if( diff < 36 ) + { + strbuf_addf( sb, Q_("%"PRIdMAX" hour ago", "%"PRIdMAX" hours ago", diff), diff ); + return; + } + /* We deal with number of days from here on */ + diff = (diff + 12) / 24; + if( diff < 14 ) + { + strbuf_addf( sb, Q_("%"PRIdMAX" day ago", "%"PRIdMAX" days ago", diff), diff ); + return; + } + /* Say weeks for the past 10 weeks or so */ + if( diff < 70 ) + { + strbuf_addf( sb, Q_("%"PRIdMAX" week ago", "%"PRIdMAX" weeks ago", (diff+3)/7), (diff+3)/7 ); + return; + } + /* Say months for the past 12 months or so */ + if( diff < 365 ) + { + strbuf_addf( sb, Q_("%"PRIdMAX" month ago", "%"PRIdMAX" months ago", (diff+15)/30), (diff+15)/30 ); + return; + } + /* Give years and months for 5 years or so */ + if( diff < 1825 ) + { + time_t totalmonths = (diff * 12 * 2 + 365) / (365 * 2); + time_t years = totalmonths / 12; + time_t months = totalmonths % 12; + if( months ) + { + struct strbuf buf = STRBUF_INIT; + strbuf_addf( &buf, Q_("%"PRIdMAX" year", "%"PRIdMAX" years", years), years ); + strbuf_addf( sb, + /* TRANSLATORS: "%s" is "<n> months" */ + Q_("%s, %"PRIdMAX" month ago", "%s, %"PRIdMAX" months ago", months), buf.buf, months ); + strbuf_release( &buf ); + } + else + strbuf_addf( sb, Q_("%"PRIdMAX" year ago", "%"PRIdMAX" years ago", years), years ); + return; + } + /* Otherwise, just years. Centuries is probably overkill. */ + strbuf_addf( sb, Q_("%"PRIdMAX" year ago", "%"PRIdMAX" years ago", (diff+183)/365), (diff+183)/365 ); +} + +struct date_mode *date_mode_from_type( enum date_mode_type type ) +{ + static struct date_mode mode; + mode.type = type; + mode.local = 0; + return &mode; +} + +static const char *month_names[] = { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +}; + +static const char *weekday_names[] = { + "Sundays", "Mondays", "Tuesdays", "Wednesdays", "Thursdays", "Fridays", "Saturdays" +}; + +static time_t gm_time_t( time_t time, int tz ) +{ + int minutes; + + minutes = tz < 0 ? -tz : tz; + minutes = (minutes / 100)*60 + (minutes % 100); + minutes = tz < 0 ? -minutes : minutes; + + time += minutes * 60; + + return time; +} + +static struct tm *time_to_tm( time_t time, int tz, struct tm *tm ) +{ + time_t t = gm_time_t( time, tz ); + return gmtime_r( &t, tm ); +} + +static struct tm *time_to_tm_local( time_t time, struct tm *tm ) +{ + time_t t = time; + return localtime_r( &t, tm ); +} + +/********************************************************** + Fill in the localtime 'struct tm' for the supplied time, + and return the local tz. + */ +static int local_time_tzoffset( time_t t, struct tm *tm ) +{ + time_t t_local; + int offset, eastwest; + + localtime_r( &t, tm ); + t_local = mktime( tm ); + if( t_local == -1 ) + return 0; /* error; just use +0000 */ + if( t_local < t ) + { + eastwest = -1; + offset = t - t_local; + } + else + { + eastwest = 1; + offset = t_local - t; + } + offset /= 60; /* in minutes */ + offset = (offset % 60) + ((offset / 60) * 100); + return offset * eastwest; +} + +static int local_tzoffset( time_t time ) +{ + struct tm tm; + return local_time_tzoffset( time, &tm ); +} + + +static void show_date_normal( struct strbuf *sb, + time_t time, struct tm *tm, int tz, + struct tm *human_tm, int human_tz, int local ) +{ + struct + { + unsigned int year:1, + date:1, + wday:1, + time:1, + seconds:1, + tz:1; + } hide = { 0 }; + + hide.tz = local || tz == human_tz; + hide.year = tm->tm_year == human_tm->tm_year; + if( hide.year ) + { + if( tm->tm_mon == human_tm->tm_mon ) + { + if( tm->tm_mday > human_tm->tm_mday ) + { + /* Future date: think timezones */ + } + else if( tm->tm_mday == human_tm->tm_mday ) + { + hide.date = hide.wday = 1; + } + else if( tm->tm_mday + 5 > human_tm->tm_mday ) + { + /* Leave just weekday if it was a few days ago */ + hide.date = 1; + } + } + } + + /* Show "today" times as just relative times */ + if( hide.wday ) + { + show_date_relative( sb, time ); + return; + } + + /****************************************************** + Always hide seconds for human-readable. + Hide timezone if showing date. + Hide weekday and time if showing year. + + The logic here is two-fold: + (a) only show details when recent enough to matter + (b) keep the maximum length "similar", and in check + ******************************************************/ + if( human_tm->tm_year ) + { + hide.seconds = 1; + hide.tz |= !hide.date; + hide.wday = hide.time = !hide.year; + } + + if( !hide.wday ) + strbuf_addf( sb, "%.3s ", weekday_names[tm->tm_wday] ); + if( !hide.date ) + strbuf_addf( sb, "%.3s %d ", month_names[tm->tm_mon], tm->tm_mday ); + + /* Do we want AM/PM depending on locale? */ + if( !hide.time ) + { + strbuf_addf( sb, "%02d:%02d", tm->tm_hour, tm->tm_min ); + if( !hide.seconds ) + strbuf_addf( sb, ":%02d", tm->tm_sec ); + } + else + strbuf_rtrim( sb ); + + if( !hide.year ) + strbuf_addf( sb, " %d", tm->tm_year + 1900 ); + + if( !hide.tz ) + strbuf_addf( sb, " %+05d", tz ); +} + +void show_date( struct strbuf *sb, time_t t, int tz, const struct date_mode *mode ) +{ + struct tm *tm; + struct tm tmbuf = { 0 }; + struct tm human_tm = { 0 }; + int human_tz = -1; + + if( mode->type == DATE_UNIX ) + { + strbuf_addf( sb, "%"PRIdMAX, t ); + } + + if( mode->type == DATE_HUMAN ) + { + time_t now = time( NULL ); + + /* Fill in the data for "current time" in human_tz and human_tm */ + human_tz = local_time_tzoffset( now, &human_tm ); + } + + if( mode->local ) + tz = local_tzoffset( t ); + + if( mode->type == DATE_RAW ) + { + strbuf_addf( sb, "%"PRIdMAX" %+05d", t, tz ); + } + + if( mode->type == DATE_RELATIVE ) + { + show_date_relative( sb, t ); + } + + if( mode->local ) + tm = time_to_tm_local( t, &tmbuf ); + else + tm = time_to_tm( t, tz, &tmbuf ); + if (!tm) { + tm = time_to_tm( 0, 0, &tmbuf ); + tz = 0; + } + + if( mode->type == DATE_SHORT ) + strbuf_addf( sb, "%04d-%02d-%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday ); + else if( mode->type == DATE_ISO8601 ) + strbuf_addf( sb, "%04d-%02d-%02d %02d:%02d:%02d %+05d", + tm->tm_year + 1900, + tm->tm_mon + 1, + tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec, + tz ); + else if( mode->type == DATE_ISO8601_STRICT ) + { + char sign = (tz >= 0) ? '+' : '-'; + tz = abs( tz ); + strbuf_addf( sb, "%04d-%02d-%02dT%02d:%02d:%02d%c%02d:%02d", + tm->tm_year + 1900, + tm->tm_mon + 1, + tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec, + sign, tz / 100, tz % 100 ); + } + else if( mode->type == DATE_RFC2822 ) + strbuf_addf( sb, "%.3s, %d %.3s %d %02d:%02d:%02d %+05d", + weekday_names[tm->tm_wday], tm->tm_mday, + month_names[tm->tm_mon], tm->tm_year + 1900, + tm->tm_hour, tm->tm_min, tm->tm_sec, tz ); + else + show_date_normal( sb, t, tm, tz, &human_tm, human_tz, mode->local ); +}