#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "config_file.h"
#include "irrd.h"

/* The functions in this file support atomic transactions for irrd.
 * For each !us...!ue transaction there is a transaction file built
 * which can be used to restore a DB to it's state prior to a trans.
 * On bootstrap irrd looks for transaction files and if it finds one
 * that is complete it will re-apply the transaction. */

/* local yokel's */

static int     file_exists         (DIR *, char *);
static char    *get_ufname         (char *);
static int     active_trans        (irr_database_t *, char *);
static void    free_rbtrans_list   (rollback_t *);
static trans_t *create_rbtrans_obj (irr_database_t *, char *);
static void    add_rbtrans_obj     (rollback_t *, trans_t *);
static long    complete_trans_file (FILE *, char *, char *);
static int     active_trans        (irr_database_t *, char *);
static int     transaction_dels    (irr_database_t *, FILE *, FILE *);

extern m_command_t m_info [];

/* Initialize the transaction update file with the file offset
 * and first three letters of the key attribute for the
 * operations in the !us...!ue update file.
 *
 * Function runs through the update file and for each 
 * object it does a lookup and records to (fout) the file offset of
 * the object and the first three letters of the key attribute.
 * In case of rollback, this information can be used to
 * restore the original DB as an '*xx' is written over the
 * first three letters of objects to signify they are deleted.
 *
 * The !us...!ue file has two operations, ADD and DEL with the
 * ADD operation being a replace operation if the object already
 * exists.  Therefore, ADD operation objects are checked and
 * if it exists an entry will be made in the transaction file we
 * are building.
 * 
 * Input:
 *  -pointer to the database struct (db)
 *  -file pointer to the update file, ie, !us...!ue (find)
 *  -file pointer to the transaction file we are writing to (fout)
 *
 * Return:
 *  -the number of upates in the transaction if there were no 
 *   errors and all DEL/REPLACE operations were successfully recorded in 
 *   the transaction update file.
 *  -0 otherwise
 */
int transaction_dels (irr_database_t *db, FILE *fin, FILE *fout) {
  int state = 0, i, first_lookup = 1, n = 0;
  u_long offset, len;
  u_short as;
  long save_fpos;
  enum IRR_OBJECTS type;
  char buf[BUFSIZE+1], pkey[BUFSIZE+1], attr[BUFSIZE+1];
  irr_connection_t irr;
  regex_t re_key, re_origin, re_bl;
  regmatch_t rm[5];
  char *key    = "^([^ \t\n]+):[ \t]*(([^ \t\n])|([^ \t\n].*[^ \t\n]))"
                 "[ \t]*\n$";
  char *origin = "^origin:[ \t]*AS([[:digit:]]+)\n$";
  char *bl     = "^[ \t]*\n?$";

  if ((save_fpos = ftell (fin)) < 0 ||
      fseek (fin, 0L, SEEK_SET) < 0) {
    trace (ERROR, default_trace, "transaction_dels (): "
	   "Transaction file I/O error: (%s).\n", strerror (errno));
    return 0;
  }

  /* compile our regex's */
  regcomp (&re_key,    key,    REG_EXTENDED);
  regcomp (&re_origin, origin, REG_EXTENDED|REG_ICASE);
  regcomp (&re_bl,     bl,     REG_EXTENDED|REG_NOSUB);

  while (fgets (buf, BUFSIZE, fin) != NULL) {    
    /* ADD or DEL state */
    if (state == 0 &&
	(!strcmp (buf, "DEL\n") ||
	 !strcmp (buf, "ADD\n"))) {
      trace (NORM, default_trace, "JW t_dels: enter state 1\n");
      /* tally the total number of updates in this trans;
       * this will be our return value */
      n++; 
      state = 1;
      continue;
    }
    
    /* blank line state */
    if (state == 1) {
      if (regexec (&re_bl, buf, 0, NULL, 0)) {
	trace (ERROR, default_trace, "transaction_dels (): malformed update "
	       "file. Expect blank line but got (%s)\n", buf);
	n = 0;
	goto ABORT_TRANS;
      }
      state = 2;
      continue;
    }

    /* beginning of object state, pick off the primary key and look it up
     * to find the object offset */
    if (state == 2) {
      trace (NORM, default_trace, "JW t_dels: enter state 2\n");
      if (regexec (&re_key, buf, 5, rm, 0)) {
	trace (ERROR, default_trace, "transaction_dels (): malformed update "
	       "file.  Expect key line but got (%s)\n", buf);
	n = 0;
	goto ABORT_TRANS;
      }

      /* save the attribute ... */
      *(buf + rm[1].rm_eo) = '\0';
      snprintf (attr, BUFSIZE, "%s,", buf + rm[1].rm_so);

trace (NORM, default_trace, "JW: attr (%s)\n", attr);

      /* ... and key values */
      i = 4;
      if (rm[4].rm_so == -1)
	i = 3;
      *(buf + rm[i].rm_eo) = '\0';
      snprintf (pkey, BUFSIZE, buf + rm[i].rm_so);

trace (NORM, default_trace, "JW: key (%s)\n", pkey);

      /* now find the objects type (eg, route, maintainer, aut-num, ...) */
      for (i = 0; i < IRR_MAX_MCMDS &&
	          strncasecmp (attr, m_info[i].command,
			       strlen (m_info[i].command));
	          i++);

      /* did we find the object type? 
       * if yes then look it up and find it's file offset and first
       * three attribute letters so we can restore delete operations */
      if (i < IRR_MAX_MCMDS) {
	/* non-route object processing */
        if ((type = m_info[i].type) != ROUTE) {
	  if (first_lookup) {
	    irr.ll_database = LL_Create (0);
	    LL_Add (irr.ll_database, db);
	    first_lookup = 0;
	  }

	  /* look it up and get the file offset in the DB */
	  len = 0;
	  irr_database_find_matches (&irr, pkey, PRIMARY, 
				     RAWHOISD_MODE|TYPE_MODE, type, 
				     &offset, &len);

	  /* save the offset and the first 3 attribute letters for rollback */
	  if (len > 0)
	    fprintf (fout, "%-10ld %.*s\n", (long) offset, 3, attr);

	  state = 0;
	}
	/* route object processing; have to get the origin to do the lookup */
	else
	  state = 3;

	continue;
      }
      /* oops!  couldn't locate the object type */
      else {
	trace (ERROR, default_trace, "transaction_dels (): couldn't identify "
	       "the key type: (%s) (%s)\n", pkey, attr);
	n = 0;
	goto ABORT_TRANS;
      }
    }

    /* route object processing */
    if (state == 3 &&
	regexec (&re_origin, buf, 2, rm, 0) == 0) {
trace (NORM, default_trace, "JW: route processing line (%s)\n", buf);
      *(buf + rm[1].rm_eo) = '\0';
      sscanf (buf + rm[1].rm_so, "%hu", &as);
trace (NORM, default_trace, "JW: route processing (%s, %hu)...\n", pkey, as);
      len = 0;
      seek_route_object (db, pkey, as, &offset, &len, 1);
      /* save the offset and first 2 attribute letters for rollback */
      if (len > 0)
	fprintf (fout, "%-10ld %.*s\n", (long) offset, 3, attr);
      state = 0;
    }
  }

ABORT_TRANS:

  /* clean up */
  regfree (&re_key);
  regfree (&re_origin);
  regfree (&re_bl);
  if (!first_lookup)
    LL_Destroy (irr.ll_database);

  if (fseek (fin, save_fpos, SEEK_SET) < 0) {
    trace (ERROR, default_trace, "transaction_dels (): Transaction "
	   "file pos restore I/O error: (%s).\n", strerror (errno));
    return 0;
  }

  /* if we didn't end up in state '0' then there were errors */
  return ((state == 0) ? n : 0);
}

