/*      $NetBSD: touch.c,v 1.40 2024/02/10 00:19:30 kre Exp $   */

/*
* Copyright (c) 1993
*      The Regents of the University of California.  All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
* 3. Neither the name of the University nor the names of its contributors
*    may be used to endorse or promote products derived from this software
*    without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/

#include <sys/cdefs.h>
#ifndef lint
__COPYRIGHT("@(#) Copyright (c) 1993\
The Regents of the University of California.  All rights reserved.");
#endif /* not lint */

#ifndef lint
#if 0
static char sccsid[] = "@(#)touch.c     8.2 (Berkeley) 4/28/95";
#endif
__RCSID("$NetBSD: touch.c,v 1.40 2024/02/10 00:19:30 kre Exp $");
#endif /* not lint */

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>
#include <time.h>
#include <tzfile.h>
#include <unistd.h>
#include <util.h>
#include <getopt.h>

static void     stime_arg0(const char *, struct timespec *);
static void     stime_arg1(char *, struct timespec *);
static void     stime_arg2(const char *, int, struct timespec *);
static void     stime_file(const char *, struct timespec *,
                  int (const char *, struct stat *));
static int      stime_posix(const char *, struct timespec *);
static int      difftm(const struct tm *, const struct tm *);
__dead static void      usage(void);

struct option touch_longopts[] = {
       { "date",               required_argument,      0,
                                               'd' },
       { "reference",          required_argument,      0,
                                               'r' },
       { NULL,                 0,                      0,
                                               0 },
};

#define YEAR_BOUNDARY           69
#define LOW_YEAR_CENTURY        2000    /* for 2 digit years < YEAR_BOUNDARY */
#define HIGH_YEAR_CENTURY       1900    /* for 2 digit years >=  "  */

#define NO_TIME         ((time_t)-1)    /* time_t might be unsigned */

int
main(int argc, char *argv[])
{
       struct stat sb;
       struct timespec ts[2];
       int aflag, cflag, Dflag, hflag, mflag, ch, fd, len, rval, timeset;
       char *p;
       int (*change_file_times)(const char *, const struct timespec *);
       int (*get_file_status)(const char *, struct stat *);

       setlocale(LC_ALL, "");

       aflag = cflag = Dflag = hflag = mflag = timeset = 0;
       if (clock_gettime(CLOCK_REALTIME, &ts[0]))
               err(1, "clock_gettime");

       while ((ch = getopt_long(argc, argv, "acDd:fhmR:r:t:", touch_longopts,
           NULL)) != -1)
               switch (ch) {
               case 'a':
                       aflag = 1;
                       break;
               case 'c':
                       cflag = 1;
                       break;
               case 'D':
                       Dflag = 1;
                       break;
               case 'd':
                       timeset = 1;
                       if (!stime_posix(optarg, ts))
                               stime_arg0(optarg, ts);
                       break;
               case 'f':
                       break;
               case 'h':
                       hflag = 1;
                       break;
               case 'm':
                       mflag = 1;
                       break;
               case 'R':
                       timeset = 1;
                       stime_file(optarg, ts, lstat);
                       break;
               case 'r':
                       timeset = 1;
                       stime_file(optarg, ts, stat);
                       break;
               case 't':
                       timeset = 1;
                       stime_arg1(optarg, ts);
                       break;
               case '?':
               default:
                       usage();
               }
       argc -= optind;
       argv += optind;

       /* Default is both -a and -m. */
       if (aflag == 0 && mflag == 0)
               aflag = mflag = 1;

       if (hflag) {
               cflag = 1;              /* Don't create new file */
               change_file_times = lutimens;
               get_file_status = lstat;
       } else {
               change_file_times = utimens;
               get_file_status = stat;
       }

       /*
        * If no -r or -t flag, at least two operands, the first of which
        * is an 8 or 10 digit number, use the obsolete time specification.
        */
       if (!timeset && argc > 1) {
               (void)strtol(argv[0], &p, 10);
               len = p - argv[0];
               if (*p == '\0' && (len == 8 || len == 10)) {
                       timeset = 1;
                       stime_arg2(*argv++, len == 10, ts);
               }
       }

       /* Otherwise use the current time of day. */
       if (!timeset)
               ts[1] = ts[0];

       if (*argv == NULL)
               usage();

       for (rval = EXIT_SUCCESS; *argv; ++argv) {
               /* See if the file exists. */
               if ((*get_file_status)(*argv, &sb)) {
                       if (!cflag) {
                               /* Create the file. */
                               fd = open(*argv,
                                   O_WRONLY | O_CREAT, DEFFILEMODE);
                               if (fd == -1 || fstat(fd, &sb) || close(fd)) {
                                       rval = EXIT_FAILURE;
                                       warn("%s", *argv);
                                       continue;
                               }

                               /* If using the current time, we're done. */
                               if (!timeset)
                                       continue;
                       } else
                               continue;
               }
               if (!aflag)
                       ts[0] = sb.st_atimespec;
               if (!mflag)
                       ts[1] = sb.st_mtimespec;

               if (Dflag &&
                   timespeccmp(&ts[0], &sb.st_atimespec, ==) &&
                   timespeccmp(&ts[1], &sb.st_mtimespec, ==))
                       continue;

               /* Try utimes(2). */
               if (!(*change_file_times)(*argv, ts))
                       continue;

               /* If the user specified a time, nothing else we can do. */
               if (timeset) {
                       rval = EXIT_FAILURE;
                       warn("%s", *argv);
               }

               /*
                * System V and POSIX 1003.1 require that a NULL argument
                * set the access/modification times to the current time.
                * The permission checks are different, too, in that the
                * ability to write the file is sufficient.  Take a shot.
                */
                if (!(*change_file_times)(*argv, NULL))
                       continue;

               rval = EXIT_FAILURE;
               warn("%s", *argv);
       }
       exit(rval);
}

