/*      $NetBSD: ntp_leapsec.c,v 1.8 2024/08/18 20:47:17 christos Exp $ */

/*
* ntp_leapsec.c - leap second processing for NTPD
*
* Written by Juergen Perlinger ([email protected]) for the NTP project.
* The contents of 'html/copyright.html' apply.
* ----------------------------------------------------------------------
* This is an attempt to get the leap second handling into a dedicated
* module to make the somewhat convoluted logic testable.
*/

#include <config.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <ctype.h>

#include "ntp.h"
#include "ntp_stdlib.h"
#include "ntp_calendar.h"
#include "ntp_leapsec.h"
#include "vint64ops.h"

#include "isc/sha1.h"

static const char * const logPrefix = "leapsecond file";

/* ---------------------------------------------------------------------
* Our internal data structure
*/
#define MAX_HIST 10     /* history of leap seconds */

struct leap_info {
       vint64   ttime; /* transition time (after the step, ntp scale) */
       uint32_t stime; /* schedule limit (a month before transition)  */
       int16_t  taiof; /* TAI offset on and after the transition      */
       uint8_t  dynls; /* dynamic: inserted on peer/clock request     */
};
typedef struct leap_info leap_info_t;

struct leap_head {
       vint64   update; /* time of information update                 */
       vint64   expire; /* table expiration time                      */
       uint16_t size;   /* number of infos in table                   */
       int16_t  base_tai;      /* total leaps before first entry      */
       int16_t  this_tai;      /* current TAI offset                  */
       int16_t  next_tai;      /* TAI offset after 'when'             */
       vint64   dtime;  /* due time (current era end)                 */
       vint64   ttime;  /* nominal transition time (next era start)   */
       vint64   stime;  /* schedule time (when we take notice)        */
       vint64   ebase;  /* base time of this leap era                 */
       uint8_t  dynls;  /* next leap is dynamic (by peer request)     */
};
typedef struct leap_head leap_head_t;

struct leap_table {
       leap_signature_t lsig;
       leap_head_t      head;
       leap_info_t      info[MAX_HIST];
};

/* Where we store our tables */
static leap_table_t _ltab[2], *_lptr;
static int/*BOOL*/  _electric;

/* Forward decls of local helpers */
static int              add_range       (leap_table_t *, const leap_info_t *);
static char *           get_line        (leapsec_reader, void *, char *,
                                        size_t);
static inline char *    skipws          (char *ptr);
static int              parsefail       (const char *cp, const char *ep);
static void             reload_limits   (leap_table_t *, const vint64 *);
static void             fetch_leap_era  (leap_era_t *, const leap_table_t *,
                                        const vint64 *);
static int              betweenu32      (u_int32, u_int32, u_int32);
static void             reset_times     (leap_table_t *);
static int              leapsec_add     (leap_table_t *, const vint64 *, int);
static int              leapsec_raw     (leap_table_t *, const vint64 *, int,
                                        int);
static const char *     lstostr         (const vint64 *ts);

/* =====================================================================
* Get & Set the current leap table
*/

/* ------------------------------------------------------------------ */
leap_table_t *
leapsec_get_table(
       int alternate)
{
       leap_table_t *p1, *p2;

       p1 = _lptr;
       if (p1 == &_ltab[0]) {
               p2 = &_ltab[1];
       } else if (p1 == &_ltab[1]) {
               p2 = &_ltab[0];
       } else {
               p1 = &_ltab[0];
               p2 = &_ltab[1];
               reset_times(p1);
               reset_times(p2);
               _lptr = p1;
       }
       if (alternate) {
               memcpy(p2, p1, sizeof(leap_table_t));
               p1 = p2;
       }

       return p1;
}

/* ------------------------------------------------------------------ */
int/*BOOL*/
leapsec_set_table(
       leap_table_t * pt)
{
       if (pt == &_ltab[0] || pt == &_ltab[1])
               _lptr = pt;
       return _lptr == pt;
}

/* ------------------------------------------------------------------ */
int/*BOOL*/
leapsec_electric(
       int/*BOOL*/ on)
{
       int res = _electric;
       if (on < 0)
               return res;

       _electric = (on != 0);
       if (_electric == res)
               return res;

       if (_lptr == &_ltab[0] || _lptr == &_ltab[1])
               reset_times(_lptr);

       return res;
}

/* =====================================================================
* API functions that operate on tables
*/

/* ---------------------------------------------------------------------
* Clear all leap second data. Use it for init & cleanup
*/
void
leapsec_clear(
       leap_table_t * pt)
{
       memset(&pt->lsig, 0, sizeof(pt->lsig));
       memset(&pt->head, 0, sizeof(pt->head));
       reset_times(pt);
}

