home *** CD-ROM | disk | FTP | other *** search
/ Chip 2001 Mobile / Chip_Mobile_2001.iso / palm / business / timeshee / timeshee.exe / timesheet / Timesheet2CSV.c < prev    next >
Encoding:
C/C++ Source or Header  |  1999-04-10  |  23.9 KB  |  661 lines

  1. /******************************************************************
  2. * Timesheet2CSV.c
  3. * (c) Copyright Stuart Nicholson, 1998-1999.
  4. * Utility for converting stored Palmpilot Timesheet database (*.PDB) file into
  5. * a more useful CSV (comma separated values) format which can be loaded into
  6. * most common office suites.
  7. *
  8. * WARNING: This code is kludgy and Intel x86 SPECIFIC as it makes some extremely
  9. *          horrible assumptions about byte ordering and such. I would most grateful if
  10. *          a Macintosh programmer out there could massage this code and release
  11. *          a Mac friendly version. The problematic sections start with a comment
  12. *          beginning 'WARNING:'.
  13. *
  14. * author : stuart nicholson
  15. * email  : snic@ihug.co.nz
  16. * version: 15-November-1998
  17. ******************************************************************/
  18.  
  19. /******************************************************************
  20. * #includes
  21. ******************************************************************/
  22. #include <stdio.h>
  23. #include <stdlib.h>
  24. #include <string.h>
  25. #include <time.h>
  26.  
  27. /******************************************************************
  28. * #defines
  29. ******************************************************************/
  30. /* The application database type */
  31. #define TSDBType    ('data')
  32. /* The application database name */
  33. #define TSDBName    ("TimesheetDB")
  34. /* The version number of the database (more or less corresponds to the application version number). */
  35. #define TSDBMajorVersion (1)
  36. #define TSDBMinorVersion (5)
  37. #define TSDBVersion ((TSDBMajorVersion << 8) | (TSDBMinorVersion))
  38.  
  39. /* The maximum number of client, projects or task entries allowed,
  40. * including the the 2 automatic entries of 'none' and 'Edit...' */
  41. #define TSMaxCatEntries_v140     (22)
  42. #define TSMaxCatEntries_v150     (102)
  43. /* The length of each client, project or task list entry. */
  44. #define TSMaxCatEntryLen_v110    (15 + 1)
  45. #define TSMaxCatEntryLen_v120     (17 + 1)
  46.  
  47. /* The database record index of the first actual day record in the timesheet database.
  48. * All records prior to this record hold application resource information. */
  49. #define TSFirstDayRecIdx     (4)
  50.  
  51. /* WARNING: Intel specific! */
  52. /* convert Motorola shorts and ints to Intel shorts and ints */
  53. #define MAC2PC_SHORT(s) (((s & 0x00FF) << 8) | (s >> 8))
  54. #define MAC2PC_INT(i) (((i & 0x000000FF) << 24) | ((i & 0x0000FF00) << 8) | ((i & 0x00FF0000) >> 8) | (i >> 24))
  55.  
  56. /******************************************************************
  57. * structs
  58. ******************************************************************/
  59.  
  60. /******************************************************************
  61. * PDB struct that holds information about the PDB file we're converting
  62. ******************************************************************/
  63. typedef struct
  64.   {
  65.   char name[32 + 1];
  66.   short attributes;
  67.   short version;
  68.   int creationDate;
  69.   int modificationDate;
  70.   int lastBackupDate;
  71.   int modificationNumber;
  72.   int appInfoID;
  73.   int sortInfoID;
  74.   char type[4];
  75.   char creator[4];
  76.   int uniqueIDSeed;
  77.   }
  78. PDBHeaderType;
  79.  
  80. /******************************************************************
  81. * PDB struct that holds information about and individual PDB record
  82. ******************************************************************/
  83. typedef struct
  84.   {
  85.   unsigned int localChunkID;
  86.   char attributes;
  87.   char uniqueID[3];
  88.   }
  89. PDBRecordType;
  90.  
  91. /* Structures from the Timesheet Palmpilot application */
  92.  
  93. /******************************************************************
  94. * Struct used to hold (and store) application internal data over
  95. * multiple app executions.
  96. ******************************************************************/
  97. /* Timesheet Version 1.2.0. */
  98. typedef struct
  99.   {
  100.   /* The number of entries in each category (clients/projects/tasks). */
  101.   char numCatEntries[3];
  102.   }
  103. TSAppPrefType_v120;
  104.  
  105. /* Timesheet Version 1.4.0. */
  106. typedef struct
  107.   {
  108.   /* The number of entries in each category (clients/projects/tasks). */
  109.   char numCatEntries[3];
  110.   char newCatEntryIdx[3];    /* saved default category indexes for new entries (for Auto Categories pref) */
  111.   char prefFlags;        /* pref flags */
  112.   char newCatHours;        /* saved default category hours/minutes for new entries (for Auto Duration pref) */
  113.   char filler[2];        /* additional future pref space */
  114.   }
  115. TSAppPrefType_v140;
  116.  
  117. /******************************************************************
  118. * Struct used to hold Client, Project and Task category lists.
  119. ******************************************************************/
  120. typedef struct
  121.   {
  122.   /* The translation table for the entries */
  123.   char transTable[TSMaxCatEntries_v140];
  124.   /* The text of the actual entries, will always contain at least two automatic entries, 'none' and 'Edit...' */
  125.   char catsStartHere;
  126.   }
  127. TSCatRecType_v140;
  128.  
  129. typedef struct
  130.   {
  131.   /* The translation table for the entries */
  132.   char transTable[TSMaxCatEntries_v150];
  133.   /* The text of the actual entries, will always contain at least two automatic entries, 'none' and 'Edit...' */
  134.   char catsStartHere;
  135.   }
  136. TSCatRecType_v150;
  137.  
  138. /******************************************************************
  139. * Struct used to hold (and store) day records.
  140. ******************************************************************/
  141. typedef struct
  142.   {
  143.   /* Number of entries in this day */
  144.   char numEntries;
  145.   /* WARNING: Intel specific! (kinda) */
  146.   /* Date this day record relates to, in the PalmPilot this is actually a DateType struct, but because
  147.   * I'm not entirely sure how structs and byte order relate so I convert dates the hard way. */
  148.   unsigned char date[3];
  149.   }
  150. TSDayRecType;
  151.  
  152. /******************************************************************
  153. * Struct used to hold (and store) entry records.
  154. ******************************************************************/
  155. /* Timesheet Version 1.2.0. */
  156. typedef struct
  157.   {
  158.   /* Which client does this record relate to. */
  159.   char clientIdx;
  160.   /* Which project does this record relate to. */
  161.   char projectIdx;
  162.   /* Which task does this record relate to. */
  163.   char taskIdx;
  164.   /* How many hours does this record use. */
  165.   char hours;
  166.   /* Following the end of the structure is the first character of descriptive text for to this record. */
  167.   }
  168. TSEntryRecType_v120;
  169.  
  170. /* Timesheet Version 1.4.0. */
  171. typedef struct
  172.   {
  173.   /* Which client does this record relate to. */
  174.   char clientIdx;
  175.   /* Which project does this record relate to. */
  176.   char projectIdx;
  177.   /* Which task does this record relate to. */
  178.   char taskIdx;
  179.   /* How many hours does this record use. */
  180.   char hours;
  181.   /* Entry number of this entry within the current day (i.e. first entry in day = 1). Used during binary searching
  182.   * of Timesheet database. Must be updated each time a record is DELETED from day. */
  183.   char entryNum;
  184.   /* Filler for future possible expansion */
  185.   char filler;
  186.   /* Note there's no 'chargeable' variable. This because 'chargeable' is indicated by setting the highest bit in the
  187.   * record category (part of attributes) on. */
  188.   /* Following the end of the structure is the first character of descriptive text for to this record. */
  189.   }
  190. TSEntryRecType_v140;
  191.  
  192. /******************************************************************
  193. * function prototypes
  194. ******************************************************************/
  195. PDBHeaderType *loadDBHeader(FILE * dbFile);
  196. short loadDBRecords(int dbMinorVersion, int dbMajorVersion, PDBHeaderType * header, FILE * dbFile, PDBRecordType ** records);
  197. int dbHasFiller(FILE * dbFile, int numRecords, PDBRecordType * records);
  198. void *loadDBRecord(FILE * dbFile, int recNum, PDBRecordType * records, int numRecords);
  199. void timesheet2Csv(FILE * dbFile, FILE * csvFile);
  200. char *lookupCatName(void *catPtr, int catIdx, int dbMajorVersion, int dbMinorVersion);
  201. void fixupEntryComment(char *entryComment);
  202. int main(int argc, char **argv);
  203.  
  204. /******************************************************************
  205. * Loads the header information for a PalmPilot PDB file and returns
  206. * a new PDBHeaderType structure containing the header info if it
  207. * was successfully loaded. Returns NULL if the PDB file doesn't contain
  208. * a valid header (which means it's probably not a PDB file).
  209. ******************************************************************/
  210. PDBHeaderType *
  211. loadDBHeader(FILE * dbFile)
  212.   {
  213.   PDBHeaderType *newHeaderPtr = NULL;
  214.   
  215.   /* dynamically allocate a new header */
  216.   if ((newHeaderPtr = malloc(sizeof(PDBHeaderType))) == NULL)
  217.     {
  218.     /* Out of memory */
  219.     perror("error reading database header");
  220.     exit(EXIT_FAILURE);
  221.     }
  222.   /* attempt to retrieve DB header from file */
  223.   if (fread(newHeaderPtr->name, sizeof(char), 32, dbFile) == 0)
  224.     {
  225.     return NULL;
  226.     }
  227.   if (fread(&(newHeaderPtr->attributes), sizeof(short), 1, dbFile) == 0)
  228.     {
  229.     return NULL;
  230.     }
  231.   /* WARNING: Intel specific! */
  232.   newHeaderPtr->attributes = MAC2PC_SHORT(newHeaderPtr->attributes);
  233.   if (fread(&(newHeaderPtr->version), sizeof(short), 1, dbFile) == 0)
  234.     {
  235.     return NULL;
  236.     }
  237.   /* WARNING: Intel specific! */
  238.   newHeaderPtr->version = MAC2PC_SHORT(newHeaderPtr->version);
  239.   if (fread(&(newHeaderPtr->creationDate), sizeof(int), 1, dbFile) == 0)
  240.     {
  241.     return NULL;
  242.     }
  243.   if (fread(&(newHeaderPtr->modificationDate), sizeof(int), 1, dbFile) == 0)
  244.     {
  245.     return NULL;
  246.     }
  247.   if (fread(&(newHeaderPtr->lastBackupDate), sizeof(int), 1, dbFile) == 0)
  248.     {
  249.     return NULL;
  250.     }
  251.   if (fread(&(newHeaderPtr->modificationNumber), sizeof(int), 1, dbFile) == 0)
  252.     {
  253.     return NULL;
  254.     }
  255.   if (fread(&(newHeaderPtr->appInfoID), sizeof(int), 1, dbFile) == 0)
  256.     {
  257.     return NULL;
  258.     }
  259.   if (fread(&(newHeaderPtr->sortInfoID), sizeof(int), 1, dbFile) == 0)
  260.     {
  261.     return NULL;
  262.     }
  263.   if (fread(&(newHeaderPtr->type), sizeof(char), 4, dbFile) == 0)
  264.     {
  265.     return NULL;
  266.     }
  267.   if (fread(&(newHeaderPtr->creator), sizeof(char), 4, dbFile) == 0)
  268.     {
  269.     return NULL;
  270.     }
  271.   if (fread(&(newHeaderPtr->uniqueIDSeed), sizeof(int), 1, dbFile) == 0)
  272.     {
  273.     return NULL;
  274.     }
  275.   /* read complete header */
  276.   return newHeaderPtr;
  277.   }
  278.  
  279.   /******************************************************************
  280.   * Load the record information for every record in the database file.
  281.   * Returns the number of records that exist in the database, or -1
  282.   * if some error has occurred. May exit the utility if out of memory.
  283. ******************************************************************/
  284. short
  285. loadDBRecords(int dbMinorVersion, int dbMajorVersion, PDBHeaderType * header, FILE * dbFile, PDBRecordType ** records)
  286.   {
  287.   int i;
  288.   int nextRecordListID = 0;
  289.   int numRecords = 0;
  290.   
  291.   if (fread(&nextRecordListID, sizeof(int), 1, dbFile) == 0)
  292.     {
  293.     return -1;
  294.     }
  295.   if (fread(&numRecords, sizeof(short), 1, dbFile) == 0)
  296.     {
  297.     return -1;
  298.     }
  299.   numRecords = MAC2PC_SHORT(numRecords);
  300.   /* dynamically allocate the record array */
  301.   if (((*records) = malloc(sizeof(PDBRecordType) * numRecords)) == NULL)
  302.     {
  303.     /* Out of memory */
  304.     perror("error reading records");
  305.     exit(EXIT_FAILURE);
  306.     }
  307.   /* load all day and entry records */
  308.   for (i = 0; i < numRecords; i++)
  309.     {
  310.     if (fread(&((*records)[i].localChunkID), sizeof(unsigned int), 1, dbFile) == 0)
  311.       {
  312.       return -1;
  313.       }
  314.     /* WARNING: Intel specific! */
  315.     (*records)[i].localChunkID = MAC2PC_INT((*records)[i].localChunkID);
  316.     if (fread(&((*records)[i].attributes), sizeof(char), 1, dbFile) == 0)
  317.       {
  318.       return -1;
  319.       }
  320.     if (fread(&((*records)[i].uniqueID), sizeof(char), 3, dbFile) == 0)
  321.       {
  322.       return -1;
  323.       }
  324.     }
  325.  
  326.   return numRecords;
  327.   }
  328.  
  329.   
  330.   /******************************************************************
  331.   * Load the actual data for a specific record in the database. 
  332.   * Returns a pointer to data loaded for the record, or NULL if the record wasn't loaded for some reason.
  333. ******************************************************************/
  334. void *
  335. loadDBRecord(FILE * dbFile, int recNum, PDBRecordType * records, int numRecords)
  336.   {
  337.   int recLength = 0;
  338.   long fileOffset = 0;
  339.   void *recPtr;
  340.   
  341.   /* sanity check */
  342.   if (recNum < 0 || recNum >= numRecords)
  343.     {
  344.     return NULL;
  345.     }
  346.   /* calculate the length of the record */
  347.   if (recNum == numRecords - 1)
  348.     {
  349.     /* at the last record in the database file, have to calculate final record length based on
  350.     * what's left in the file to read, this is a little ugly and may not be portable. */
  351.     fseek(dbFile, 0, SEEK_END);
  352.     recLength = ftell(dbFile) - records[recNum].localChunkID;
  353.     fseek(dbFile, fileOffset, SEEK_SET);
  354.     }
  355.   else
  356.     {
  357.     /* at intermediate record, calculate length based on offset of NEXT record */
  358.     recLength = records[recNum + 1].localChunkID - records[recNum].localChunkID;
  359.     }
  360.   /* dynamically allocate memory to contain the record */
  361.   if ((recPtr = malloc(recLength)) == NULL)
  362.     {
  363.     /* ack! out of memory? */
  364.     perror("error reading record");
  365.     exit(EXIT_FAILURE);
  366.     }
  367.   /* retrieve record contents */
  368.   fseek(dbFile, (long)records[recNum].localChunkID, SEEK_SET);
  369.   if (fread(recPtr, 1, recLength, dbFile) == 0)
  370.     {
  371.     return NULL;
  372.     }
  373.   return recPtr;
  374.   }
  375.  
  376.   /******************************************************************
  377.   * Read the Timesheet database from dbFile and convert it to CSV file in csvFile.
  378.   * This function may exit the program if a serious problem occurs (truncated files,
  379.   * out of memory etc).
  380.   * NOTE: This is a rather large function that should be split into parts...my apologies.
  381. ******************************************************************/
  382. void
  383. timesheet2csv(FILE * dbFile, FILE * csvFile)
  384.   {
  385.   int i, j;
  386.   int dbMinorVersion = 0;
  387.   int dbMajorVersion = 0;
  388.   short numRecords = 0;
  389.   short year = 0;
  390.   short month = 0;
  391.   short day = 0;
  392.   short chargeable = 0;
  393.   short hours = 0;
  394.   short minutes = 0;
  395.   char ctimeStr[26 + 1] = "\x0";
  396.   PDBHeaderType *headerPtr = NULL;
  397.   PDBRecordType *records = NULL;
  398.   TSAppPrefType_v120 *tsAppPrefs = NULL;
  399.   void *tsClients = NULL;
  400.   void *tsProjects = NULL;
  401.   void *tsTasks = NULL;
  402.   TSDayRecType *tsDay = NULL;
  403.   TSEntryRecType_v120 *tsEntry_v120 = NULL;
  404.   char *entryComment = NULL;
  405.   time_t localTime;
  406.   const char monthNames[12][3 + 1] =
  407.     {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
  408.   
  409.   /* read the DB header */
  410.   if ((headerPtr = loadDBHeader(dbFile)) == NULL)
  411.     {
  412.     fprintf(stderr, "error: input file does not appear to be a PalmPilot PDB database.\n");
  413.     exit(EXIT_FAILURE);
  414.     }
  415.   /* print some database information for the user */
  416.   fprintf(stderr, "Palmpilot PDB database file:\n"
  417.     "\tname   : %s\n\ttype   : %s\n\tcreator: %s\n",
  418.     headerPtr->name, headerPtr->type, headerPtr->creator);
  419.   /* confirm the database is a Timesheet database this utility can convert */
  420.   if (strcmp(headerPtr->name, TSDBName) != 0)
  421.     {
  422.     fprintf(stderr, "\nerror: this PalmPilot PDB file is not a Timesheet database.\n");
  423.     exit(EXIT_FAILURE);
  424.     }
  425.   /* check the Timesheet database version against the utility version */
  426.   dbMajorVersion = (headerPtr->version) >> 8;
  427.   dbMinorVersion = (headerPtr->version) & 0x00FF;
  428.   fprintf(stderr, "\tversion: %d.%d\n", dbMajorVersion, dbMinorVersion);
  429.   if ((dbMajorVersion > TSDBMajorVersion) ||
  430.     (dbMajorVersion == TSDBMajorVersion && dbMinorVersion > TSDBMinorVersion))
  431.     {
  432.     fprintf(stderr, "\nerror: incompatible Timesheet database version.\n"
  433.       "This version of Timesheet2CSV translates databases version %d.%d and earlier.\n",
  434.       TSDBMajorVersion, TSDBMinorVersion);
  435.     exit(EXIT_FAILURE);
  436.     }
  437.   /* looks good, so load all the record information */
  438.   numRecords = loadDBRecords(dbMinorVersion, dbMajorVersion, headerPtr, dbFile, &records);
  439.   if (numRecords == -1)
  440.     {
  441.     perror("error reading records");
  442.     exit(EXIT_FAILURE);
  443.     }
  444.   fprintf(stderr, "\t%d records in database.\n", numRecords);
  445.   /* check if the database is empty or truncated */
  446.   if (numRecords < TSFirstDayRecIdx)
  447.     {
  448.     fprintf(stderr, "error: Timesheet database appears to be truncated (not enough records).\n");
  449.     exit(EXIT_FAILURE);
  450.     }
  451.   if (numRecords == TSFirstDayRecIdx)
  452.     {
  453.     fprintf(stderr, "error: Timesheet database contains no entry records to convert.\n");
  454.     exit(EXIT_FAILURE);
  455.     }
  456.   /* put a header on the CSV file */
  457.   time(&localTime);
  458.   strcpy(ctimeStr, ctime(&localTime));
  459.   ctimeStr[strlen(ctimeStr) - 1] = '\x0';
  460.   fprintf(csvFile, "\"Date\",\"Year\",\"Month\",\"Day\",\"Client\",\"Project\",\"Task\",\""
  461.     "Hours\",\"Minutes\",\"Duration\",\"Chargeable\",\"Comment\"\n");
  462.   /* begin loading the actual Timesheet database records */
  463.   /* first 4 records in the database are the Timesheet application preference records */
  464.   tsAppPrefs = loadDBRecord(dbFile, 0, records, numRecords);
  465.   if (tsAppPrefs == NULL)
  466.     {
  467.     fprintf(stderr, "error: unable to read record 0.\n");
  468.     exit(EXIT_FAILURE);
  469.     }
  470.   tsClients = loadDBRecord(dbFile, 1, records, numRecords);
  471.   if (tsClients == NULL)
  472.     {
  473.     fprintf(stderr, "error: unable to read record 1.\n");
  474.     exit(EXIT_FAILURE);
  475.     }
  476.   tsProjects = loadDBRecord(dbFile, 2, records, numRecords);
  477.   if (tsProjects == NULL)
  478.     {
  479.     fprintf(stderr, "error: unable to read record 2.\n");
  480.     exit(EXIT_FAILURE);
  481.     }
  482.   tsTasks = loadDBRecord(dbFile, 3, records, numRecords);
  483.   if (tsTasks == NULL)
  484.     {
  485.     fprintf(stderr, "error: unable to read record 3.\n");
  486.     exit(EXIT_FAILURE);
  487.     }
  488.   /* then the actual Timesheet day/entry records begin */
  489.   for (i = TSFirstDayRecIdx; i < numRecords; i++)
  490.     {
  491.     /* get the day record */
  492.     tsDay = loadDBRecord(dbFile, i, records, numRecords);
  493.     if (tsDay == NULL)
  494.       {
  495.       fprintf(stderr, "error: unable to read record %d.\n", i);
  496.       exit(EXIT_FAILURE);
  497.       }
  498.     /* WARNING: INTEL SPECIFIC CODE */
  499.     /* build the date the hard way because I can't figure out how compiler works it's structs... */
  500.     year = ((tsDay->date[1] >> 1) & 0x7F) + 1920;
  501.     month = ((tsDay->date[1] & 0x01) << 3) | ((tsDay->date[2] >> 5) & 0x07);
  502.     day = tsDay->date[2] & 0x1F;
  503.     /* process each entry in the day */
  504.     for (j = 0; j < tsDay->numEntries; j++, i++)
  505.       {
  506.       /* retrieve the entry record in v120 format or earlier */
  507.       tsEntry_v120 = loadDBRecord(dbFile, i + 1, records, numRecords);
  508.       if (tsEntry_v120 == NULL)
  509.     {
  510.     fprintf(stderr, "error: unable to read record %d.\n", i + 1);
  511.     exit(EXIT_FAILURE);
  512.     }
  513.       /* extract the chargeable flag from the high bit of the low attribute byte */
  514.       chargeable = records[i + 1].attributes & 0x08;
  515.       /* extract hours and minutes */
  516.       hours = tsEntry_v120->hours & 0x0F;
  517.       minutes = ((tsEntry_v120->hours >> 4) & 0x0F) * 5;
  518.       /* produce another CSV row in the output file for this entry */
  519.       /* This is an ugly bit of coding. Particularly the way I've treated the tsClient, tsProject and tsTask record pointers.
  520.       * I've just assumed they're version 1.5 or higher. This assumption works for earlier versions because there should be no
  521.       * client/project/task index greater than 22. Told you this utility was a hack ;) */
  522.       fprintf(csvFile, "%d-%s-%d,%d,%d,%d,\"%s\",\"%s\",\"%s\",%d,%d,\"%d:%d\",\"%c\",",
  523.     day, monthNames[month - 1], year,
  524.     year, month, day,
  525.     lookupCatName(tsClients, ((TSCatRecType_v150 *) tsClients)->transTable[tsEntry_v120->clientIdx], dbMajorVersion, dbMinorVersion),
  526.     lookupCatName(tsProjects, ((TSCatRecType_v150 *) tsProjects)->transTable[tsEntry_v120->projectIdx], dbMajorVersion, dbMinorVersion),
  527.     lookupCatName(tsTasks, ((TSCatRecType_v150 *) tsTasks)->transTable[tsEntry_v120->taskIdx], dbMajorVersion, dbMinorVersion),
  528.     hours, minutes, hours, minutes,
  529.     chargeable ? ('Y') : ('N'));
  530.     /* now for a bit of dodgy coding. The rest of the entry record (after the structure ends)
  531.       * contains the text of the entry description, so do some nasty pointer arithmetic */
  532.       if (dbMajorVersion == 1 && dbMinorVersion <= 2)
  533.     {
  534.       /* dealing with v120 or earlier entry record */
  535.       entryComment = (char *) tsEntry_v120 + sizeof(TSEntryRecType_v120);
  536.     }
  537.       else
  538.     {
  539.       /* dealing with v140 or later entry record */
  540.       entryComment = (char *) tsEntry_v120 + sizeof(TSEntryRecType_v140);
  541.     }
  542.       /* fixup comment for csv by converting all embedded newlines into spaces */
  543.       fixupEntryComment(entryComment);
  544.       fprintf(csvFile, "\"%s\"\n", entryComment);
  545.       /* free the entry record now we've finished with it */
  546.       free(tsEntry_v120);
  547.       tsEntry_v120 = NULL;
  548.       }
  549.     /* free the day record now we've finished with it */
  550.     free(tsDay);
  551.     tsDay = NULL;
  552.     }
  553.   /* put trailer on csv file */
  554.   fprintf(csvFile, "\n\"End of database.\"\n");
  555.   /* free all the memory we've allocated so far */
  556.   free(headerPtr);
  557.   headerPtr = NULL;
  558.   free(records);
  559.   records = NULL;
  560.   free(tsAppPrefs);
  561.   tsAppPrefs = NULL;
  562.   free(tsClients);
  563.   tsClients = NULL;
  564.   free(tsProjects);
  565.   tsClients = NULL;
  566.   free(tsTasks);
  567.   tsClients = NULL;
  568. }
  569.  
  570. /******************************************************************
  571. * Returns the category name for the specified category index. Required as the 
  572. * database category format changed somewhat from database v1.0, v1.1 -> v1.2
  573. * and again from v1.4 -> v1.5.
  574. ******************************************************************/
  575. char *
  576. lookupCatName(void *catPtr, int catIdx, int dbMajorVersion, int dbMinorVersion)
  577.   {
  578.   char *catNames;
  579.   
  580.   if (dbMajorVersion == 1 && dbMinorVersion < 5)
  581.     {
  582.     /* version 1.4 and earlier category format (22 category names max) */
  583.     catNames = &(((TSCatRecType_v140 *) catPtr)->catsStartHere);
  584.     }
  585.   else
  586.     {
  587.     /* version 1.5 and later category format (102 category names max) */
  588.     catNames = &(((TSCatRecType_v150 *) catPtr)->catsStartHere);
  589.     }
  590.   if (dbMajorVersion > 1 || dbMinorVersion > 1)
  591.     {
  592.     /* new v1.2 category format */
  593.     return &(catNames[sizeof(char) * catIdx * TSMaxCatEntryLen_v120]);
  594.     }
  595.   else
  596.     {
  597.     /* old v1.0 category format */
  598.     return &(catNames[sizeof(char) * catIdx * TSMaxCatEntryLen_v110]);
  599.     }
  600.   }
  601.  
  602.   /******************************************************************
  603.   * Fixup comment for csv by converting all embedded newlines into spaces
  604.   ******************************************************************/
  605. void fixupEntryComment(char *entryComment)
  606.   {
  607.     while((*entryComment) != '\x0')
  608.       {
  609.     if ((*entryComment) == '\n')
  610.       {
  611.         (*entryComment) = ' ';
  612.       }
  613.     entryComment ++;
  614.       }
  615.   }
  616.  
  617.   /******************************************************************
  618.   * Timesheet2CSV main function, just checks the command line is okay,
  619.   * opens the input and output files and starts the translation.
  620. ******************************************************************/
  621. int
  622. main(int argc, char **argv)
  623.   {
  624.   FILE *dbFile = NULL;
  625.   FILE *csvFile = NULL;
  626.   
  627.   fprintf(stderr, "Timesheet2CSV utility, version %d.%d.\n"
  628.     "Converts a saved PalmPilot Timesheet database into a CSV file.\n"
  629.     "(c) Copyright 1998, Stuart Nicholson.\n\n", TSDBMajorVersion, TSDBMinorVersion);
  630.   /* check the command line contains input, output file names */
  631.   if (argc != 3)
  632.     {
  633.     fprintf(stderr, "error: bad command line.\nusage: Timesheet2CSV input_dbFile output_csvFile\n");
  634.     exit(EXIT_FAILURE);
  635.     }
  636.   /* open the input file */
  637.   if ((dbFile = fopen(argv[1], "rb")) == NULL)
  638.     {
  639.     perror("error opening input database file");
  640.     exit(EXIT_FAILURE);
  641.     }
  642.   /* open the output file */
  643.   if ((csvFile = fopen(argv[2], "wt")) == NULL)
  644.     {
  645.     perror("error opening output csv file");
  646.     exit(EXIT_FAILURE);
  647.     }
  648.   /* translate the palmpilot database file into a csv file on stdout */
  649.   timesheet2csv(dbFile, csvFile);
  650.   /* close the input and output file */
  651.   fclose(dbFile);
  652.   dbFile = NULL;
  653.   fclose(csvFile);
  654.   csvFile = NULL;
  655.   /* successfully converted Timesheet database */
  656.   fprintf(stderr, "Finished.\n");
  657.   return EXIT_SUCCESS;
  658.   }
  659.  
  660. /* End of file */
  661.