#define ATOI2(s)        ((s) += 2, ((s)[-2] - '0') * 10 + ((s)[-1] - '0'))

static void
stime_arg0(const char *arg, struct timespec *tsp)
{
       tsp[1].tv_sec = tsp[0].tv_sec = parsedate(arg, NULL, NULL);
       if (tsp[0].tv_sec == NO_TIME)
               errx(EXIT_FAILURE, "Could not parse `%s'", arg);
       tsp[0].tv_nsec = tsp[1].tv_nsec = 0;
}

static void
stime_arg1(char *arg, struct timespec *tsp)
{
       struct tm *t, tm;
       time_t tmptime;
       int yearset;
       char *p;
       char *initarg = arg;

                                       /* Start with the current time. */
       tmptime = tsp[0].tv_sec;
       if ((t = localtime(&tmptime)) == NULL)
               err(EXIT_FAILURE, "localtime");
                                       /* [[CC]YY]MMDDhhmm[.ss] */
       if ((p = strchr(arg, '.')) == NULL)
               t->tm_sec = 0;          /* Seconds defaults to 0. */
       else {
               if (strlen(p + 1) != 2)
                       goto terr;
               *p++ = '\0';
               t->tm_sec = ATOI2(p);
       }

       yearset = 0;
       switch (strlen(arg)) {
       case 12:                        /* CCYYMMDDhhmm */
               t->tm_year = ATOI2(arg) * 100 - TM_YEAR_BASE;
               yearset = 1;
               /* FALLTHROUGH */
       case 10:                        /* YYMMDDhhmm */
               if (yearset) {
                       t->tm_year += ATOI2(arg);
               } else {
                       yearset = ATOI2(arg);
                       if (yearset < YEAR_BOUNDARY)
                               t->tm_year = yearset +
                                   LOW_YEAR_CENTURY - TM_YEAR_BASE;
                       else
                               t->tm_year = yearset +
                                   HIGH_YEAR_CENTURY - TM_YEAR_BASE;
               }
               /* FALLTHROUGH */
       case 8:                         /* MMDDhhmm */
               t->tm_mon = ATOI2(arg);
               --t->tm_mon;            /* Convert from 01-12 to 00-11 */
               /* FALLTHROUGH */
       case 6:
               t->tm_mday = ATOI2(arg);
               /* FALLTHROUGH */
       case 4:
               t->tm_hour = ATOI2(arg);
               /* FALLTHROUGH */
       case 2:
               t->tm_min = ATOI2(arg);
               break;
       default:
               goto terr;
       }

       t->tm_isdst = -1;               /* Figure out DST. */
       tm = *t;
       tsp[0].tv_sec = tsp[1].tv_sec = mktime(t);
       if (tsp[0].tv_sec == NO_TIME || difftm(t, &tm))
terr:          errx(EXIT_FAILURE, "out of range or bad time specification:\n"
                   "\t'%s' should be [[CC]YY]MMDDhhmm[.ss]", initarg);

       tsp[0].tv_nsec = tsp[1].tv_nsec = 0;
}

