/* recedit.c -- Record editor for GNU. */

/* Copyright (C) 1988,1990 Free Software Foundation, Inc.

   This file is part of GNU Finger.

   GNU Finger is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published
   by the Free Software Foundation; either version 1, or (at your
   option) any later version.

   GNU Finger is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GNU Finger; see the file COPYING.  If not, write to the
   Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. */

#include <stdio.h>
#include <config.h>
#include <sys/types.h>
#include <sys/file.h>
#include <general.h>
#include "recedit.h"

/* Print FUNC complaining that TYPE is a bad type. */
static
bad_field_type (func, type)
     char *func;
     int type;
{
  fprintf (stderr, "Bad field type (%d) in %s.\n", type, func);
  abort ();
}

/* Create a new, blank FIELD_DESC and return a pointer to it. */
static FIELD_DESC *
new_field_desc ()
{
  FIELD_DESC *field = (FIELD_DESC *)xmalloc (sizeof (FIELD_DESC));

  bzero (field, sizeof (FIELD_DESC));

  return (field);
}

/* Free the memory used by the FIELDS. */
void
re_free_fields (fields)
     FIELD_DESC **fields;
{
  register int i;
  FIELD_DESC *field;

  for (i = 0; field = fields[i]; i++)
    {
      if (field->type == Char && field->data.chardata)
	free (field->data.chardata);

      free (field->name);
      free (field);
    }

  free (fields);
}

/* Return a copy of the fields in FIELDS. */
FIELD_DESC **
re_copy_fields (fields)
     FIELD_DESC **fields;
{
  register int i;
  FIELD_DESC *field, **newfields;

  /* Count the fields. */
  for (i = 0; fields[i]; i++);

  newfields = (FIELD_DESC **)xmalloc ((1 + i) * sizeof (FIELD_DESC *));

  /* Copy the fields. */

  for (i = 0; field = fields[i]; i++)
    {
      FIELD_DESC *newfield = new_field_desc ();

      newfield->name = savestring (field->name);
      newfield->type = field->type;
      newfield->attributes = field->attributes;

      switch (field->type)
	{
	case Char:
	  if (field->data.chardata)
	    newfield->data.chardata = savestring (field->data.chardata);
	  break;

	case Int:
	  newfield->data.intdata = field->data.intdata;
	  break;

	case Date:
	  {
	    DATE *newdate = &(newfield->data.date);
	    DATE *date = &(field->data.date);

	    newdate->month = date->month;
	    newdate->day = date->day;
	    newdate->year = date->year;
	    break;
	  }

	case Phone:
	  {
	    PHONE *newphone = &(newfield->data.phone);
	    PHONE *phone = &(field->data.phone);

	    newphone->area_code = phone->area_code;
	    newphone->prefix = phone->prefix;
	    newphone->number = phone->number;
	    break;
	  }
	}
      newfields[i] = newfield;
    }

  newfields[i] = (FIELD_DESC *)NULL;

  return (newfields);
}

/* Passed a pointer to some packed data, and a pointer to an array of
   field descriptors, fill in the data slots in the array of fields from
   the contents of the packed data.  Storage space for character strings
   is malloc'ed.  */
void
re_extract_fields (data, fields)
     char *data;
     FIELD_DESC **fields;
{
  FIELD_DESC *field;
  char *here = data;
  register int i;

  for (i = 0; field = fields[i]; i++)
    {
      switch (field->type)
	{
	case Char:
	  field->data.chardata = savestring (here);
	  here += strlen (here) + 1;
	  break;

	case Int:
	  bcopy (here, &(field->data.intdata), sizeof (int));
	  here += sizeof (int);
	  break;

	case Date:
	  bcopy (here, &(field->data.date), sizeof (DATE));
	  here += sizeof (DATE);
	  break;

	case Phone:
	  bcopy (here, &(field->data.phone), sizeof (PHONE));
	  here += sizeof (PHONE);
	  break;

	default:
	  bad_field_type ("re_extract_fields", field->type);
	}
    }
}