/* Build an update transaction file to be used to restore the
 * DB in the event of a crash or other error in processing
 * the update.
 *
 * file format:
 * ts: cs + n            # long value in a field of 10; curr serial 
 *                         of this transaction (field width 14)
 * cs                    # original current serial; -1 means irrd is
 *                         not journaling (ie, !ms...) >= 0 otherwise
 * original journal size # length of journal before transaction; -1
 *                         means irrd is not journaling 
 *                         (ie, !ms...) >= 0 otherwise
 * transaction file name # full path name of the !us...!ue file
 * original file length  # length of DB before the transaction
 * fpos xx               # file position of deleted object and first 
 *  ...                    2 attr letters
 * ts: cs + n            # same as line one.  used to check for complete
 *                         transaction file
 *
 * A completly built transaction file is signified by the first and
 * last lines of the file being equal.
 *
 * See db_rollback () for an outline of the DB restoration procedure.
 * 
 * Input:
 *  -pointer to the database struct (db)
 *  -file pointer to the update file, ie, !us...!ue file (u_fd)
 *  -char template to return the name of the transaction file
 *   this function builds (fname).
 *  -integer pointer to the number of updates in the transaction
 *   
 * 
 * Return:
 *  -NULL if the transaction file was built without error.
 *  -otherwise an string indicating that the transaction file 
 *   could not be built.
 *
 *  function will return a transaction file name suitable for rollback.
 */
