#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <time.h>
#include <string.h>

#define USE_INLINE_ASM_IO

/* V1.0
 * CMOS clock manipulation - Charles Hedrick, hedrick@cs.rutgers.edu, Apr 1992
 * 
 * clock [-u] -r  - read cmos clock
 * clock [-u] -w  - write cmos clock from system time
 * clock [-u] -s  - set system time from cmos clock
 * clock [-u] -a  - set system time from cmos clock, adjust the time to
 *                  correct for systematic error, and put it back to the cmos.
 *  -u indicates cmos clock is kept in universal time
 *
 * The program is designed to run setuid, since we need to be able to
 * write the CMOS port.
 *
 * I don't know what the CMOS clock will do in 2000, so this program
 * probably won't work past the century boundary.
 *
 *********************
 * V1.1
 * Modified for clock adjustments - Rob Hooft, hooft@chem.ruu.nl, Nov 1992
 * Also moved error messages to stderr. The program now uses getopt.
 * Changed some exit codes. Made 'gcc 2.3 -Wall' happy.
 *
 * I think a small explanation of the adjustment routine should be given
 * here. The problem with my machine is that its CMOS clock is 10 seconds 
 * per day slow. With this version of clock.c, and my '/etc/rc.local' 
 * reading '/etc/clock -au' instead of '/etc/clock -u -s', this error 
 * is automatically corrected at every boot. 
 *
 * To do this job, the program reads and writes the file '/etc/adjtime' 
 * to determine the correction, and to save its data. In this file are 
 * three numbers: 
 *
 * 1) the correction in seconds per day (So if your clock runs 5 
 *    seconds per day fast, the first number should read -5.0)
 * 2) the number of seconds since 1/1/1970 the last time the program was
 *    used.
 * 3) the remaining part of a second which was leftover after the last 
 *    adjustment
 *
 * Installation and use of this program:
 *
 * a) create a file '/etc/adjtime' containing as the first and only line:
 *    '0.0 0 0.0'
 * b) run 'clock -au' or 'clock -a', depending on whether your cmos is in
 *    universal or local time. This updates the second number.
 * c) set your system time using the 'date' command.
 * d) update your cmos time using 'clock -wu' or 'clock -w'
 * e) replace the first number in /etc/adjtime by your correction.
 * f) put the command 'clock -au' or 'clock -a' in your '/etc/rc.local' or 
 *    let 'cron' start it regularly.
 *
 * If the adjustment doesn't work for you, try contacting me by E-mail.
 *********************
 * V1.2
 * Applied patches by Harald Koenig (koenig@nova.tat.physik.uni-tuebingen.de)
 * Patched and indented by Rob Hooft (hooft@EMBL-Heidelberg.DE)
 * 
 * A free quote from a MAIL-message:
 *
 * "I found the explanation and solution for the CMOS reading 0xff problem
 *  in the 0.99pl13c (ALPHA) kernel: the RTC goes offline for a small amount
 *  of time for updateing. Solution is included in the kernel source 
 *  (linux/kernel/time.c)."
 *
 * "I modified clock.c to fix this problem and add an option (now default,
 *  look for USE_INLINE_ASM_IO) that I/O instructions were used as inline
 *  code and not via /dev/port (still possible vila #undef ...)."
 *
 * With the new code, which is partially taken from the kernel sources, 
 * the CMOS clock handling looks much more "official".
 * Thanks Harald (and Torsten for the kernel code)!
 */

/* Here the information for time adjustments is kept. */
#define ADJPATH "/etc/adjtime"

/* used for debugging the code. */
/*#define DEBUG */
/*#define KEEP_OFF */

/* Stupid constants */
#define SECONDSPERDAY 86400

/* Globals */
int readit = 0;
int adjustit = 0;
int writeit = 0;
int setit = 0;
int universal = 0;

volatile void 
usage ()
{
  fprintf (stderr, 
    "clock [-u] -r|w|s|a\n"
    "  r: read and print CMOS clock\n"
    "  w: write CMOS clock from system time\n"
    "  s: set system time from CMOS clock\n"
    "  a: get system time and adjust CMOS clock\n"
    "  u: CMOS clock is in universal time\n"
  );
  exit (1);
}

#ifndef USE_INLINE_ASM_IO
int cmos_fd;
#endif

#define CMOS_READ(addr)      ({outb(0x70,(addr)|0x80); inb(0x71);})
#define CMOS_WRITE(addr,val) ({outb(0x70,(addr)|0x80); outb(0x71,(val)); })