/* ---------------------------------------------------------------------
* Load a leap second file and check expiration on the go
*/
int/*BOOL*/
leapsec_load(
       leap_table_t *  pt,
       leapsec_reader  func,
       void *          farg,
       int             use_build_limit
       )
{
       char            *cp, *ep, *endp, linebuf[50];
       vint64          ttime, limit;
       long            taiof;
       struct calendar build;

       leapsec_clear(pt);
       if (use_build_limit && ntpcal_get_build_date(&build)) {
               /* don't prune everything -- permit the last 10yrs
                * before build.
                */
               build.year -= 10;
               limit = ntpcal_date_to_ntp64(&build);
       } else {
               memset(&limit, 0, sizeof(limit));
       }

       while (get_line(func, farg, linebuf, sizeof(linebuf))) {
               cp = linebuf;
               if (*cp == '#') {
                       cp++;
                       if (*cp == '@') {
                               cp = skipws(cp+1);
                               pt->head.expire = strtouv64(cp, &ep, 10);
                               if (parsefail(cp, ep))
                                       goto fail_read;
                               pt->lsig.etime = pt->head.expire.D_s.lo;
                       } else if (*cp == '$') {
                               cp = skipws(cp+1);
                               pt->head.update = strtouv64(cp, &ep, 10);
                               if (parsefail(cp, ep))
                                       goto fail_read;
                       }
               } else if (isdigit((u_char)*cp)) {
                       ttime = strtouv64(cp, &ep, 10);
                       if (parsefail(cp, ep))
                               goto fail_read;
                       cp = skipws(ep);
                       taiof = strtol(cp, &endp, 10);
                       if (   parsefail(cp, endp)
                           || taiof > INT16_MAX || taiof < INT16_MIN)
                               goto fail_read;
                       if (ucmpv64(&ttime, &limit) >= 0) {
                               if (!leapsec_raw(pt, &ttime,
                                                taiof, FALSE))
                                       goto fail_insn;
                       } else {
                               pt->head.base_tai = (int16_t)taiof;
                       }
                       pt->lsig.ttime = ttime.D_s.lo;
                       pt->lsig.taiof = (int16_t)taiof;
               }
       }
       return TRUE;

fail_read:
       errno = EILSEQ;
fail_insn:
       leapsec_clear(pt);
       return FALSE;
}

/* ---------------------------------------------------------------------
* Dump a table in human-readable format. Use 'fprintf' and a FILE
* pointer if you want to get it printed into a stream.
*/
void
leapsec_dump(
       const leap_table_t * pt  ,
       leapsec_dumper       func,
       void *               farg)
{
       int             idx;
       vint64          ts;
       struct calendar atb, ttb;

       ntpcal_ntp64_to_date(&ttb, &pt->head.expire);
       (*func)(farg, "leap table (%u entries) expires at %04u-%02u-%02u:\n",
               pt->head.size,
               ttb.year, ttb.month, ttb.monthday);
       idx = pt->head.size;
       while (idx-- != 0) {
               ts = pt->info[idx].ttime;
               ntpcal_ntp64_to_date(&ttb, &ts);
               ts = subv64u32(&ts, pt->info[idx].stime);
               ntpcal_ntp64_to_date(&atb, &ts);

               (*func)(farg, "%04u-%02u-%02u [%c] (%04u-%02u-%02u) - %d\n",
                       ttb.year, ttb.month, ttb.monthday,
                       "-*"[pt->info[idx].dynls != 0],
                       atb.year, atb.month, atb.monthday,
                       pt->info[idx].taiof);
       }
}

/* =====================================================================
* usecase driven API functions
*/

