Conversion functions for JFile PDB databases

Here's the source layout.

<*>=
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>


#include "pushchar.h"
#include "pdblua.h"
#include "pdbio.h"
#include "pdbrep.h"
#include "pdbconv.h"

<type and macro definitions>
<functions>

JFile support info

Now we get into stuff specific to JFile. First, JFile's sizes and limits.

<type and macro definitions>= (<-U) [D->]
#define         CURRENT_VERSION                 452
#define         MAX_FIND_STRING                 16
#define         MAX_PASSWORD_LENGTH             10
#define         MAX_FIELDS                      20
#define         MAX_FIELD_NAME_LENGTH           20     
#define         MAX_DATA_LENGTH                 500    
#define         MAX_TOTAL_POPUP_LENGTH          4000        
#define         MAX_RECORDS                     50000      
#define         MAX_RECORD_LENGTH               4000

typedef enum fieldtype {
  FLDTYPE_STRING  = 0x0001,
  FLDTYPE_BOOLEAN = 0x0002,
  FLDTYPE_DATE    = 0x0004,
  FLDTYPE_INT     = 0x0008,
  FLDTYPE_FLOAT   = 0x0010,
  FLDTYPE_TIME    = 0x0020,
  FLDTYPE_LIST    = 0x0040
} Fieldtype;
static char *typestring(Fieldtype t);
Defines CURRENT_VERSION, Fieldtype, MAX_DATA_LENGTH, MAX_FIELD_NAME_LENGTH, MAX_FIELDS, MAX_FIND_STRING, MAX_PASSWORD_LENGTH, MAX_RECORD_LENGTH, MAX_RECORDS, MAX_TOTAL_POPUP_LENGTH, typestring (links are to index).

String format for types. These should be used on input, too, and they aren't. Bad dog.

<functions>= (<-U) [D->]
static char *typestring(Fieldtype t) {
  switch (t) {
    case FLDTYPE_STRING: return "string";  
    case FLDTYPE_BOOLEAN: return "boolean"; 
    case FLDTYPE_DATE: return "date";    
    case FLDTYPE_INT: return "int";     
    case FLDTYPE_FLOAT: return "float";   
    case FLDTYPE_TIME: return "time";     
    case FLDTYPE_LIST: return "popup";    
    default: return "unknown";
  }
}
Defines typestring (links are to index).

<type and macro definitions>+= (<-U) [<-D]
typedef struct {
        char    fieldNames[MAX_FIELDS][MAX_FIELD_NAME_LENGTH+1];
        short   fieldTypes[MAX_FIELDS];                 /* values are the FLDTYPE_... defines above */
        short   numFields;
        short   version;                                /* should be equal to the CURRENT_VERSION above */
        short   showDBColumnWidths[MAX_FIELDS];         /* width in pixels to display for each column */
        short   showDataWidth;                          /* width in pixels of the data area when editing a record */
        short   sort1Field;                             /* which fields were last chosen to sort on */
        short   sort2Field;                             /* secondary... */
        short   sort3Field;                             /* tertiary.... */
        short   findField;                              /* which field was last 'Find' */
        short   filterField;                            /* which field was last 'Filter' */
        char    findString[MAX_FIND_STRING];            /* the previous 'Find' string */
        char    filterString[MAX_FIND_STRING];          /* the previous 'Filter' string */
        short   lockPWOnExit;                           /* 1 if auto-lock a password'ed db is on, 0 otherwise */
        short   firstColumnToShow;                      /* which field to show as the first field  */
        char    password[MAX_PASSWORD_LENGTH+2];        /* password for this db, or the empty string */
        char*   popupLists;             
} JFileAppInfoType;
Defines JFileAppInfoType (links are to index).