static inline void 
outb (short port, char val)
{
#ifdef USE_INLINE_ASM_IO
  __asm__ volatile ("out%B0 %0,%1"::"a" (val), "d" (port));
#else
  lseek (cmos_fd, port, 0);
  write (cmos_fd, &val, 1);
#endif
}

static inline unsigned char 
inb (short port)
{
  unsigned char ret;
#ifdef USE_INLINE_ASM_IO
  __asm__ volatile ("in%B0 %1,%0":"=a" (ret):"d" (port));
#else
  lseek (cmos_fd, port, 0);
  read (cmos_fd, &ret, 1);
#endif
  return ret;
}

void 
cmos_init ()
{
#ifdef USE_INLINE_ASM_IO
  if (ioperm (0x70, 2, 1)) 
    {
      fprintf(stderr,"clock: unable to get I/O port access\n");
      exit (1);
    }
#else
  cmos_fd = open ("/dev/port", 2);
  if (cmos_fd < 0)
    {
      perror ("unable to open /dev/port read/write : ");
      exit (1);
    }
  if (lseek (cmos_fd, 0x70, 0) < 0 || lseek (cmos_fd, 0x71, 0) < 0)
    {
      perror ("unable to seek port 0x70 in /dev/port : ");
      exit (1);
    }
#endif
}

static inline int 
cmos_read_bcd (int addr)
{
  int b;
  b = CMOS_READ (addr);
  return (b & 15) + (b >> 4) * 10;
}

static inline void 
cmos_write_bcd (int addr, int value)
{
  CMOS_WRITE (addr, ((value / 10) << 4) + value % 10);
}