static void
stime_arg2(const char *arg, int year, struct timespec *tsp)
{
       struct tm *t, tm;
       time_t tmptime;
                                       /* Start with the current time. */
       tmptime = tsp[0].tv_sec;
       if ((t = localtime(&tmptime)) == NULL)
               err(EXIT_FAILURE, "localtime");

       t->tm_mon = ATOI2(arg);         /* MMDDhhmm[yy] */
       --t->tm_mon;                    /* Convert from 01-12 to 00-11 */
       t->tm_mday = ATOI2(arg);
       t->tm_hour = ATOI2(arg);
       t->tm_min = ATOI2(arg);
       if (year) {
               year = ATOI2(arg);
               if (year < YEAR_BOUNDARY)
                       t->tm_year = year + LOW_YEAR_CENTURY - TM_YEAR_BASE;
               else
                       t->tm_year = year + HIGH_YEAR_CENTURY - TM_YEAR_BASE;
       }
       t->tm_sec = 0;

       t->tm_isdst = -1;               /* Figure out DST. */
       tm = *t;
       tsp[0].tv_sec = tsp[1].tv_sec = mktime(t);
       if (tsp[0].tv_sec == NO_TIME || difftm(t, &tm))
               errx(EXIT_FAILURE,
                   "out of range or bad time specification: MMDDhhmm[YY]");

       tsp[0].tv_nsec = tsp[1].tv_nsec = 0;
}

static void
stime_file(const char *fname, struct timespec *tsp,
   int statfunc(const char *, struct stat *))
{
       struct stat sb;

       if (statfunc(fname, &sb))
               err(1, "%s", fname);
       tsp[0] = sb.st_atimespec;
       tsp[1] = sb.st_mtimespec;
}