<functions>+= (<-U) [<-D->]
static JFileAppInfoType readinfo(Text *t) {
  JFileAppInfoType info;
  int i;
  get_chars(info.fieldNames, t, sizeof(info.fieldNames));
  for (i=0; i < MAX_FIELDS; i++) info.fieldTypes[i] = get_short(t);
  info.numFields = get_short(t);
  info.version = get_short(t);
  for (i=0; i < MAX_FIELDS; i++) info.showDBColumnWidths[i] = get_short(t);
  info.showDataWidth = get_short(t);
  info.sort1Field = get_short(t);
  info.sort2Field = get_short(t);
  info.sort3Field = get_short(t);
  info.findField = get_short(t);
  info.filterField = get_short(t);
  get_chars(info.findString, t, sizeof(info.findString));
  get_chars(info.filterString, t, sizeof(info.filterString));
  info.lockPWOnExit = get_short(t);
  info.firstColumnToShow = get_short(t);
  get_chars(info.password, t, sizeof(info.password));
  info.popupLists = (char *)t->p;
  return info;
}
Defines readinfo (links are to index).

Popups---their reading and writing

The popupLists in the above structure is a actually just a chunk of memory with a continuous sequence of null terminated strings that holds the contents of the items in the popup lists for this database. Lists are seperated with the string "popupX", where X is a,b,c.... for field item 1,2,3... etc. The list must be terminated with two null characters in a row, and must be shorter than 4000 chars in length.

Example: "popupb\0First 2\0Second 2\0popupd\0First 4\0Second4\0Third 4\0\0"

The above string would translate to the following popup lists in JFile:

For the second field (ie field 'b'):

First 2 Second 2

For the fourth field (ie field 'd'):

First 4 Second 4 Third 4

Note that the app info structure should not be saving a pointer to the field at all, it should save the string above as an example - this means that when creating the .pdb file it is necessary to compute the size of the required appinfo block as the program runs if you are handling conversion TO JFile .pdb format. If no popup lists are to be defined, just save 4 '\0' characters into the space alloted for the popupLists.

Reading in popup lists is easy; just wait for the two nulls.

Writing the Lua database

Read in .pdb and write Lua. I include code to convert to CSV if desired. This is another monster that should be split.

<functions>+= (<-U) [<-D->]
static void dumpinfo(FILE *out, struct pdb *db, Text txt) {
  int i;
  Text *t = &txt;
  JFileAppInfoType info = readinfo(t);
        
  fprintf(out, "\n    {\n");
  fprintf(out, "    -- standard relation between types and typenums\n");
  fprintf(out, "      Typemap = {\n        ");
#define DUMP(X) fprintf(out, "%s = %d, ", typestring(FLDTYPE_ ## X), FLDTYPE_ ## X);
  DUMP(STRING) DUMP(BOOLEAN) DUMP(DATE) DUMP(INT) DUMP(FLOAT) DUMP(TIME)
  fprintf(out, "%s = %d\n      },\n", typestring(FLDTYPE_LIST), FLDTYPE_LIST);
  fprintf(out, "      version = %d,\n", info.version);
  fprintf(out, "      fields = {\n");
  lua_beginblock();
  for(i=0; i<info.numFields; i++) {
    fprintf(out, "        { type = \"%s\", typenum = %d, width = %d, name = %s",
           typestring(info.fieldTypes[i]),
           info.fieldTypes[i], info.showDBColumnWidths[i],
           QUOTE(info.fieldNames[i]));
    <possibly print popups for field i>
    fprintf(out, " }%s\n", comma(i < info.numFields));
  }
  lua_endblock();
  fprintf(out, "      },\n      dataWidth = %d, firstColumnToShow = %d,\n",
         info.showDataWidth, info.firstColumnToShow); 
  fprintf(out, "      findField = %d, filterField = %d,\n",
         info.findField, info.filterField);
  fprintf(out, "      findString = %s, filterString = %s,\n",
         quote(info.findString, 1), quote(info.filterString, 1));
  fprintf(out, "      sortFields = { %d, %d, %d },\n",
         info.sort1Field, info.sort2Field, info.sort3Field);
  fprintf(out, "      password = %s, lock = %d,\n", quote(info.password, 1),
         info.lockPWOnExit);
  fprintf(out, "    }");
}
Defines DUMP, dumpinfo (links are to index).