char *build_transaction_file (irr_database_t *db, FILE *u_fp, char *uname,
			      char *fname, int *n_updates) {
  int fd, n;
  long jsize, dbsize;
  FILE *fp;
  struct stat fstats;

  /* sanity check */
  if (db->db_fp == NULL) {
    trace (ERROR, default_trace, "DB (%s) is not open.  "
	   "Abort update op.\n", db->name);
    return "DB does not exist or is not open for updates!";
  }

  /* we need to find out how large the DB is */
  sprintf (fname, "%s/%s.db", IRR.database_dir, db->name);
  if ((fd = open (fname, O_RDONLY, 0)) < 0) {
    trace (ERROR, default_trace, "Abort update!  Cannot open (%s) "
	   "to determine how large it is: %s\n", db->name, strerror (errno));
    return "Cannot determine DB size.";
  }
  fstat (fd, &fstats);
  dbsize = fstats.st_size;
  close (fd);

  /* if we are journaling then we need the original journal file
   * size for rollback */
  jsize = -1;
  if ((db->flags & IRR_AUTHORITATIVE) ||
      db->mirror_prefix != NULL) {
    if (db->journal_fd > 0) {
      fstat (db->journal_fd, &fstats);
      jsize = fstats.st_size;
    }
    else
      jsize = 0;
  }

  /* open/create a transaction file */
  sprintf (fname, "%s/%s.trans", IRR.database_dir, db->name);
  if ((fp = fopen (fname, "w+")) == NULL) {
    trace (ERROR, default_trace, "Abort update!  Can't open atomic transaction"
	   " file for DB (%s): (%s).\n", db->name, strerror (errno));
    return "Cannot open atomic transaction file!";
  }

  /* log the cs of the transaction, the original cs,
   * the !us...!ue update file name and the original DB file size */
  fprintf (fp, "ts: %-10ld\n", 0L);
  fprintf (fp, "%ld\n", (long) ((jsize >= 0) ? db->serial_number : -1));
  fprintf (fp, "%ld\n", jsize);
  fprintf (fp, "%s\n", uname);
  fprintf (fp, "%ld\n", dbsize);

  /* build the DEL list, ie, the file positions
   * of deleted/replaced objects and the first 2 attr letters */
  if (!(n = transaction_dels (db, u_fp, fp))) {
    fclose (fp);
    remove (fname);
    return "Abort update!  transaction file operation error.";
  }

  /* write the cs at the end of the file ... */
  if (fseek (fp, 0, SEEK_END) < 0) {
    trace (ERROR, default_trace, "Abort update!  "
	   "Transaction rewind file op error (%s): %s.\n", 
	   db->name, strerror (errno));
    fclose (fp);
    remove (fname);
    return "Transaction file operation error.";
  }
  fprintf (fp, "ts: %-10ld\n", (long) (db->serial_number + n));

  /* ... now write the cs to at the beginning of the file to indicate a 
   * complete transaction file has been initialized */
  if (fseek (fp, 0L, SEEK_SET) < 0) {
    trace (ERROR, default_trace, "Abort update!  "
	   "Transaction rewind file op error (%s): %s.\n", 
	   db->name, strerror (errno));
    fclose (fp);
    remove (fname);
    return "Transaction file operation error.";
  }

  trace (NORM, default_trace, "JW: serial_num (%lu) conv (%-10ld)\n", 
	 db->serial_number, (long) (db->serial_number + n));

  fprintf (fp, "ts: %-10ld\n", (long) (db->serial_number + n));
  fclose  (fp);
  *n_updates = n;

  trace (NORM, default_trace, "JW: transaction update file successfully created (%s) bye-bye!\n", fname);

  return NULL;
}

/* Rollback DB (db->name) to it's original state after a
 * transaction.
 *
 * Rollback can occur after a system crash or after an
 * aborted transaction.  In the event of a system crash,
 * the function will check the integrity of the transaction
 * file which is used to roll back the trancsaction.  
 *
 * Function assumes that first a transaction file (fname) is
 * built which can be used to restore the DB to it's original
 * state and then the transaction is applied to the DB.  
 * Therefore, if the transaction file is not complete then no 
 * changes were made to the DB and no repair is needed.
 *
 * This function is idempotent and can be called any number
 * of times and the final state of the DB will be the same.
 * This is a desirable property since it is possible for the 
 * system to crash again (or some other error) while the DB 
 * repair is taking place.
 *
 * See build_transaction_file () for an explanation of the
 * transaction file format used to restore the DB.
 *
 * Restoration procedure:
 * 1. see if transaction file was built completely
 * 2. truncate the DB it's orignal length
 * 3. for each "fpos xx" line, go to byte offset "fpos" and write "xx"
 *    to undo the delete operation.
 * The DB should now be in it's original state.
 * 
 * Input:
 *  -pointer to the database struct (db)
 *  -name of the transaction file which is used to restore the
 *   DB (fname).
 *
 * Return:
 *  -1 if it restored the DB to it's original state without error
 *  -0 otherwise
 */