int/*BOOL*/
leapsec_query(
       leap_result_t * qr   ,
       uint32_t        ts32 ,
       const time_t *  pivot)
{
       leap_table_t *   pt;
       vint64           ts64, last, next;
       uint32_t         due32;
       int              fired;

       /* preset things we use later on... */
       fired = FALSE;
       ts64  = ntpcal_ntp_to_ntp(ts32, pivot);
       pt    = leapsec_get_table(FALSE);
       memset(qr, 0, sizeof(leap_result_t));

       if (ucmpv64(&ts64, &pt->head.ebase) < 0) {
               /* Most likely after leap frame reset. Could also be a
                * backstep of the system clock. Anyway, get the new
                * leap era frame.
                */
               reload_limits(pt, &ts64);
       } else if (ucmpv64(&ts64, &pt->head.dtime) >= 0) {
               /* Boundary crossed in forward direction. This might
                * indicate a leap transition, so we prepare for that
                * case.
                *
                * Some operations below are actually NOPs in electric
                * mode, but having only one code path that works for
                * both modes is easier to maintain.
                *
                * There's another quirk we must keep looking out for:
                * If we just stepped the clock, the step might have
                * crossed a leap boundary. As with backward steps, we
                * do not want to raise the 'fired' event in that case.
                * So we raise the 'fired' event only if we're close to
                * the transition and just reload the limits otherwise.
                */
               last = addv64i32(&pt->head.dtime, 3); /* get boundary */
               if (ucmpv64(&ts64, &last) >= 0) {
                       /* that was likely a query after a step */
                       reload_limits(pt, &ts64);
               } else {
                       /* close enough for deeper examination */
                       last = pt->head.ttime;
                       qr->warped = (int16_t)(last.D_s.lo -
                                              pt->head.dtime.D_s.lo);
                       next = addv64i32(&ts64, qr->warped);
                       reload_limits(pt, &next);
                       fired = ucmpv64(&pt->head.ebase, &last) == 0;
                       if (fired) {
                               ts64 = next;
                               ts32 = next.D_s.lo;
                       } else {
                               qr->warped = 0;
                       }
               }
       }

       qr->tai_offs = pt->head.this_tai;
       qr->ebase    = pt->head.ebase;
       qr->ttime    = pt->head.ttime;

       /* If before the next scheduling alert, we're done. */
       if (ucmpv64(&ts64, &pt->head.stime) < 0)
               return fired;

       /* now start to collect the remaining data */
       due32 = pt->head.dtime.D_s.lo;

       qr->tai_diff  = pt->head.next_tai - pt->head.this_tai;
       qr->ddist     = due32 - ts32;
       qr->dynamic   = pt->head.dynls;
       qr->proximity = LSPROX_SCHEDULE;

       /* if not in the last day before transition, we're done. */
       if (!betweenu32(due32 - SECSPERDAY, ts32, due32))
               return fired;

       qr->proximity = LSPROX_ANNOUNCE;
       if (!betweenu32(due32 - 10, ts32, due32))
               return fired;

       /* The last 10s before the transition. Prepare for action! */
       qr->proximity = LSPROX_ALERT;
       return fired;
}

/* ------------------------------------------------------------------ */
int/*BOOL*/
leapsec_query_era(
       leap_era_t *   qr   ,
       uint32_t       ntpts,
       const time_t * pivot)
{
       const leap_table_t * pt;
       vint64               ts64;

       pt   = leapsec_get_table(FALSE);
       ts64 = ntpcal_ntp_to_ntp(ntpts, pivot);
       fetch_leap_era(qr, pt, &ts64);
       return TRUE;
}

/* ------------------------------------------------------------------ */
int/*BOOL*/
leapsec_frame(
       leap_result_t *qr)
{
       const leap_table_t * pt;

       memset(qr, 0, sizeof(leap_result_t));
       pt = leapsec_get_table(FALSE);

       qr->tai_offs = pt->head.this_tai;
       qr->tai_diff = pt->head.next_tai - pt->head.this_tai;
       qr->ebase    = pt->head.ebase;
       qr->ttime    = pt->head.ttime;
       qr->dynamic  = pt->head.dynls;

       return ucmpv64(&pt->head.ttime, &pt->head.stime) >= 0;
}

/* ------------------------------------------------------------------ */
/* Reset the current leap frame */
void
leapsec_reset_frame(void)
{
       reset_times(leapsec_get_table(FALSE));
}