<possibly print popups for field i>= (<-U)
{ char *p;
  for(p = info.popupLists; strlen(p) != 0; p += strlen(p)+1)
    if (!strncmp(p, "popup", 5) && strlen(p) == 6 && p[5] == 'a' + i)
      break;
  if (strlen(p)) {
    lua_beginblock();
    fprintf(out, ",\n           popups = { ");
    for (p += strlen(p)+1; strlen(p) && !(!strncmp(p, "popup", 5) && strlen(p) == 6);){
      fprintf(out, "%s", QUOTE(p));
      p += strlen(p)+1;
      fprintf(out, "%s", strlen(p) == 0 || !strncmp(p, "popup", 5) ? "" : ", ");
    }
    fprintf(out, " },\n       ");
    lua_endblock();
  }
}

<functions>+= (<-U) [<-D->]
static char *infodocs[] = {
  "`version' is required.",
  "The `fields' array is required, and every element of it must be a",
  "table or a string.  (If a string, it stands for a field with that",
  "name and all else defaulted.)",
  "The table doesn't have to contain anything, but 'name' is",
  "highly recommended.  typenum has priority over type if both are",
  "present; missings types are strings.  The type field can only be",
  "interpreted if the Typemap array is present. ",
  "Everything else besides `fields' and `version' can be defaulted.",
  0
};
Defines infodocs (links are to index).

<functions>+= (<-U) [<-D->]
static int WalkLuaPopups(FILE *fp, int nfields);
static int pack(FILE *pdb, int appinfo) {
  JFileAppInfoType info;
  int nfields;
  memset(&info, 0, sizeof(info));
  if (lua_dostring("info = Database.appInfo")) assert(0);
  if (lua_isnil(objectval("info.fields"))) {
    fprintf(stderr, "appInfo for JFile must have `fields'\n");
    exit(1);
  }
  nfields = intval("listlength(info.fields)");
  if (pdb) {
    int i;
    <set the fields of the info structure>
    <write the info structure>
  }
  return sizeof(info) - 4 + WalkLuaPopups(pdb, nfields);
}
Defines pack (links are to index).

<set the fields of the info structure>= (<-U)
for (i = 0; i < MAX_FIELDS; i++) {
  lua_beginblock();
  lua_pushnumber((double)(i+1));
  lua_setglobal("i");
  if (lua_dostring("if type(info.fields[i]) == 'string' then "
                   "  info.fields[i] = { name = info.fields[i] } "
                   "end")) 
    assert(0);
  setblock(info.fieldNames[i],
           "(info.fields[i] and (info.fields[i].name or ('Field ' .. i))) or ''");
  info.fieldTypes[i] = intopt("info.fields[i] and (info.fields[i].typenum or "
                                   "(info.fields[i].type and info.Typemap and"
                                   "       info.Typemap[info.fields[i].type]))",
                                   FLDTYPE_STRING);
  info.showDBColumnWidths[i] = intopt("info.fields[i] and info.fields[i].width", 80);
  lua_endblock();
}
info.numFields = intval("listlength(info.fields)");
info.version = intval("info.version");
info.showDataWidth = intopt("info.dataWidth", 80);
info.sort1Field = intopt("info.sortFields and info.sortFields[1]", 0);
info.sort2Field = intopt("info.sortFields and info.sortFields[2]", 0);
info.sort3Field = intopt("info.sortFields and info.sortFields[3]", 0);
info.findField = intopt("info.findField", 0);
info.filterField = intopt("info.filterField", 0);
setblock(info.findString, "info.findString or ''");                 
setblock(info.filterString, "info.filterString or ''");                 
info.lockPWOnExit = intopt("info.lock", 0);
info.firstColumnToShow = intopt("info.firstColumnToShow", 1);
setstringopt(info.password, "info.password", "", MAX_PASSWORD_LENGTH);

<write the info structure>= (<-U)
put_block(pdb, info.fieldNames);
for (i = 0; i < MAX_FIELDS; i++)
  put_short(pdb, info.fieldTypes[i]);
put_short(pdb, info.numFields);
put_short(pdb, info.version);
for (i = 0; i < MAX_FIELDS; i++)
  put_short(pdb, info.showDBColumnWidths[i]);