int db_rollback (irr_database_t *db, char *fname) {
  int reopenf = -1, ret_code = 0;
  FILE *fin;
  char buf[BUFSIZE+1], attr[256];
  long fpos;

  /* sanity check */
  if (fname == NULL) {
    trace (ERROR, default_trace, "db_rollback (): NULL transcation file "
	   "name.\n");
    return 0;
  }

  trace (NORM, default_trace, "JW: rollback fname (%s)\n", fname);
  
  /* open the transaction file which is used to restore the DB */  
  if ((fin = fopen (fname, "r")) == NULL) {
    trace (ERROR, default_trace, "db_rollback ():  Can't open atomic "
	   "transaction file (%s) for DB (%s): (%s): exit (0)\n", 
	   fname, db->name, strerror (errno));
    return 0;
  }

  /* was the transaction file built completely? 
   * if not then no changes were made to the DB 
   * and there is nothing to roll back */
  switch (complete_trans_file (fin, fname, db->name)) {
  case -1: /* trans file I/O error */
    trace (NORM, default_trace, "db_rollback (): transaction file (%s) "
	   "error.\n", fname);
    return 0;
  case 0: /* incomplete trans file */
    trace (NORM, default_trace, "db_rollback (): transaction file (%s) "
	   "was not complete; nothing to do, returning.\n", fname);
    goto CLEAN_UP;
  }

  /* read past the cs, original current serial, journal file length,
   * and !us...!ue update file name */
  if (fgets (buf, BUFSIZE, fin) == NULL ||
      fgets (buf, BUFSIZE, fin) == NULL ||
      fgets (buf, BUFSIZE, fin) == NULL ||
      fgets (buf, BUFSIZE, fin) == NULL) {
    trace (ERROR, default_trace, "db_rollback ():  Can't read atomic "
	   "transaction file (%s) for DB (%s), fgets () error: (%s)\n", 
	   fname, db->name, strerror (errno));
    goto CLEAN_UP;
  } 

  /* read the DB original file length before the transaction */
    if (fscanf (fin, "%ld\n", &fpos) != 1) {
    trace (ERROR, default_trace, "db_rollback ():  Can't read the atomic "
	   "transaction file (%s, original size) for DB (%s): (%s)\n", 
	   fname, db->name, strerror (errno));
    goto CLEAN_UP;
  }

  /* we need to have the DB closed for the file truncation operation.
   * so if the DB is open (ie, transacton abort) then we need to re-open
   * it after we truncate */
  reopenf = 0;
  if (db->db_fp != NULL) {
    reopenf = 1;
    fclose (db->db_fp);
  }

  /* truncate the DB to it's original length;
   * this effectively undoes any ADD operations to the DB */
  sprintf (buf, "%s/%s.db", IRR.database_dir, db->name);
  if (truncate (buf, fpos) < 0) {
    trace (ERROR, default_trace, "db_rollback ():  Can't truncate DB "
	   "(%s) to (%ld) bytes: (%s)\n", buf, fpos, strerror (errno));
    goto CLEAN_UP;
  }

  /* open the DB */
  if ((db->db_fp = fopen (buf, "r+")) == NULL) {
    trace (ERROR, default_trace, "db_rollback (): Can't re-open DB (%s):"
	   "(%s)\n", strerror (errno));
    goto CLEAN_UP;
  }

  trace (NORM, default_trace, "JW: rollback original size (%ld)\n", fpos);


  /* Undo the delete operations */
  while (fgets (buf, BUFSIZE, fin) != NULL) {
    /* last line is 'ts: <cs>' */
    if (buf[0] == 't')
      break;

    /* read the file position of the DEL and the attribute
     * letters that were overwritten with '*xx' */
    if (sscanf (buf, "%ld %s", &fpos, attr) != 2) {
      trace (ERROR, default_trace, "db_rollback (): Can't read an 'fpos attr' "
	     "line for file (%s) for DB (%s)\n", fname, db->name);
      goto CLEAN_UP;
    }

    trace (NORM, default_trace, "JW: rollback buf (%s) (%ld,%s)\n", buf, fpos, attr);

    /* go to the DEL operation file position ... */
    if (fseek (db->db_fp, fpos, SEEK_SET) < 0) {
      trace (ERROR, default_trace, "db_rollback (): fseek DEL error for trans "
	     "file (%s) for DB (%s) to fpos (%ld): (%s)\n", 
	     fname, db->name, fpos, strerror (errno));
      goto CLEAN_UP;
    }
    
    /* ... and reverse the operation */
    if (!fwrite (attr, 3, 1, db->db_fp)) {
      trace (ERROR, default_trace, "db_rollback (): undo DEL fwrite error for "
	     "trans file (%s) for DB (%s) attr string (%s)): (%s)\n", 
	     fname, db->name, attr, strerror (errno));
      goto CLEAN_UP;
    }
  }
  
  /* if we get here then there were no errors and we rolled back the DB */
  ret_code = 1;

CLEAN_UP:

  /* maintain original file status */
  if (reopenf == 0 && 
      db->db_fp != NULL) {
    fclose (db->db_fp);
    db->db_fp = NULL;
  }
  fclose (fin);

  return ret_code;
trace (NORM, default_trace, "JW: rollback bye-bye! ret_code (%d)\n", ret_code);
}


/* Control the process of checking for transactions that did 
 * not complete and then rolling back the transaction.
 *
 * Input:
 *  -pointer to a struct of any DB's in which an incomplete
 *   transaction was detected and was rolled back.
 *
 * Return:
 *  -1 no errors occured in the transaction check process.
 *     if there were active transactions they are returned
 *     in the (ll) linked list. 
 *  -0 no determination can be made as an error occured during
 *     the process.  The error condition is written to log.
 */
int rollback_check (rollback_t *ll) {
  int ret_code = 0;
  char fname[BUFSIZE+1], *ufname;
  irr_database_t *db;
  DIR *dirp;

  /* sanity checks */

  /* is there a DB cache directory defined? */
  if (IRR.database_dir == NULL) {
    trace (NORM, default_trace, "rollback_check (): no DB cache defined.  "
	   "Nothing to do.\n");
    return 1;
  }
 
  /* can we read the DB cache dir? */
  if ((dirp = opendir (IRR.database_dir)) == NULL) {
    trace (ERROR, default_trace, "rollback_check (): Can't read the DB cache "
	   "dir (%s): (%s)\n", IRR.database_dir, strerror (errno));
    return 0;
  }

  /* loop through our DB list and look for a *.trans file.
   * if a *.trans file exists then roll back the DB if 
   * necessary. */
  LL_Iterate (IRR.ll_database, db) {
    /* does a transaction file exist for this DB? */
    sprintf (fname, "%s.trans", db->name);
    if (file_exists (dirp, fname)) {
      /* this is the transaction file name */
      sprintf (fname, "%s/%s.trans", IRR.database_dir, db->name);

      /* is this transaction active? ie, is the trans file complete and
       * was the trans interupted before finishing?  if yes, add the DB 
       * to our linked list and roll the DB back to it's state before the 
       * transaction. */
      switch (active_trans (db, fname)) {
      case 1: /* active transaction */
	trace (NORM, default_trace, "JW: rollback_check (): active_trans () "
	       "calling db_rollback...\n");
	add_rbtrans_obj (ll, create_rbtrans_obj (db, fname));

	/* rollback the DB to it's original state */
	if (!db_rollback (db, fname)) {
	  trace (ERROR, default_trace, "rollback_check (): db_rollback "
		 "error.  Aborting check process.\n");
	  goto CLEAN_UP;
	}

	/* roll back the journal if we are doing IRRd style mirroring */
	if (((db->flags & IRR_AUTHORITATIVE) || db->mirror_prefix != NULL) &&
	    !journal_rollback (db, fname)) {
	  trace (ERROR, default_trace, "rollback_check (): journal_rollback "
		 "error.  Aborting check process.\n");
	  goto CLEAN_UP;
	}
	break;
      case -1: /* I/O error, couldn't determine trans outcome */
	goto CLEAN_UP;
      default: /* transaction not active */
	if ((ufname = get_ufname (fname)) == NULL) {
	  trace (ERROR, default_trace, "rollback_check (): get_ufname "
		 "error.  Aborting reapply process.\n");
	  goto CLEAN_UP;
	}
	
	/* clean up old transaction files */
	remove (ufname);
	remove (fname);
	free   (ufname);
	break;
      }
    }
  }
  
  /* if we get here there were no errors */
  ret_code = 1;

CLEAN_UP:

  closedir (dirp);
trace (NORM, default_trace, "JW: exiting rollback_check (return 1)\n");
  return ret_code;
}