/* ------------------------------------------------------------------ */
/* load a file from a FILE pointer. Note: If vhash is true, load
* only after successful signature check. The stream must be seekable
* or this will fail.
*/
int/*BOOL*/
leapsec_load_stream(
       FILE       * ifp  ,
       const char * fname,
       int/*BOOL*/  logall,
       int/*BOOL*/  vhash)
{
       leap_table_t *pt;
       int           rcheck;

       if (NULL == fname)
               fname = "<unknown>";

       if (vhash) {
               rcheck = leapsec_validate((leapsec_reader)&getc, ifp);
               if (logall)
                       switch (rcheck)
                       {
                       case LSVALID_GOODHASH:
                               msyslog(LOG_NOTICE, "%s ('%s'): good hash signature",
                                       logPrefix, fname);
                               break;

                       case LSVALID_NOHASH:
                               msyslog(LOG_ERR, "%s ('%s'): no hash signature",
                                       logPrefix, fname);
                               break;
                       case LSVALID_BADHASH:
                               msyslog(LOG_ERR, "%s ('%s'): signature mismatch",
                                       logPrefix, fname);
                               break;
                       case LSVALID_BADFORMAT:
                               msyslog(LOG_ERR, "%s ('%s'): malformed hash signature",
                                       logPrefix, fname);
                               break;
                       default:
                               msyslog(LOG_ERR, "%s ('%s'): unknown error code %d",
                                       logPrefix, fname, rcheck);
                               break;
                       }
               if (rcheck < 0)
                       return FALSE;
               rewind(ifp);
       }
       pt = leapsec_get_table(TRUE);
       if (!leapsec_load(pt, (leapsec_reader)getc, ifp, TRUE)) {
               switch (errno) {
               case EINVAL:
                       msyslog(LOG_ERR, "%s ('%s'): bad transition time",
                               logPrefix, fname);
                       break;
               case ERANGE:
                       msyslog(LOG_ERR, "%s ('%s'): times not ascending",
                               logPrefix, fname);
                       break;
               default:
                       msyslog(LOG_ERR, "%s ('%s'): parsing error",
                               logPrefix, fname);
                       break;
               }
               return FALSE;
       }

       if (pt->head.size)
               msyslog(LOG_NOTICE, "%s ('%s'): loaded, expire=%s last=%s ofs=%d",
                       logPrefix, fname, lstostr(&pt->head.expire),
                       lstostr(&pt->info[0].ttime), pt->info[0].taiof);
       else
               msyslog(LOG_NOTICE,
                       "%s ('%s'): loaded, expire=%s ofs=%d (no entries after build date)",
                       logPrefix, fname, lstostr(&pt->head.expire),
                       pt->head.base_tai);

       return leapsec_set_table(pt);
}

/* ------------------------------------------------------------------ */
int/*BOOL*/
leapsec_load_file(
       const char  * fname,
       struct stat * sb_old,
       int/*BOOL*/   force,
       int/*BOOL*/   logall,
       int/*BOOL*/   vhash)
{
       FILE       * fp;
       struct stat  sb_new;
       int          rc;

       /* just do nothing if there is no leap file */
       if ( !(fname && *fname) )
               return FALSE;

       /* try to stat the leapfile */
       if (0 != stat(fname, &sb_new)) {
               if (logall)
                       msyslog(LOG_ERR, "%s ('%s'): stat failed: %m",
                               logPrefix, fname);
               return FALSE;
       }

       /* silently skip to postcheck if no new file found */
       if (NULL != sb_old) {
               if (!force
                && sb_old->st_mtime == sb_new.st_mtime
                && sb_old->st_ctime == sb_new.st_ctime
                  )
                       return FALSE;
               *sb_old = sb_new;
       }

       /* try to open the leap file, complain if that fails
        *
        * [[email protected]]
        * coverity raises a TOCTOU (time-of-check/time-of-use) issue
        * here, which is not entirely helpful: While there is indeed a
        * possible race condition between the 'stat()' call above and
        * the 'fopen)' call below, I intentionally want to omit the
        * overhead of opening the file and calling 'fstat()', because
        * in most cases the file would have be to closed anyway without
        * reading the contents.  I chose to disable the coverity
        * warning instead.
        *
        * So unless someone comes up with a reasonable argument why
        * this could be a real issue, I'll just try to silence coverity
        * on that topic.
        */
       /* coverity[toctou] */
       if ((fp = fopen(fname, "r")) == NULL) {
               if (logall)
                       msyslog(LOG_ERR,
                               "%s ('%s'): open failed: %m",
                               logPrefix, fname);
               return FALSE;
       }

       rc = leapsec_load_stream(fp, fname, logall, vhash);
       fclose(fp);
       return rc;
}

/* ------------------------------------------------------------------ */
void
leapsec_getsig(
       leap_signature_t * psig)
{
       const leap_table_t * pt;

       pt = leapsec_get_table(FALSE);
       memcpy(psig, &pt->lsig, sizeof(leap_signature_t));
}

/* ------------------------------------------------------------------ */
int/*BOOL*/
leapsec_expired(
       uint32_t       when,
       const time_t * tpiv)
{
       const leap_table_t * pt;
       vint64 limit;

       pt = leapsec_get_table(FALSE);
       limit = ntpcal_ntp_to_ntp(when, tpiv);
       return ucmpv64(&limit, &pt->head.expire) >= 0;
}

/* ------------------------------------------------------------------ */
int32_t
leapsec_daystolive(
       uint32_t       when,
       const time_t * tpiv)
{
       const leap_table_t * pt;
       vint64 limit;

       pt = leapsec_get_table(FALSE);
       limit = ntpcal_ntp_to_ntp(when, tpiv);
       limit = subv64(&pt->head.expire, &limit);
       return ntpcal_daysplit(&limit).hi;
}

