summaryrefslogtreecommitdiff
path: root/csvncgi/date.c
diff options
context:
space:
mode:
authorkx <kx@radix.pro>2023-03-24 03:55:33 +0300
committerkx <kx@radix.pro>2023-03-24 03:55:33 +0300
commitbfc1508d26c89c9a36d2d9a827fe2c4ed128884d (patch)
tree8d41298a7072a3e289e4912f77ece75cbea1bd54 /csvncgi/date.c
parentc836ae3775cf72f17e0b7e3792d156fdb389bee3 (diff)
downloadcsvn-ui-bfc1508d26c89c9a36d2d9a827fe2c4ed128884d.tar.xz
Version 0.1.4
Diffstat (limited to 'csvncgi/date.c')
-rw-r--r--csvncgi/date.c596
1 files changed, 596 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 );
+}