static int
stime_posix(const char *arg, struct timespec *tsp)
{
       struct tm tm, tms;
       const char *p;
       char *ep;
       int utc = 0;
       long val;

#define isdigch(c)      (isdigit((int)((unsigned char)(c))))

       if ((p = strchr(arg, '-')) == NULL)
               return 0;
       if (p - arg < 4)        /* at least 4 year digits required */
               return 0;

       if (!isdigch(arg[0]))   /* and the first must be a digit! */
               return 0;

       (void)memset(&tm, 0, sizeof tm);

       errno = 0;
       val = strtol(arg, &ep, 10);             /* YYYY */
       if (val < 0 || val > INT_MAX)
               return 0;
       if (*ep != '-')
               return 0;
       tm.tm_year = (int)val - 1900;

       p = ep + 1;

       if (!isdigch(*p))
               return 0;
       val = strtol(p, &ep, 10);               /* MM */
       if (val < 1 || val > 12)
               return 0;
       if (*ep != '-' || ep != p + 2)
               return 0;
       tm.tm_mon = (int)val - 1;

       p = ep + 1;

       if (!isdigch(*p))
               return 0;
       val = strtol(p, &ep, 10);               /* DD */
       if (val < 1 || val > 31)
               return 0;
       if ((*ep != 'T' && *ep != ' ') || ep != p + 2)
               return 0;
       tm.tm_mday = (int)val;

       p = ep + 1;

       if (!isdigch(*p))
               return 0;
       val = strtol(p, &ep, 10);               /* hh */
       if (val < 0 || val > 23)
               return 0;
       if (*ep != ':' || ep != p + 2)
               return 0;
       tm.tm_hour = (int)val;

       p = ep + 1;

       if (!isdigch(*p))
               return 0;
       val = strtol(p, &ep, 10);               /* mm */
       if (val < 0 || val > 59)
               return 0;
       if (*ep != ':' || ep != p + 2)
               return 0;
       tm.tm_min = (int)val;

       p = ep + 1;

       if (!isdigch(*p))
               return 0;
       val = strtol(p, &ep, 10);               /* ss (or in POSIX, SS) */
       if (val < 0 || val > 60)
               return 0;
       if ((*ep != '.' && *ep != ',' && *ep != 'Z' && *ep != '\0') ||
             ep != p + 2)
               return 0;
       tm.tm_sec = (int)val;

       if (*ep == ',' || *ep == '.') {
               double frac;
               ptrdiff_t fdigs;

               p = ep + 1;
               if (!isdigch(*p))
                       return 0;
               val = strtol(p, &ep, 10);
               if (val < 0)
                       return 0;
               if (ep == p)    /* at least 1 digit required */
                       return 0;
               if (*ep != 'Z' && *ep != '\0')
                       return 0;

               if (errno != 0)
                       return 0;

               fdigs = ep - p;
               if (fdigs > 15) {
                       /* avoid being absurd */
                       /* don't want to risk 10^fdigs being INF */
                       if (val == 0)
                               fdigs = 1;
                       else while (fdigs > 15) {
                               val = (val + 5) / 10;
                               fdigs--;
                       }
               }

               frac = pow(10.0, (double)fdigs);

               tsp[0].tv_nsec = tsp[1].tv_nsec =
                       (long)round(((double)val / frac) * 1000000000.0);
       } else
               tsp[0].tv_nsec = tsp[1].tv_nsec = 0;

       if (*ep == 'Z') {
               if (ep[1] != '\0')
                       return 0;
               utc = 1;
       }

       if (errno != 0)
               return 0;

       tm.tm_isdst = -1;
       tms = tm;
       if (utc)
               tsp[0].tv_sec = tsp[1].tv_sec = timegm(&tm);
       else
               tsp[0].tv_sec = tsp[1].tv_sec = mktime(&tm);

       if ((errno != 0 && tsp[1].tv_sec == NO_TIME) || difftm(&tm, &tms))
               return 0;

       return 1;
}

/*
* Determine whether 2 struct tn's are different
* return true (1) if theu are, false (0) otherwise.
*
* Note that we only consider the fields that are set
* for mktime() to use - if mktime() returns them
* differently than was set, then there was a problem
* with the setting.
*/
static int
difftm(const struct tm *t1, const struct tm *t2)
{
#define CHK(fld) do {                                           \
                       if (t1->tm_##fld != t2->tm_##fld) {     \
                               return  1;                      \
                       }                                       \
               } while(/*CONSTCOND*/0)

       CHK(year);
       CHK(mon);
       CHK(mday);
       CHK(hour);
       CHK(min);
       CHK(sec);

       return 0;
}

static void
usage(void)
{
       (void)fprintf(stderr,
           "Usage: %s [-acDfhm] [-d|--date datetime] [-R|-r|--reference file]"
           " [-t time] file ...\n", getprogname());
       exit(EXIT_FAILURE);
}