/* ------------------------------------------------------------------ */
#if 0 /* currently unused -- possibly revived later */
int/*BOOL*/
leapsec_add_fix(
       int            total,
       uint32_t       ttime,
       uint32_t       etime,
       const time_t * pivot)
{
       time_t         tpiv;
       leap_table_t * pt;
       vint64         tt64, et64;

       if (pivot == NULL) {
               time(&tpiv);
               pivot = &tpiv;
       }

       et64 = ntpcal_ntp_to_ntp(etime, pivot);
       tt64 = ntpcal_ntp_to_ntp(ttime, pivot);
       pt   = leapsec_get_table(TRUE);

       if (   ucmpv64(&et64, &pt->head.expire) <= 0
          || !leapsec_raw(pt, &tt64, total, FALSE) )
               return FALSE;

       pt->lsig.etime = etime;
       pt->lsig.ttime = ttime;
       pt->lsig.taiof = (int16_t)total;

       pt->head.expire = et64;

       return leapsec_set_table(pt);
}
#endif

/* ------------------------------------------------------------------ */
int/*BOOL*/
leapsec_add_dyn(
       int            insert,
       uint32_t       ntpnow,
       const time_t * pivot )
{
       leap_table_t * pt;
       vint64         now64;

       pt = leapsec_get_table(TRUE);
       now64 = ntpcal_ntp_to_ntp(ntpnow, pivot);
       return (   leapsec_add(pt, &now64, (insert != 0))
               && leapsec_set_table(pt));
}

/* ------------------------------------------------------------------ */
int/*BOOL*/
leapsec_autokey_tai(
       int            tai_offset,
       uint32_t       ntpnow    ,
       const time_t * pivot     )
{
       leap_table_t * pt;
       leap_era_t     era;
       vint64         now64;
       int            idx;

       (void)tai_offset;
       pt = leapsec_get_table(FALSE);

       /* Bail out if the basic offset is not zero and the putative
        * offset is bigger than 10s. That was in 1972 -- we don't want
        * to go back that far!
        */
       if (pt->head.base_tai != 0 || tai_offset < 10)
               return FALSE;

       /* If there's already data in the table, check if an update is
        * possible. Update is impossible if there are static entries
        * (since this indicates a valid leapsecond file) or if we're
        * too close to a leapsecond transition: We do not know on what
        * side the transition the sender might have been, so we use a
        * dead zone around the transition.
        */

       /* Check for static entries */
       for (idx = 0; idx != pt->head.size; idx++)
               if ( ! pt->info[idx].dynls)
                       return FALSE;

       /* get the fulll time stamp and leap era for it */
       now64 = ntpcal_ntp_to_ntp(ntpnow, pivot);
       fetch_leap_era(&era, pt, &now64);

       /* check the limits with 20s dead band */
       era.ebase = addv64i32(&era.ebase,  20);
       if (ucmpv64(&now64, &era.ebase) < 0)
               return FALSE;

       era.ttime = addv64i32(&era.ttime, -20);
       if (ucmpv64(&now64, &era.ttime) > 0)
               return FALSE;

       /* Here we can proceed. Calculate the delta update. */
       tai_offset -= era.taiof;

       /* Shift the header info offsets. */
       pt->head.base_tai += tai_offset;
       pt->head.this_tai += tai_offset;
       pt->head.next_tai += tai_offset;

       /* Shift table entry offsets (if any) */
       for (idx = 0; idx != pt->head.size; idx++)
               pt->info[idx].taiof += tai_offset;

       /* claim success... */
       return TRUE;
}


/* =====================================================================
* internal helpers
*/

/* [internal] Reset / init the time window in the leap processor to
* force reload on next query. Since a leap transition cannot take place
* at an odd second, the value chosen avoids spurious leap transition
* triggers. Making all three times equal forces a reload. Using the
* maximum value for unsigned 64 bits makes finding the next leap frame
* a bit easier.
*/
static void
reset_times(
       leap_table_t * pt)
{
       memset(&pt->head.ebase, 0xFF, sizeof(vint64));
       pt->head.stime = pt->head.ebase;
       pt->head.ttime = pt->head.ebase;
       pt->head.dtime = pt->head.ebase;
}