/* Control the process of re-applying any transactions which
 * did not fully complete.
 *
 * Function should be called on bootstrap.
 * 
 * Input:
 *  -pointer to a struct of any DB's in which an incomplete
 *   transaction was detected and should be re-applied.
 *
 * Return:
 *  -1 no errors occured in the transaction re-application process.
 *  -0 an error occured such that the re-application process 
 *     could not be carried out.
 */
int reapply_transaction (rollback_t *ll) {
  char *p, *ufname;
  trans_t *t;
  FILE *ufin;

  /* look through the linked list of DB's for which a transaction
   * did not complete */
  for (t = ll->first; t != NULL; t = t->next) {
    if ((ufname = get_ufname (t->tfile)) == NULL) {
      trace (ERROR, default_trace, "reapply_transaction (): get_ufname "
	     "error.  Aborting reapply process.\n");
      return 0;
    }

      /* open up the !us...!ue update file */
    if ((ufin = fopen (ufname, "r+")) == NULL) {
      trace (ERROR, default_trace, "reapply_transaction (): "
             "Can't re-open update file (%s) for DB (%s): (%s)\n", 
	     ufname, t->db->name, strerror (errno));
      return 0;
    }

    trace (NORM, default_trace, "JW: reapply_trans (): calling scan_irr_file "
	   "(%s)\n", ufname);

    /* reapply the !us...!ue update file and journal file (if necessary) */
    if ((p = scan_irr_file (t->db, "update", 1, ufin)) != NULL)
      trace (ERROR, default_trace, "reapply_transaction (): scan_irr_file () "
	     "error, couldn't re-apply transaction: (%s)\n", p);
    /* update the journal for pre-rpsdit mirror and authoritative DB's */
    else if (((t->db->flags & IRR_AUTHORITATIVE) || 
	      t->db->mirror_prefix != NULL) &&
	     !update_journal (ufin, t->db, -1)) {
      trace (ERROR, default_trace, "error in updating journal for DB (%s) "
	     "rolling back transaction\n", t->db->name);
      p = "";
    }

    fclose (ufin);

    /* cleanup */
    if (p == NULL) {
      remove (t->tfile);
      remove (ufname);
      free   (ufname);
      trace (NORM, default_trace, "reapply_transaction (): DB (%s) "
	     "reapplied transaction successfully.\n", t->db->name);
    }
    else
      return 0;
  }

  free_rbtrans_list (ll);

  /* no errors, successful reapply transaction(s) */
  return 1;
}

/* For DB (db->name) determine if a transaction was in 
 * progress at the time of a system crash and return
 * the name of the transaction file.
 *
 * The definition of an "active transaction" is a transaction
 * for which there was a complete transaction file built and
 * the transaction was interupted before it could complete.
 * 
 * The transaction steps are like this:
 * 1. build a transaction file, the first and last lines
 *    are the current serial number for the transaction.
 *    The last step is to write the current serial number
 *    in the last line of the file and so a complete trans
 *    file can be determined by comparing the first and last
 *    lines of the file.
 * 2. irrd updates the DB with the trans (ie, call scan_irr_file ()).
 * 3. Update the journal (if necessary; for rpsdist files irrd does
 *    not keep a journal) and the last step is to update the
 *    current serial file.  So the current serial number in the 
 *    transaction file can be compared with the current serial
 *    number in the DB current serial to determine if a transaction
 *    completed.
 * 
 * The presence of a transaction file does not mean
 * the DB is in an inconsistent state.  There are 3 cases,
 * 1 of which action needs to be taken:
 *
 * Case 1.  Transaction file is present but is not complete.
 *  This means the system crashed while the transaction file
 *  was being built so there were no changes made to the DB.  
 *  For this case we remove the incomplete transaction file and no
 *  further action is taken.
 *
 * Case 2. Transaction file is present and is complete and the
 *  transaction current serial matches the DB current serial.
 * 
 *  Then the system crashed after the transaction had been
 *  fully applied and the transaction file should be removed.
 *  No other action is necessary.
 *
 * Case 3. Transaction file is present and is complete and the
 *  transaction current serial does not match the DB current serial.
 *
 *  Then the system crashed during the DB update.  For this case we 
 *  rollback the DB (and journal file if necessary) and re-apply the 
 *  update file.
 *
 * Input:
 *  -pointer to the DB struct for the DB we are checking (db)
 *  -char template to return the name of the transaction file
 *   if the transaction should be re-applied.
 *
 * Return:
 *  -1 if the transaction should be re-applied (Case 3 above)
 *     [Note: transaction file name is returned in (fname).]
 *  -0 Case 1 and 2
 *  -1 an error occured and the result could not be determined
 *
 *  Function will remove transaction files in cases 1 and 2 above.
 */