/* Return the total storage space needed to store this set of fields. */
int
re_data_length (fields, attributes)
     FIELD_DESC **fields;
     int attributes;
{
  register int i;
  FIELD_DESC *field;
  int len = 0;

  for (i = 0; field = fields[i]; i++)
    {
      if (!attributes ||
	  (field->attributes & attributes) == attributes)
	{
	  switch (field->type)
	    {
	    case Char:
	      if (!field->data.chardata)
		len += 1;
	      else
		len += 1 + strlen (field->data.chardata);
	      break;

	    case Int:
	      len += sizeof (int);
	      break;

	    case Date:
	      len += sizeof (DATE);
	      break;

	    case Phone:
	      len += sizeof (PHONE);
	      break;

	    default:
	      bad_field_type ("re_data_length", field->type);
	    }
	}
    }
  return (len);
}

/* Return a pointer to a string of bytes which is the packed representation
   of the info in FIELDS.  ATTRIBUTES, if non-zero, specifies which
   attributes the fields must have.  The space for the returned data
   is malloc'ed. */
char *
re_extract_data (fields, attributes)
     FIELD_DESC **fields;
     int attributes;
{
  register FIELD_DESC *field;
  register int i, len;
  char *data, *here;

  /* Find the total amount of space needed to store this record. */
  len = re_data_length (fields, attributes);

  data = (char *)xmalloc (len);
  here = data;

  for (i = 0; field = fields[i]; i++)
    {
      if (!attributes || (field->attributes & attributes) == attributes)
	{
	  switch (field->type)
	    {
	    case Char:
	      if (!field->data.chardata)
		*here++ = '\0';
	      else
		{
		  strcpy (here, field->data.chardata);
		  here += strlen (here) + 1;
		}
	      break;

	    case Int:
	      bcopy (&(field->data.intdata), here, sizeof (int));
	      here += sizeof (int);
	      break;

	    case Date:
	      bcopy (&(field->data.date), here, sizeof (DATE));
	      here += sizeof (DATE);
	      break;

	    case Phone:
	      bcopy (&(field->data.phone), here, sizeof (PHONE));
	      here += sizeof (PHONE);
	      break;

	    default:
	      bad_field_type ("re_extract_data", field->type);
	    }
	}
    }
  return (data);
}

/* The format of input data to re_read_description () is a series of lines
   read from a stream.  Each line contains a field name followed by a
   colon, then the recedit type name, and then optionally, some attributes,
   separated by commas.  The possible attributes are:

	key:	This field is part of the key for these records.  More
		than one field can have this flag; the contents of
		those fields are concatenated to make the key.

	privileged: This field can only be modified by the superuser.

	invisible: This field doesn't normally appear in the record
		   display.

  Lines which have a `#' as the first non-blank character are comments;
  they have no effect on the record description.

  An example record description might be:

  	User Name: Char  key, privileged
	Real Name: Char

  which says that this record contains two fields.  First, a character type
  field which has the label "User Name", and which is the key field.  The
  User Name field can only be modified by the superuser.  Second, a field
  which has the label "Real Name", consists of character data, and has
  no special attributes.  The record description is ended at the end of
  file, or at the string "End." */

typedef struct {
  char *name;
  int value;
} ALIST;

ALIST type_value_alist[] = {
  { "Char", Char },
  { "Int", Int },
  { "Date", Date },
  { "Phone", Phone },
  { (char *)NULL, 0 }
};

ALIST attributes_value_alist[] = {
  { "key", FieldKey },
  { "privileged", FieldPrivileged },
  { "invisible", FieldInvisible },
  { (char *)NULL, 0 }
};

/* Read the format of the database from FILENAME.  Return
   a pointer to an array of pointers to fields. */
FIELD_DESC **
re_read_description_file (filename)
     char *filename;
{
  FILE *file = fopen (filename, "r");
  FIELD_DESC **fields;

  if (!file)
    {
      perror (filename);
      return (FIELD_DESC **)NULL;
    }

  fields = re_read_description (file);
  fclose (file);

  return (fields);
}

/* Read the description of a record from STREAM.  Return a pointer to
   an array of pointers of FIELD_DESC which describe what a record
   looks like.  */