/* [internal] Add raw data to the table, removing old entries on the
* fly. This cannot fail currently.
*/
static int/*BOOL*/
add_range(
       leap_table_t *      pt,
       const leap_info_t * pi)
{
       /* If the table is full, make room by throwing out the oldest
        * entry. But remember the accumulated leap seconds!
        *
        * Setting the first entry is a bit tricky, too: Simply assuming
        * it is an insertion is wrong if the first entry is a dynamic
        * leap second removal. So we decide on the sign -- if the first
        * entry has a negative offset, we assume that it is a leap
        * second removal. In both cases the table base offset is set
        * accordingly to reflect the decision.
        *
        * In practice starting with a removal can only happen if the
        * first entry is a dynamic request without having a leap file
        * for the history proper.
        */
       if (pt->head.size == 0) {
               if (pi->taiof >= 0)
                       pt->head.base_tai = pi->taiof - 1;
               else
                       pt->head.base_tai = pi->taiof + 1;
       } else if (pt->head.size >= MAX_HIST) {
               pt->head.size     = MAX_HIST - 1;
               pt->head.base_tai = pt->info[pt->head.size].taiof;
       }

       /* make room in lower end and insert item */
       memmove(pt->info+1, pt->info, pt->head.size*sizeof(*pt->info));
       pt->info[0] = *pi;
       pt->head.size++;

       /* invalidate the cached limit data -- we might have news ;-)
        *
        * This blocks a spurious transition detection. OTOH, if you add
        * a value after the last query before a leap transition was
        * expected to occur, this transition trigger is lost. But we
        * can probably live with that.
        */
       reset_times(pt);
       return TRUE;
}

/* [internal] given a reader function, read characters into a buffer
* until either EOL or EOF is reached. Makes sure that the buffer is
* always NUL terminated, but silently truncates excessive data. The
* EOL-marker ('\n') is *not* stored in the buffer.
*
* Returns the pointer to the buffer, unless EOF was reached when trying
* to read the first character of a line.
*/
static char *
get_line(
       leapsec_reader func,
       void *         farg,
       char *         buff,
       size_t         size)
{
       int   ch;
       char *ptr;

       /* if we cannot even store the delimiter, declare failure */
       if (buff == NULL || size == 0)
               return NULL;

       ptr = buff;
       while (EOF != (ch = (*func)(farg)) && '\n' != ch)
               if (size > 1) {
                       size--;
                       *ptr++ = (char)ch;
               }
       /* discard trailing whitespace */
       while (ptr != buff && isspace((u_char)ptr[-1]))
               ptr--;
       *ptr = '\0';
       return (ptr == buff && ch == EOF) ? NULL : buff;
}

/* [internal] skips whitespace characters from a character buffer. */
static inline char *
skipws(
       char *  ptr
       )
{
       while (isspace((u_char)*ptr)) {
               ptr++;
       }
       return ptr;
}

/* [internal] check if a strtoXYZ ended at EOL or whitespace and
* converted something at all. Return TRUE if something went wrong.
*/
static int/*BOOL*/
parsefail(
       const char * cp,
       const char * ep)
{
       return (cp == ep)
           || (*ep && *ep != '#' && !isspace((u_char)*ep));
}

/* [internal] reload the table limits around the given time stamp. This
* is where the real work is done when it comes to table lookup and
* evaluation. Some care has been taken to have correct code for dealing
* with boundary conditions and empty tables.
*
* In electric mode, transition and trip time are the same. In dumb
* mode, the difference of the TAI offsets must be taken into account
* and trip time and transition time become different. The difference
* becomes the warping distance when the trip time is reached.
*/
static void
reload_limits(
       leap_table_t * pt,
       const vint64 * ts)
{
       int idx;

       /* Get full time and search the true lower bound. Use a
        * simple loop here, since the number of entries does
        * not warrant a binary search. This also works for an empty
        * table, so there is no shortcut for that case.
        */
       for (idx = 0; idx != pt->head.size; idx++)
               if (ucmpv64(ts, &pt->info[idx].ttime) >= 0)
                       break;

       /* get time limits with proper bound conditions. Note that the
        * bounds of the table will be observed even if the table is
        * empty -- no undefined condition must arise from this code.
        */
       if (idx >= pt->head.size) {
               memset(&pt->head.ebase, 0x00, sizeof(vint64));
               pt->head.this_tai = pt->head.base_tai;
       } else {
               pt->head.ebase    = pt->info[idx].ttime;
               pt->head.this_tai = pt->info[idx].taiof;
       }
       if (--idx >= 0) {
               pt->head.next_tai = pt->info[idx].taiof;
               pt->head.dynls    = pt->info[idx].dynls;
               pt->head.ttime    = pt->info[idx].ttime;

               if (_electric)
                       pt->head.dtime = pt->head.ttime;
               else
                       pt->head.dtime = addv64i32(
                               &pt->head.ttime,
                               pt->head.next_tai - pt->head.this_tai);

               pt->head.stime = subv64u32(
                       &pt->head.ttime, pt->info[idx].stime);

       } else {
               memset(&pt->head.ttime, 0xFF, sizeof(vint64));
               pt->head.stime    = pt->head.ttime;
               pt->head.dtime    = pt->head.ttime;
               pt->head.next_tai = pt->head.this_tai;
               pt->head.dynls    = 0;
       }
}