int active_trans (irr_database_t *db, char *fname) {
  long ts;
  FILE *fin;

  /* sanity check */
  if (fname == NULL) {
    trace (ERROR, default_trace, "active_trans ():  NULL transcation file "
	   "name.\n");
    return -1;
  }

  trace (NORM, default_trace, "JW: rollback fname (%s)\n", fname);
  
  /* open the transaction file which is used to restore the DB */  
  if ((fin = fopen (fname, "r")) == NULL) {
    trace (ERROR, default_trace, "active_trans ():  Can't open atomic "
	   "transaction file (%s) for DB (%s): (%s)\n", 
	   fname, db->name, strerror (errno));
    return -1;
  }
  
  /* Case #1: was the transaction file built completely? 
   * if not then no changes were made to the DB 
   * and the trans was not active */
  if ((ts = complete_trans_file (fin, fname, db->name)) <= 0) {
    /* Error: trans file I/O error */
    if (ts < 0)
      trace (NORM, default_trace, "active_trans (): transaction file (%s) "
	     "error.\n", fname);
    /* Case #1: incomplete trans file */
    else
      trace (NORM, default_trace, "active_trans (): transaction file (%s) "
	     "was not complete.\n", fname);
    
    return ts;
  }

  trace (NORM, default_trace, "JW: active_trans ts (%ld)\n", ts);

  /* Case #3: if there is no serial then it's probably a rpsdist DB;
   * if it's not an rpsdist DB we can re-apply the trans
   * anyway to try and fix the DB */
  if (!scan_irr_serial (db))
    return 1;

  /* Case #2 or Case #3: trans is active if the DB cs is less than 
   * the trans cs */
trace (NORM, default_trace, "JW: active_trans returns (ts, db->cs)=(%ld, %ld) (%d)\n", ts, (long) db->serial_number, ts > ((long) db->serial_number));
  return (ts > ((long) db->serial_number));
}


/* Determine if transaction file (fname) is complete or not.
 *
 * Input:
 *  -stream file pointer to the transaction file (fin)
 *  -file name of the transaction file (fname)
 *  -DB name associated with the transaction file (dbname)
 *
 * Return:
 *  -serial of the transaction if the transaction file is compelete
 *  -0 if the transaction file is not complete
 *  --1 if there was a file I/O error
 */
long complete_trans_file (FILE *fin, char *fname, char *dbname) {
  int ret_code = -1;
  long cs1 = 0, cs2 = 0, save_pos;

  /* restore original file position on exit */
  save_pos = ftell (fin);

  if (fseek (fin, 0L, SEEK_SET) < 0) {
    trace (ERROR, default_trace, "complete_trans_file (): trans file I/O "
	   "rewind file op error on trans file (%s) for DB (%s): %s.\n",
	   fname, dbname, strerror (errno));
    return -1;
  }
  
  /* read the current serial from the first line ... */    
  if (fscanf (fin, "ts: %ld\n", &cs1) != 1) {
    trace (NORM, default_trace, "complete_trans_file ():  Can't read line 1 "
	   "cs (%ld) for trans file (%s) for DB (%s)\n", cs1, fname, dbname);
    ret_code = 0;
    goto CLEAN_UP;
  }
  
trace (NORM, default_trace, "JW: complete_trans_file (): ts from first line of file (%ld)\n",
       cs1);

  /* ... seek the last line ... */
  if (fseek (fin, 0, SEEK_END) < 0 ||
      (cs2 = ftell (fin)) < 0) {
    trace (ERROR, default_trace, "complete_trans_file (): trans file I/O "
	   "seek EOF file op error on trans file (%s) for DB (%s): %s.\n",
	   fname, dbname, strerror (errno));
    goto CLEAN_UP;
  }

  /* if file is too small it cannot be complete */
  if (cs2 < 42) {
    ret_code = 0;
    goto CLEAN_UP;
  }

  /* read the last line */
  if (fseek (fin, cs2 - 15L, SEEK_SET)) {
    trace (ERROR, default_trace, "complete_trans_file ():  File I/O seek EOF "
	   "error on trans file (%s) for DB (%s): (%s)\n", 
	   fname, dbname, strerror (errno));
    goto CLEAN_UP;
  }

  /* ... read the current serial from the last line */    
  if (fscanf (fin, "ts: %ld\n", &cs2) != 1) {
    trace (NORM, default_trace, "complete_trans_file ():  Can't read last "
	   "line for trans file (%s) for DB (%s)\n", fname, dbname);
    ret_code = 0;
    goto CLEAN_UP;
  }

  trace (NORM, default_trace, "JW: complete_trans_file (): ts from last line of "
	 "file (%ld)\n", cs2);

  /* if we get here then we got cs1 and cs2 successfully
   * if they are equal then we have a complete transaction file */
  ret_code = (cs1 == cs2);

CLEAN_UP:

  /* restore the original file position */
  if (fseek (fin, save_pos, SEEK_SET) < 0)
    trace (ERROR, default_trace, "complete_trans_file (): trans file I/O "
	   "restore file pos  error on trans file (%s) for DB (%s): %s.\n", 
	   fname, dbname, strerror (errno));
  
  /* if they are the same we have a complete file */
  trace (NORM, default_trace, "JW: complete_trans_file (): return complete trans file? (%ld)\n", ((ret_code == 1) ? cs1 : (long) ret_code));

  /* return the serial # of the transaction ... */
  if (ret_code == 1)
    return cs1;

  /* ... otherwise return incomplete trans or I/O error */
  return ret_code;
}