put_short(pdb, info.showDataWidth);
put_short(pdb, info.sort1Field);
put_short(pdb, info.sort2Field);
put_short(pdb, info.sort3Field);
put_short(pdb, info.findField);
put_short(pdb, info.filterField);
put_block(pdb, info.findString);
put_block(pdb, info.filterString);
put_short(pdb, info.lockPWOnExit);
put_short(pdb, info.firstColumnToShow);
put_block(pdb, info.password);

Popups---their reading and writing

The popupLists in the above structure is a actually just a chunk of memory with a continuous sequence of null terminated strings that holds the contents of the items in the popup lists for this database. Lists are seperated with the string "popupX", where X is a,b,c.... for field item 1,2,3... etc. The list must be terminated with two null characters in a row, and must be shorter than 4000 chars in length.

Example: "popupb\0First 2\0Second 2\0popupd\0First 4\0Second4\0Third 4\0\0"

The above string would translate to the following popup lists in JFile:

For the second field (ie field 'b'):

First 2 Second 2

For the fourth field (ie field 'd'):

First 4 Second 4 Third 4

Note that the app info structure should not be saving a pointer to the field at all, it should save the string above as an example - this means that when creating the .pdb file it is necessary to compute the size of the required appinfo block as the program runs if you are handling conversion TO JFile .pdb format. If no popup lists are to be defined, just save 4 '\0' characters into the space alloted for the popupLists.

The following function both computes the size of the popup lists and (if its argument is non-null) emits them. We need the size first in order to compute the size of the app-info area, which we need in order to compute the record offsets, which have to be written first. Therefore this function is always called exactly twice: once with NULL, and once with the output file.

<functions>+= (<-U) [<-D->]
static int WalkLuaPopups(FILE *fp, int nfields) {
  int i, j;
  int len = 0;
  lua_beginblock();
  for (i = 0; i < nfields; i++) {
    lua_pushnumber((double)(i+1));
    lua_setglobal("i");
    if (!lua_istable(objectval("Database.appInfo.fields[i]")) ||
        !lua_istable(objectval("Database.appInfo.fields[i].popups")))
      continue;
    len += strlen("popupX") + 1;
    if (fp) fprintf(fp, "popup%c%c", 'a' + i, 0);
    lua_beginblock();
    for (j = 1; ; j++) {
      lua_Object item;
      lua_pushnumber((double)j);
      lua_setglobal("j");
      item = objectval("Database.appInfo.fields[i].popups[j]");
      if (!lua_isstring(item))
        break;
      len += strlen(lua_getstring(item)) + 1;
      if (fp) (void)fprintf(fp, "%s%c", lua_getstring(item), 0);
    }
    lua_endblock();
  }
  lua_endblock();
  len++;
  if (fp) (void)putc(0, fp);
  while (len < 4) {
    len++;
    if (fp) (void)putc(0, fp);
  }
  return len;
}
Defines WalkLuaPopups (links are to index).

<functions>+= (<-U) [<-D->]
static char *trailers[] = {
  "-- take a Database and write comma-separated values",
  "-- note this doesn't use C-like quoting conventions",
  "function writecsv(t)",
  "  local fields, records",
  "  i = 1",
  "  while t.appInfo.fields[i] do",
  "    if i > 1 then write(',') end",
  "    write(csvquote(t.appInfo.fields[i].name))",
  "    i = i + 1",
  "  end",
  "  write('\\n')",
  "",
  "  j = 1",
  "  while t.records[j] do",
  "    i = 1",
  "    while t.records[j].data[i] do",
  "      if i > 1 then write(',') end",
  "      write(csvquote(t.records[j].data[i]))",
  "      i = i + 1",
  "    end",
  "    write('\\n')",
  "    j = j + 1",
  "  end",
  "end",
  "",
  "function csvquote(s)",
  "  -- convert newlines to blanks and repeat double quotes",
  "  return '\"' .. gsub(gsub(s, '\"', '\"\"'), '[\\n\\r]', ' ') .. '\"'",
  "end",
  (char *)0
};

Defines trailers (links are to index).

<functions>+= (<-U) [<-D]
struct PDBConv JFile = {
  "JFile database",
  "JbDb", "JBas",
  { infodocs, dumpinfo, pack },
  { 0, generic_unpackInfo, generic_packInfo },
  { 0, generic_unpackRecord, generic_packRecord },
  trailers
};