/* [internal] fetch the leap era for a given time stamp.
* This is a cut-down version the algorithm used to reload the table
* limits, but it does not update any global state and provides just the
* era information for a given time stamp.
*/
static void
fetch_leap_era(
       leap_era_t         * into,
       const leap_table_t * pt  ,
       const vint64       * ts  )
{
       int idx;

       /* Simple search loop, also works with empty table. */
       for (idx = 0; idx != pt->head.size; idx++)
               if (ucmpv64(ts, &pt->info[idx].ttime) >= 0)
                       break;
       /* fetch era data, keeping an eye on boundary conditions */
       if (idx >= pt->head.size) {
               memset(&into->ebase, 0x00, sizeof(vint64));
               into->taiof = pt->head.base_tai;
       } else {
               into->ebase = pt->info[idx].ttime;
               into->taiof = pt->info[idx].taiof;
       }
       if (--idx >= 0)
               into->ttime = pt->info[idx].ttime;
       else
               memset(&into->ttime, 0xFF, sizeof(vint64));
}

/* [internal] Take a time stamp and create a leap second frame for
* it. This will schedule a leap second for the beginning of the next
* month, midnight UTC. The 'insert' argument tells if a leap second is
* added (!=0) or removed (==0). We do not handle multiple inserts
* (yet?)
*
* Returns 1 if the insert worked, 0 otherwise. (It's not possible to
* insert a leap second into the current history -- only appending
* towards the future is allowed!)
*/
static int/*BOOL*/
leapsec_add(
       leap_table_t*  pt    ,
       const vint64 * now64 ,
       int            insert)
{
       vint64          ttime, starttime;
       struct calendar fts;
       leap_info_t     li;

       /* Check against the table expiration and the latest available
        * leap entry. Do not permit inserts, only appends, and only if
        * the extend the table beyond the expiration!
        */
       if (   ucmpv64(now64, &pt->head.expire) < 0
           || (pt->head.size && ucmpv64(now64, &pt->info[0].ttime) <= 0)) {
               errno = ERANGE;
               return FALSE;
       }

       ntpcal_ntp64_to_date(&fts, now64);
       /* To guard against dangling leap flags: do not accept leap
        * second request on the 1st hour of the 1st day of the month.
        */
       if (fts.monthday == 1 && fts.hour == 0) {
               errno = EINVAL;
               return FALSE;
       }

       /* Ok, do the remaining calculations */
       fts.monthday = 1;
       fts.hour     = 0;
       fts.minute   = 0;
       fts.second   = 0;
       starttime = ntpcal_date_to_ntp64(&fts);
       fts.month++;
       ttime = ntpcal_date_to_ntp64(&fts);

       li.ttime = ttime;
       li.stime = ttime.D_s.lo - starttime.D_s.lo;
       li.taiof = (pt->head.size ? pt->info[0].taiof : pt->head.base_tai)
                + (insert ? 1 : -1);
       li.dynls = 1;
       return add_range(pt, &li);
}

/* [internal] Given a time stamp for a leap insertion (the exact begin
* of the new leap era), create new leap frame and put it into the
* table. This is the work horse for reading a leap file and getting a
* leap second update via authenticated network packet.
*/
int/*BOOL*/
leapsec_raw(
       leap_table_t * pt,
       const vint64 * ttime,
       int            taiof,
       int            dynls)
{
       vint64          starttime;
       struct calendar fts;
       leap_info_t     li;

       /* Check that we either extend the table or get a duplicate of
        * the latest entry. The latter is a benevolent overwrite with
        * identical data and could happen if we get an autokey message
        * that extends the lifetime of the current leapsecond table.
        * Otherwise paranoia rulez!
        */
       if (pt->head.size) {
               int cmp = ucmpv64(ttime, &pt->info[0].ttime);
               if (cmp == 0)
                       cmp -= (taiof != pt->info[0].taiof);
               if (cmp < 0) {
                       errno = ERANGE;
                       return FALSE;
               }
               if (cmp == 0)
                       return TRUE;
       }

       ntpcal_ntp64_to_date(&fts, ttime);
       /* If this does not match the exact month start, bail out. */
       if (fts.monthday != 1 || fts.hour || fts.minute || fts.second) {
               errno = EINVAL;
               return FALSE;
       }
       fts.month--; /* was in range 1..12, no overflow here! */
       starttime = ntpcal_date_to_ntp64(&fts);
       li.ttime = *ttime;
       li.stime = ttime->D_s.lo - starttime.D_s.lo;
       li.taiof = (int16_t)taiof;
       li.dynls = (dynls != 0);
       return add_range(pt, &li);
}