/* Create a transaction object.
 *
 * Object will be part of a linked list of transactions
 * that should be re-applied because of a system crash.
 * 
 * Input:
 *  -pointer to the DB struct of the transaction.
 *  -full path name of the transaction file (fname)
 *
 * Return:
 *  -pointer to a trans_t record
 */
trans_t *create_rbtrans_obj (irr_database_t *db, char *fname) {
  trans_t *obj;

  obj = (trans_t *) malloc (sizeof (trans_t));
  obj->db    = db;
  obj->tfile = strdup (fname);

  return (obj);
}

/* Add (obj) to the end of the rollback transaction linked list.
 *
 * This list will be used by reapply_transction () to repply a transaction
 * which interupted by a system crash.
 *
 * Input:
 *  -pointer to the start of the transaction linked list (start)
 *  -object to be added to the list (obj)
 *
 * Return:
 *  -void
 */
void add_rbtrans_obj (rollback_t *start, trans_t *obj) {

  if (obj == NULL)
    return;

  obj->next = NULL;
  if (start->first == NULL)
    start->first = obj;
  else
    start->last->next = obj;

  start->last = obj;
}

/* Free the rollback memory linked list.
 *
 * Input:
 *   -pointer to the start of the linked list (start)
 *
 * Return:
 *  void
 */
void free_rbtrans_list (rollback_t *start) {
  trans_t *obj, *temp;

  obj = start->first;
  while (obj != NULL) {
    /*fprintf (dfile, "trans object: %s %s %s state (%d) \n", 
      obj->key, obj->type, obj->source, obj->state);*/
    temp = obj->next;
    free (obj->tfile);
    free (obj);
    obj = temp;
  }
} 

/* Determine if file (fname) exists in the directory
 * pointed to by (dirp).
 *
 * Function expects (dirp) to be an open directory pointer.
 *
 * Input:
 *  -pointer to the directory to be searched (dirp)
 *  -name of the file we are looking for (fnam)
 *   [Note: fname should be unqualified.]
 *
 * Return:
 *  -1 if fname is found in the directory
 *  -0 otherwise
 */
int file_exists (DIR *dirp, char *fname) {
  struct dirent *dp;

  rewinddir (dirp);
  while ((dp = readdir (dirp)) != NULL) {
    if (!strcmp (dp->d_name, fname)) {
trace (NORM, default_trace, "JW: yes file_exists (%s)\n", fname);
      return 1;
    }
  }
  
trace (NORM, default_trace, "JW: no file_exists (%s)\n", fname);

  return 0;
}

/* Rollback the journal file and currentserial file for (db) to 
 * their original state before the last transaction.
 *
 * Function assumes the transaction file is complete.  Function
 * will restore the journal file to it's original size and
 * reset the current serial file.
 *
 * Input;
 *  -pointer to the db struct for the journal we are rolling back (db)
 *  -fully qualified transaction file name (fname)
 *   [see build_transaction_file () for an explanation of the trans file.]
 *
 * Return:
 *  -1 if we restored the journal and current serial file to their
 *     original states without error
 *  -0 otherwise
 */
int journal_rollback (irr_database_t *db, char *fname) {
  FILE *fin;
  char buf[BUFSIZE+1];
  long cs, jsize;
  int ret_code = 0;

  /* sanity check */
  if (fname == NULL) {
    trace (ERROR, default_trace, "journal_rollback ():  NULL transcation file "
	   "name.\n");
    return 0;
  }

  /* is there a DB cache? */
  if (IRR.database_dir == NULL) {
    trace (ERROR, default_trace, "journal_rollback ():  NULL transcation file "
	   "name.\n");
    return 0;
  }

  trace (NORM, default_trace, "JW: journal_rollback fname (%s)\n", fname);
  
  /* open the transaction file which is used to restore the journal */  
  if ((fin = fopen (fname, "r")) == NULL) {
    trace (ERROR, default_trace, "journal_rollback (): Can't open atomic "
	   "transaction file (%s) for DB (%s): (%s)\n", 
	   fname, db->name, strerror (errno));
    return 0;
  }

  /* function assumes the transaction file built completely */

  /* read past the original current serial, journal file length,
   * and !us...!ue update file name */
  if (fgets  (buf, BUFSIZE, fin)    == NULL ||
      fscanf (fin, "%ld\n", &cs)    != 1    ||
      fscanf (fin, "%ld\n", &jsize) != 1) {
    trace (ERROR, default_trace, "journal_rollback ():  Can't read atomic "
	   "transaction file (%s) for DB (%s) the orig CS/update file: (%s):"
	   "\n", fname, db->name, strerror (errno));
    goto CLEAN_UP;
  } 

  trace (NORM, default_trace, "JW: journal_rollback cs (%ld) jsize (%ld)\n", 
	 cs, jsize);
  

  /* truncate the journal to it's original size */
  make_journal_name (db->name, JOURNAL_NEW, buf);
  if (truncate (buf, jsize) < 0) {
    trace (ERROR, default_trace, "journal_rollback ():  Can't truncate journal"
	   "file (%s) to (%ld) bytes: (%s)\n", buf, jsize, strerror (errno));
    goto CLEAN_UP;
  }
  
  /* set the original current serial */
  db->serial_number = cs;
  if (!write_irr_serial (db)) {
    trace (ERROR, default_trace, "journal_rollback ():  write_irr_serial () "
	   "error.\n");
    goto CLEAN_UP;
  }

  /* if we get here then there were no errors and we 
   * rolled back the journal and current serial file */
  ret_code = 1;

CLEAN_UP:

  fclose (fin);
  return ret_code;
trace (NORM, default_trace, "JW: journal_rollback () bye-bye!\n");
}