FIELD_DESC **
re_read_description (stream)
     FILE *stream;
{
  register int i;
  char *temp, line[256];
  FIELD_DESC **result = (FIELD_DESC **)NULL;
  FIELD_DESC *field;
  int result_size = 0, result_index = 0;
  char last_char;

  while (temp = fgets (line, sizeof (line), stream))
    {
      temp[strlen (temp) - 1] = '\0';

      if (xstricmp (temp, "End.") == 0)
	break;

      /* Skip leading whitespace. */
      while (whitespace (*temp))
	temp++;

      /* If at comment, skip the remainder of this line. */
      if (!*temp || *temp == '#')
	continue;

      /* Create a place holder for this field. */
      if (result_index + 1 >= result_size)
	{
	  if (!result)
	    result = (FIELD_DESC **)
	      xmalloc ((result_size = 10) * sizeof (FIELD_DESC *));
	  else
	    result = (FIELD_DESC **)
	      xrealloc (result, (result_size += 10) * sizeof (FIELD_DESC *));
	}

      field = new_field_desc ();
      result[result_index++] = field;
      result[result_index] = (FIELD_DESC *)NULL;

      /* Isolate the field name. */
      for (i = 0; temp[i] && temp[i] != ':'; i++);
      last_char = temp[i];
      temp[i] = '\0';
      field->name = savestring (temp);
      temp += i;
      if (last_char)
	temp++;

      /* Skip whitespace. */
      while (whitespace (*temp))
	temp++;

      /* Isolate the field type. */
      for (i = 0; temp[i] && !whitespace (temp[i]); i++);
      last_char = temp[i];
      temp[i] = '\0';

      for (i = 0; type_value_alist[i].name; i++)
	if (xstricmp (temp, type_value_alist[i].name) == 0)
	  {
	    field->type = type_value_alist[i].value;
	    break;
	  }

      if (!field->type)
	{
	  fprintf (stderr, "Bad type name `%s'.\n", temp);
	  abort ();
	}

      /* See if there are attributes on this line. */
      temp += strlen (temp);
      if (last_char)
	temp++;

      /* Get the attributes from the rest of the line. */
      while (*temp)
	{
	  /* Skip whitespace. */
	  while (whitespace (*temp))
	    temp++;

	  /* If comment, all done with line. */
	  if (!*temp || *temp == '#')
	    break;

	  /* Isolate flag. */
	  for (i = 0; temp[i] && !whitespace (temp[i]) && temp[i] != ','; i++);
	  last_char = temp[i];
	  temp[i] = '\0';

	  for (i = 0; attributes_value_alist[i].name; i++)
	    if (xstricmp (temp, attributes_value_alist[i].name) == 0)
	      {
		field->attributes |= attributes_value_alist[i].value;
		break;
	      }

	  temp += strlen (temp);
	  if (last_char)
	    temp++;
	}
    }
  return (result);
}

/* Output the field names and contents of FIELDS to STREAM. */
void
re_print_fields (fields, stream)
     FIELD_DESC **fields;
     FILE *stream;
{
  register int i, longest = 0;
  FIELD_DESC *field;

  /* Find the longest field name. */
  for (i = 0; field = fields[i]; i++)
    {
      if (field->attributes & FieldInvisible)
	continue;

      if (strlen (field->name) >= longest)
	{
	  longest = strlen (field->name);
	  if ((field->attributes & FieldPrivileged) &&
	      geteuid () != 0)
	    longest++;
	}
    }

  /* Now print them. */
  for (i = 0; field = fields[i]; i++)
    {
      int field_width = longest;

      if (field->attributes & FieldInvisible)
	continue;

      if ((field->attributes & FieldPrivileged) && geteuid () != 0)
	field_width--;

      fprintf (stream, "%s%*s: ", (field_width != longest) ? "*" : "",
	       field_width, field->name);

      /* Print the value of this field. */
      switch (field->type)
	{
	case Char:
	  if (field->data.chardata)
	    fprintf (stream, "%s", field->data.chardata);
	  break;

	case Int:
	  fprintf (stream, "%d", field->data.intdata);
	  break;

	case Date:
	  {
	    DATE *date = &(field->data.date);

	    fprintf (stream, "%02d/%02d/%02d",
		     date->month, date->day, date->year);
	    break;
	  }

	case Phone:
	  {
	    PHONE *phone = &(field->data.phone);

	    fprintf (stream, "(%03d) %03d-%04d",
		     phone->area_code, phone->prefix, phone->number);
	    break;
	  }

	default:
	  fprintf (stream, "!!Unknown Type!!");
	  field->attributes |= FieldPrivileged;
	}

      fprintf (stream, "\n");
      fflush (stream);
    }
}