/* [internal] Do a wrap-around save range inclusion check.
* Returns TRUE if x in [lo,hi[ (intervall open on right side) with full
* handling of an overflow / wrap-around.
*/
static int/*BOOL*/
betweenu32(
       uint32_t lo,
       uint32_t x,
       uint32_t hi)
{
       int rc;

       if (lo <= hi)
               rc = (lo <= x) && (x < hi);
       else
               rc = (lo <= x) || (x < hi);
       return rc;
}

/* =====================================================================
* validation stuff
*/

typedef struct {
       unsigned char hv[ISC_SHA1_DIGESTLENGTH];
} sha1_digest;

/* [internal] parse a digest line to get the hash signature
* The NIST code creating the hash writes them out as 5 hex integers
* without leading zeros. This makes reading them back as hex-encoded
* BLOB impossible, because there might be less than 40 hex digits.
*
* The solution is to read the values back as integers, and then do the
* byte twiddle necessary to get it into an array of 20 chars. The
* drawback is that it permits any acceptable number syntax provided by
* 'scanf()' and 'strtoul()', including optional signs and '0x'
* prefixes.
*/
static int/*BOOL*/
do_leap_hash(
       sha1_digest * mac,
       char const  * cp )
{
       int wi, di, num, len;
       unsigned long tmp[5];

       memset(mac, 0, sizeof(*mac));
       num = sscanf(cp, " %lx %lx %lx %lx %lx%n",
                    &tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4],
                    &len);
       if (num != 5 || cp[len] > ' ')
               return FALSE;

       /* now do the byte twiddle */
       for (wi=0; wi < 5; ++wi)
               for (di=3; di >= 0; --di) {
                       mac->hv[wi*4 + di] =
                               (unsigned char)(tmp[wi] & 0x0FF);
                       tmp[wi] >>= 8;
               }
       return TRUE;
}

/* [internal] add the digits of a data line to the hash, stopping at the
* next hash ('#') character.
*/
static void
do_hash_data(
       isc_sha1_t * mdctx,
       char const * cp   )
{
       unsigned char  text[32]; // must be power of two!
       unsigned int   tlen =  0;
       unsigned char  ch;

       while ('\0' != (ch = *cp++) && '#' != ch)
               if (isdigit(ch)) {
                       text[tlen++] = ch;
                       tlen &= (sizeof(text)-1);
                       if (0 == tlen)
                               isc_sha1_update(
                                       mdctx, text, sizeof(text));
               }

       if (0 < tlen)
               isc_sha1_update(mdctx, text, tlen);
}

/* given a reader and a reader arg, calculate and validate the the hash
* signature of a NIST leap second file.
*/
int
leapsec_validate(
       leapsec_reader func,
       void *         farg)
{
       isc_sha1_t     mdctx;
       sha1_digest    rdig, ldig; /* remote / local digests */
       char           line[50];
       int            hlseen = -1;

       isc_sha1_init(&mdctx);
       while (get_line(func, farg, line, sizeof(line))) {
               if (!strncmp(line, "#h", 2))
                       hlseen = do_leap_hash(&rdig, line+2);
               else if (!strncmp(line, "#@", 2))
                       do_hash_data(&mdctx, line+2);
               else if (!strncmp(line, "#$", 2))
                       do_hash_data(&mdctx, line+2);
               else if (isdigit((unsigned char)line[0]))
                       do_hash_data(&mdctx, line);
       }
       isc_sha1_final(&mdctx, ldig.hv);
       isc_sha1_invalidate(&mdctx);

       if (0 > hlseen)
               return LSVALID_NOHASH;
       if (0 == hlseen)
               return LSVALID_BADFORMAT;
       if (0 != memcmp(&rdig, &ldig, sizeof(sha1_digest)))
               return LSVALID_BADHASH;
       return LSVALID_GOODHASH;
}

/*
* lstostr - prettyprint NTP seconds
*/
static const char *
lstostr(
       const vint64 * ts)
{
       char *          buf;
       struct calendar tm;

       LIB_GETBUF(buf);

       if ( ! (ts->d_s.hi >= 0 && ntpcal_ntp64_to_date(&tm, ts) >= 0))
               snprintf(buf, LIB_BUFLENGTH, "%s", "9999-12-31T23:59:59Z");
       else
               snprintf(buf, LIB_BUFLENGTH, "%04d-%02d-%02dT%02d:%02d:%02dZ",
                       tm.year, tm.month, tm.monthday,
                       tm.hour, tm.minute, tm.second);

       return buf;
}

/* reset the global state for unit tests */
void
leapsec_ut_pristine(void)
{
       memset(_ltab, 0, sizeof(_ltab));
       _lptr     = NULL;
       _electric = 0;
}



/* -*- that's all folks! -*- */