/* Get the update file name (ie, the !us...!ue name) from the transaction
 * file.
 *
 * Input:
 *  -full path name of the transaction file (tfname)
 *
 * Return:
 *  -the name of the update file
 *  -NULL if there was an error and the update file name could
 *   not be obtained.
 */
char *get_ufname (char *tfname) {
  int n;
  char buf[BUFSIZE+1];
  FILE *fin;

  /* sanity check */
  if (tfname == NULL) {
    trace (ERROR, default_trace, "get_ufname (): NULL transcation file "
	   "name.\n");
    return NULL;
  }

  trace (NORM, default_trace, "JW: get_ufname (%s)\n", tfname);
  
  /* open the transaction file  */  
  if ((fin = fopen (tfname, "r")) == NULL) {
    trace (ERROR, default_trace, "get_ufname ():  Can't open atomic "
	   "transaction file (%s): (%s)\n", tfname, strerror (errno));
    return NULL;
  }

  /* read past the cs, original current serial, journal file length,
   * and get the !us...!ue update file name */
  if (fgets (buf, BUFSIZE, fin) == NULL ||
      fgets (buf, BUFSIZE, fin) == NULL ||
      fgets (buf, BUFSIZE, fin) == NULL ||
      fgets (buf, BUFSIZE, fin) == NULL) {
    trace (ERROR, default_trace, "get_ufname ():  Can't read atomic "
	   "transaction file (%s) fgets () error: (%s)\n", 
	   tfname, strerror (errno));
    buf[0] = '\0';
    goto CLEAN_UP;
  } 

  /* remove the '\n' */
  if ((n = strlen (buf)) > 0)
    buf[n - 1] = '\0';

CLEAN_UP:

  fclose (fin);

  if (buf[0] == '\0')
    return NULL;

  return strdup (buf);
}

/* Update the journal file for DB (db->name) with the updates from
 * a transaction.
 *
 * Function writes the update entries to the journal and updates
 * the current serial file.
 *
 * Input:
 *  -file descriptor to the transaction file (!us...!ue) (fin)
 *  -pointer to the DB struct (db)
 *  -number of updates in this transaction, used for debug purposes (n_updates)
 *
 * Return:
 *  -1 if the transaction was successfully written to the journal
 *  -0 otherwise
 */
int update_journal (FILE *fin, irr_database_t *db, int n_updates) {
  char buf[BUFSIZE+1], tag[64];
  int n = 0, bl = 0;
  long save_fpos;

  /* sanity check */
  if (fin == NULL) {
    trace (ERROR, default_trace, "update_journal (): "
	   "NULL update file pointer.  Can't update the journal for DB (%s)", 
	   db->name);
    return 0;
  }

  /* rewind the update file */
  if ((save_fpos = ftell (fin)) < 0 ||
      fseek (fin, 0L, SEEK_SET) < 0) {
    trace (ERROR, default_trace, "update_journal (): "
	   "rewind file I/O error for DB (%s): (%s).\n", 
	   db->name, strerror (errno));
    return 0;
  }

  while (fgets (buf, BUFSIZE, fin) != NULL) {
    /* "%END" tells up we're done */
    if (!strncmp ("%END", buf, 4))
      break;
    
    /* write the serial stamp line */
    if (!memcmp (buf, "ADD", 3) ||
	!memcmp (buf, "DEL", 3)) {
      n++;
      sprintf (tag, "%% SERIAL %ld\n", ++db->serial_number);
      if (write (db->journal_fd, tag, strlen (tag)) < 0) {
	trace (ERROR, default_trace, "update_journal (): file I/O write tag "
	       "error: (%s)\n", strerror (errno));
	return 0;
      }
    }
    
    /* insure's we are not dumping extra blank lines at the end of
     * the update file or before/after an update */
    if (buf[0] == '\n')
      bl++;
    else
      bl = 0;

    /* write a journal data line */
    if (bl < 2 &&
	write (db->journal_fd, buf, strlen (buf)) < 0) {
      trace (ERROR, default_trace, "update_journal (): file I/O write data "
	     "error: (%s)\n", strerror (errno));
      return 0;
    }
  }

  /* restore the original file position */
  if (fseek (fin, save_fpos, SEEK_SET) < 0) {
    trace (ERROR, default_trace, "update_journal (): "
	   "restore file pos I/O error: (%s).\n", strerror (errno));
    return 0;
  }

  /* sanity check */
  if (n_updates > 0 && 
      n != n_updates) {
    trace (ERROR, default_trace, "update_journal (): number of updates "
	   "written to  journal (%d) does not match expected value (%d)\n", 
	   n, n_updates);
    return 0;    
  }


  /* the last step is to update the current serial file */
  return write_irr_serial (db); 
}


syntax highlighted by Code2HTML, v. 0.9.1