/* Return a FIELD_DESC * to the field in FIELDS that has FIELD_NAME as
   its name.  Return a NULL pointer if that field cannot be found. */
FIELD_DESC *
re_find_field (fields, field_name)
     FIELD_DESC **fields;
     char *field_name;
{
  register int i;
  FIELD_DESC *field;

  for (i = 0; field = fields[i]; i++)
    if (xstricmp (field_name, field->name) == 0)
      return (field);

  return ((FIELD_DESC *)NULL);
}

/* Search through FIELDS, and return the data value associated with
   FIELD_NAME.  This returns a pointer to the data field. */
void *
re_get_field_value (fields, field_name)
     FIELD_DESC **fields;
     char *field_name;
{
  FIELD_DESC *field;

  field = re_find_field (fields, field_name);

  if (field)
    return (void *)&(field->data.chardata);
  else
    return ((void *)NULL);
}

/* Directly modify the field with name NAME in FIELDS, forcing the
   data value to be DATA.  If the field wasn't found, then return
   non-zero, else zero. */
int
re_set_field (fields, name, data)
     FIELD_DESC **fields;
     char *name, *data;
{
  FIELD_DESC *field;

  field = re_find_field (fields, name);

  if (field)
    {
      if ((field->attributes & FieldPrivileged) == 0 ||
	  geteuid () == 0)
	{
	  switch (field->type)
	    {
	    case Char:
	      field->data.chardata = data;
	      break;
	    case Int:
	      field->data.intdata = (int)data;
	      break;

	    case Date:
	      bcopy (data, &(field->data.date), sizeof (DATE));
	      break;

	    case Phone:
	      bcopy (data, &(field->data.phone), sizeof (PHONE));
	      break;
	    }
	}
      return (0);
    }
  return (1);
}

/* Modify the contents of FIELDS from the data read from STREAM.
   Each line read from stream has a field name followed by a colon,
   and then the value for that field.  Stop reading values when the
   stream is exhausted, or if a line containing "End." is read. */
void
re_eval_fields (fields, stream)
     FIELD_DESC **fields;
     FILE *stream;
{
  register int i;
  char *temp, line[256], field_name[256];
  char last_char;
  FIELD_DESC *field;

  while (temp = fgets (line, sizeof (line), stream))
    {
      temp[strlen (temp) - 1] = '\0';

      /* Skip leading whitespace. */
      while (whitespace (*temp) || *temp == '*')
	temp++;

      if (xstricmp (temp, "End."))
	break;

      /* If at comment, skip the remainder of this line. */
      if (!*temp || *temp == '#')
	continue;

      /* Isolate the field name. */
      for (i = 0; temp[i] && temp[i] != ':'; i++);
      last_char = temp[i];
      temp[i] = '\0';
      strcpy (field_name, temp);
      temp += i;

      if (last_char)
	temp++;

      /* Skip whitespace in front of value. */
      while (whitespace (*temp))
	temp++;

      /* Find the field to modify, and set its value. */
      field = re_find_field (fields, field_name);

      if (field)
	{
	  if ((field->attributes & FieldPrivileged) &&
	      geteuid () != 0)
	    continue;

	  switch (field->type)
	    {
	    case Char:
	      if (field->data.chardata)
		free (field->data.chardata);

	      field->data.chardata = savestring (temp);
	      break;

	    case Int:
	      sscanf (temp, "%d", &(field->data.intdata));
	      break;

	    case Date:
	      {
		DATE *date = &(field->data.date);

		sscanf (temp, "%d/%d/%d",
			&(date->month), &(date->day), &(date->year));
		break;
	      }

	    case Phone:
	      {
		PHONE *phone = &(field->data.phone);

		sscanf (temp, "(%d) %d-%d", &(phone->area_code),
			&(phone->prefix), &(phone->number));
		break;
	      }

	    default:
	      bad_field_type ("re_eval_fields", field->type);
	    }
	  continue;
	}
      else
	{
	  /* The field name wasn't found.  Ignore that fact. */
	}
    }
}