int 
main (int argc, char **argv, char **envp)
{
  struct tm tm;
  struct tm *tmp;
  time_t systime;
  time_t last_time;
  char *zone;
  char zonebuf[256];
  char arg;
  FILE *adj;
  double factor;
  double not_adjusted;
  int adjustment = 0;
  int i;
  unsigned char save_control, save_freq_select;

  while ((arg = getopt (argc, argv, "rwsua")) != -1)
    {
      switch (arg)
	{
	case 'r':
	  readit = 1;
	  break;
	case 'w':
	  writeit = 1;
	  break;
	case 's':
	  setit = 1;
	  break;
	case 'u':
	  universal = 1;
	  break;
	case 'a':
	  adjustit = 1;
	  break;
	default:
	  usage ();
	}
    }

  if (readit + writeit + setit + adjustit > 1)
    usage ();			/* only allow one of these */

  if (!(readit | writeit | setit | adjustit))	/* default to read */
    readit = 1;

  cmos_init ();

  if (adjustit)
    {				/* Read adjustment parameters first */
      if ((adj = fopen (ADJPATH, "r")) == NULL)
	{
	  perror (ADJPATH);
	  exit (2);
	}
      if (fscanf (adj, "%lf %d %lf", &factor, &last_time, &not_adjusted) < 0)
	{
	  perror (ADJPATH);
	  exit (2);
	}
      fclose (adj);
#ifdef DEBUG
      printf ("Last adjustment done at %d seconds after 1/1/1970\n", last_time);
#endif
    }

  if (readit || setit || adjustit)
    {

/* read RTC exactly on falling edge of update flag */
/* Wait for rise.... (may take upto 1 second) */

      for (i = 0; i < 10000000; i++)	
	if (CMOS_READ (10) & 0x80)
	  break;

/* Wait for fall.... (must try at least 2.228 ms) */

      for (i = 0; i < 1000000; i++)	
	if (!(CMOS_READ (10) & 0x80))
	  break;

/* The purpose of the "do" loop is called "low-risk programming" */
/* In theory it should never run more than once */
      do
	{ 
	  tm.tm_sec = cmos_read_bcd (0);
	  tm.tm_min = cmos_read_bcd (2);
	  tm.tm_hour = cmos_read_bcd (4);
	  tm.tm_wday = cmos_read_bcd (6);
	  tm.tm_mday = cmos_read_bcd (7);
	  tm.tm_mon = cmos_read_bcd (8);
	  tm.tm_year = cmos_read_bcd (9);
	}
      while (tm.tm_sec != cmos_read_bcd (0));
      tm.tm_mon--;		/* DOS uses 0 base */
      tm.tm_wday -= 3;		/* DOS uses 3 - 9 for week days */
      tm.tm_isdst = -1;		/* don't know whether it's daylight */
#ifdef DEBUG
      printf ("Cmos time : %d:%d:%d\n", tm.tm_hour, tm.tm_min, tm.tm_sec);
#endif
    }

  if (readit)
    {
/*
 * Mktime assumes we're giving it local time.  If the CMOS clock
 * is in GMT, we have to set up TZ to mktime knows it.  Tzset gets
 * called implicitly by the time code, but only the first time.  When
 * changing the environment variable, better call tzset explicitly.
 */
      if (universal)
	{
	  zone = (char *) getenv ("TZ");	/* save original time zone */
	  (void) putenv ("TZ=");
	  tzset ();
	  systime = mktime (&tm);
	  /* now put back the original zone */
	  if (zone)
	    {
	      if ((strlen (zone) + 4) > sizeof (zonebuf))
		{
		  fprintf (stderr, "Size of TZ variable is too long\n");
		  exit (2);
		}
	      strcpy (zonebuf, "TZ=");
	      strcat (zonebuf, zone);
	      putenv (zonebuf);
	    }
	  else
	    {			/* wasn't one, so clear it */
	      putenv ("TZ");
	    }
	  tzset ();
	  printf ("%s", ctime (&systime));
	}
      else
	{
	  printf ("%s", asctime (&tm));
	}
    }

  if (setit || adjustit)
    {

/* program is designed to run setuid, be secure! */

      if (getuid () != 0)
	{			
	  fprintf (stderr, "Sorry, must be root to set or adjust time\n");
	  exit (2);
	}

      if (universal)
	(void) putenv ("TZ=");
      systime = mktime (&tm);
#ifdef DEBUG
      printf ("Number of seconds since 1/1/1970 is %d\n", systime);
#endif
      if (adjustit)
	{			/* the actual adjustment */
	  double exact_adjustment;

	  exact_adjustment = ((double) (systime - last_time))
	    * factor / SECONDSPERDAY
	    + not_adjusted;
	  if (exact_adjustment > 0)
	    adjustment = (int) (exact_adjustment + 0.5);
	  else
	    adjustment = (int) (exact_adjustment - 0.5);
	  not_adjusted = exact_adjustment - (double) adjustment;
	  systime += adjustment;
#ifdef DEBUG
	  printf ("Time since last adjustment is %d seconds\n",
		  (int) (systime - last_time));
	  printf ("Adjusting time by %d seconds\n",
		  adjustment);
	  printf ("remaining adjustment is %.3f seconds\n",
		  not_adjusted);
#endif
	}
#ifdef KEEP_OFF
      if (0 != 0)
	{
#else
      if (stime (&systime) != 0)
	{
#endif
	  fprintf (stderr, "Unable to set time -- probably you are not root\n");
	  exit (1);
	}
    }
  if (writeit || (adjustit && adjustment != 0))
    {
      systime = time (NULL);
      if (universal)
	tmp = gmtime (&systime);
      else
	tmp = localtime (&systime);

#ifndef KEEP_OFF
      save_control = CMOS_READ (11);	/* tell the clock it's being set */
      CMOS_WRITE (11, (save_control | 0x80));
      save_freq_select = CMOS_READ (10);	/* stop and reset prescaler */
      CMOS_WRITE (10, (save_freq_select | 0x70));

      cmos_write_bcd (0, tmp->tm_sec);
      cmos_write_bcd (2, tmp->tm_min);
      cmos_write_bcd (4, tmp->tm_hour);
      cmos_write_bcd (6, tmp->tm_wday + 3);
      cmos_write_bcd (7, tmp->tm_mday);
      cmos_write_bcd (8, tmp->tm_mon + 1);
      cmos_write_bcd (9, tmp->tm_year);

      CMOS_WRITE (10, save_freq_select);
      CMOS_WRITE (11, save_control);
#endif
#ifdef DEBUG
      printf ("Set to : %d:%d:%d\n", tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
#endif
    }
#ifdef DEBUG
  else
    printf ("CMOS clock unchanged.\n");
#endif
  /* Save data for next 'adjustit' call */
  if (adjustit)
    {
      if ((adj = fopen (ADJPATH, "w")) == NULL)
	{
	  perror (ADJPATH);
	  exit (2);
	}
      fprintf (adj, "%f %d %f\n", factor, systime, not_adjusted);
      fclose (adj);
    }
  exit (0);
}
