home *** CD-ROM | disk | FTP | other *** search
- /******************************************************************
- * Timesheet2CSV.c
- * (c) Copyright Stuart Nicholson, 1998-1999.
- * Utility for converting stored Palmpilot Timesheet database (*.PDB) file into
- * a more useful CSV (comma separated values) format which can be loaded into
- * most common office suites.
- *
- * WARNING: This code is kludgy and Intel x86 SPECIFIC as it makes some extremely
- * horrible assumptions about byte ordering and such. I would most grateful if
- * a Macintosh programmer out there could massage this code and release
- * a Mac friendly version. The problematic sections start with a comment
- * beginning 'WARNING:'.
- *
- * author : stuart nicholson
- * email : snic@ihug.co.nz
- * version: 15-November-1998
- ******************************************************************/
-
- /******************************************************************
- * #includes
- ******************************************************************/
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <time.h>
-
- /******************************************************************
- * #defines
- ******************************************************************/
- /* The application database type */
- #define TSDBType ('data')
- /* The application database name */
- #define TSDBName ("TimesheetDB")
- /* The version number of the database (more or less corresponds to the application version number). */
- #define TSDBMajorVersion (1)
- #define TSDBMinorVersion (5)
- #define TSDBVersion ((TSDBMajorVersion << 8) | (TSDBMinorVersion))
-
- /* The maximum number of client, projects or task entries allowed,
- * including the the 2 automatic entries of 'none' and 'Edit...' */
- #define TSMaxCatEntries_v140 (22)
- #define TSMaxCatEntries_v150 (102)
- /* The length of each client, project or task list entry. */
- #define TSMaxCatEntryLen_v110 (15 + 1)
- #define TSMaxCatEntryLen_v120 (17 + 1)
-
- /* The database record index of the first actual day record in the timesheet database.
- * All records prior to this record hold application resource information. */
- #define TSFirstDayRecIdx (4)
-
- /* WARNING: Intel specific! */
- /* convert Motorola shorts and ints to Intel shorts and ints */
- #define MAC2PC_SHORT(s) (((s & 0x00FF) << 8) | (s >> 8))
- #define MAC2PC_INT(i) (((i & 0x000000FF) << 24) | ((i & 0x0000FF00) << 8) | ((i & 0x00FF0000) >> 8) | (i >> 24))
-
- /******************************************************************
- * structs
- ******************************************************************/
-
- /******************************************************************
- * PDB struct that holds information about the PDB file we're converting
- ******************************************************************/
- typedef struct
- {
- char name[32 + 1];
- short attributes;
- short version;
- int creationDate;
- int modificationDate;
- int lastBackupDate;
- int modificationNumber;
- int appInfoID;
- int sortInfoID;
- char type[4];
- char creator[4];
- int uniqueIDSeed;
- }
- PDBHeaderType;
-
- /******************************************************************
- * PDB struct that holds information about and individual PDB record
- ******************************************************************/
- typedef struct
- {
- unsigned int localChunkID;
- char attributes;
- char uniqueID[3];
- }
- PDBRecordType;
-
- /* Structures from the Timesheet Palmpilot application */
-
- /******************************************************************
- * Struct used to hold (and store) application internal data over
- * multiple app executions.
- ******************************************************************/
- /* Timesheet Version 1.2.0. */
- typedef struct
- {
- /* The number of entries in each category (clients/projects/tasks). */
- char numCatEntries[3];
- }
- TSAppPrefType_v120;
-
- /* Timesheet Version 1.4.0. */
- typedef struct
- {
- /* The number of entries in each category (clients/projects/tasks). */
- char numCatEntries[3];
- char newCatEntryIdx[3]; /* saved default category indexes for new entries (for Auto Categories pref) */
- char prefFlags; /* pref flags */
- char newCatHours; /* saved default category hours/minutes for new entries (for Auto Duration pref) */
- char filler[2]; /* additional future pref space */
- }
- TSAppPrefType_v140;
-
- /******************************************************************
- * Struct used to hold Client, Project and Task category lists.
- ******************************************************************/
- typedef struct
- {
- /* The translation table for the entries */
- char transTable[TSMaxCatEntries_v140];
- /* The text of the actual entries, will always contain at least two automatic entries, 'none' and 'Edit...' */
- char catsStartHere;
- }
- TSCatRecType_v140;
-
- typedef struct
- {
- /* The translation table for the entries */
- char transTable[TSMaxCatEntries_v150];
- /* The text of the actual entries, will always contain at least two automatic entries, 'none' and 'Edit...' */
- char catsStartHere;
- }
- TSCatRecType_v150;
-
- /******************************************************************
- * Struct used to hold (and store) day records.
- ******************************************************************/
- typedef struct
- {
- /* Number of entries in this day */
- char numEntries;
- /* WARNING: Intel specific! (kinda) */
- /* Date this day record relates to, in the PalmPilot this is actually a DateType struct, but because
- * I'm not entirely sure how structs and byte order relate so I convert dates the hard way. */
- unsigned char date[3];
- }
- TSDayRecType;
-
- /******************************************************************
- * Struct used to hold (and store) entry records.
- ******************************************************************/
- /* Timesheet Version 1.2.0. */
- typedef struct
- {
- /* Which client does this record relate to. */
- char clientIdx;
- /* Which project does this record relate to. */
- char projectIdx;
- /* Which task does this record relate to. */
- char taskIdx;
- /* How many hours does this record use. */
- char hours;
- /* Following the end of the structure is the first character of descriptive text for to this record. */
- }
- TSEntryRecType_v120;
-
- /* Timesheet Version 1.4.0. */
- typedef struct
- {
- /* Which client does this record relate to. */
- char clientIdx;
- /* Which project does this record relate to. */
- char projectIdx;
- /* Which task does this record relate to. */
- char taskIdx;
- /* How many hours does this record use. */
- char hours;
- /* Entry number of this entry within the current day (i.e. first entry in day = 1). Used during binary searching
- * of Timesheet database. Must be updated each time a record is DELETED from day. */
- char entryNum;
- /* Filler for future possible expansion */
- char filler;
- /* Note there's no 'chargeable' variable. This because 'chargeable' is indicated by setting the highest bit in the
- * record category (part of attributes) on. */
- /* Following the end of the structure is the first character of descriptive text for to this record. */
- }
- TSEntryRecType_v140;
-
- /******************************************************************
- * function prototypes
- ******************************************************************/
- PDBHeaderType *loadDBHeader(FILE * dbFile);
- short loadDBRecords(int dbMinorVersion, int dbMajorVersion, PDBHeaderType * header, FILE * dbFile, PDBRecordType ** records);
- int dbHasFiller(FILE * dbFile, int numRecords, PDBRecordType * records);
- void *loadDBRecord(FILE * dbFile, int recNum, PDBRecordType * records, int numRecords);
- void timesheet2Csv(FILE * dbFile, FILE * csvFile);
- char *lookupCatName(void *catPtr, int catIdx, int dbMajorVersion, int dbMinorVersion);
- void fixupEntryComment(char *entryComment);
- int main(int argc, char **argv);
-
- /******************************************************************
- * Loads the header information for a PalmPilot PDB file and returns
- * a new PDBHeaderType structure containing the header info if it
- * was successfully loaded. Returns NULL if the PDB file doesn't contain
- * a valid header (which means it's probably not a PDB file).
- ******************************************************************/
- PDBHeaderType *
- loadDBHeader(FILE * dbFile)
- {
- PDBHeaderType *newHeaderPtr = NULL;
-
- /* dynamically allocate a new header */
- if ((newHeaderPtr = malloc(sizeof(PDBHeaderType))) == NULL)
- {
- /* Out of memory */
- perror("error reading database header");
- exit(EXIT_FAILURE);
- }
- /* attempt to retrieve DB header from file */
- if (fread(newHeaderPtr->name, sizeof(char), 32, dbFile) == 0)
- {
- return NULL;
- }
- if (fread(&(newHeaderPtr->attributes), sizeof(short), 1, dbFile) == 0)
- {
- return NULL;
- }
- /* WARNING: Intel specific! */
- newHeaderPtr->attributes = MAC2PC_SHORT(newHeaderPtr->attributes);
- if (fread(&(newHeaderPtr->version), sizeof(short), 1, dbFile) == 0)
- {
- return NULL;
- }
- /* WARNING: Intel specific! */
- newHeaderPtr->version = MAC2PC_SHORT(newHeaderPtr->version);
- if (fread(&(newHeaderPtr->creationDate), sizeof(int), 1, dbFile) == 0)
- {
- return NULL;
- }
- if (fread(&(newHeaderPtr->modificationDate), sizeof(int), 1, dbFile) == 0)
- {
- return NULL;
- }
- if (fread(&(newHeaderPtr->lastBackupDate), sizeof(int), 1, dbFile) == 0)
- {
- return NULL;
- }
- if (fread(&(newHeaderPtr->modificationNumber), sizeof(int), 1, dbFile) == 0)
- {
- return NULL;
- }
- if (fread(&(newHeaderPtr->appInfoID), sizeof(int), 1, dbFile) == 0)
- {
- return NULL;
- }
- if (fread(&(newHeaderPtr->sortInfoID), sizeof(int), 1, dbFile) == 0)
- {
- return NULL;
- }
- if (fread(&(newHeaderPtr->type), sizeof(char), 4, dbFile) == 0)
- {
- return NULL;
- }
- if (fread(&(newHeaderPtr->creator), sizeof(char), 4, dbFile) == 0)
- {
- return NULL;
- }
- if (fread(&(newHeaderPtr->uniqueIDSeed), sizeof(int), 1, dbFile) == 0)
- {
- return NULL;
- }
- /* read complete header */
- return newHeaderPtr;
- }
-
- /******************************************************************
- * Load the record information for every record in the database file.
- * Returns the number of records that exist in the database, or -1
- * if some error has occurred. May exit the utility if out of memory.
- ******************************************************************/
- short
- loadDBRecords(int dbMinorVersion, int dbMajorVersion, PDBHeaderType * header, FILE * dbFile, PDBRecordType ** records)
- {
- int i;
- int nextRecordListID = 0;
- int numRecords = 0;
-
- if (fread(&nextRecordListID, sizeof(int), 1, dbFile) == 0)
- {
- return -1;
- }
- if (fread(&numRecords, sizeof(short), 1, dbFile) == 0)
- {
- return -1;
- }
- numRecords = MAC2PC_SHORT(numRecords);
- /* dynamically allocate the record array */
- if (((*records) = malloc(sizeof(PDBRecordType) * numRecords)) == NULL)
- {
- /* Out of memory */
- perror("error reading records");
- exit(EXIT_FAILURE);
- }
- /* load all day and entry records */
- for (i = 0; i < numRecords; i++)
- {
- if (fread(&((*records)[i].localChunkID), sizeof(unsigned int), 1, dbFile) == 0)
- {
- return -1;
- }
- /* WARNING: Intel specific! */
- (*records)[i].localChunkID = MAC2PC_INT((*records)[i].localChunkID);
- if (fread(&((*records)[i].attributes), sizeof(char), 1, dbFile) == 0)
- {
- return -1;
- }
- if (fread(&((*records)[i].uniqueID), sizeof(char), 3, dbFile) == 0)
- {
- return -1;
- }
- }
-
- return numRecords;
- }
-
-
- /******************************************************************
- * Load the actual data for a specific record in the database.
- * Returns a pointer to data loaded for the record, or NULL if the record wasn't loaded for some reason.
- ******************************************************************/
- void *
- loadDBRecord(FILE * dbFile, int recNum, PDBRecordType * records, int numRecords)
- {
- int recLength = 0;
- long fileOffset = 0;
- void *recPtr;
-
- /* sanity check */
- if (recNum < 0 || recNum >= numRecords)
- {
- return NULL;
- }
- /* calculate the length of the record */
- if (recNum == numRecords - 1)
- {
- /* at the last record in the database file, have to calculate final record length based on
- * what's left in the file to read, this is a little ugly and may not be portable. */
- fseek(dbFile, 0, SEEK_END);
- recLength = ftell(dbFile) - records[recNum].localChunkID;
- fseek(dbFile, fileOffset, SEEK_SET);
- }
- else
- {
- /* at intermediate record, calculate length based on offset of NEXT record */
- recLength = records[recNum + 1].localChunkID - records[recNum].localChunkID;
- }
- /* dynamically allocate memory to contain the record */
- if ((recPtr = malloc(recLength)) == NULL)
- {
- /* ack! out of memory? */
- perror("error reading record");
- exit(EXIT_FAILURE);
- }
- /* retrieve record contents */
- fseek(dbFile, (long)records[recNum].localChunkID, SEEK_SET);
- if (fread(recPtr, 1, recLength, dbFile) == 0)
- {
- return NULL;
- }
- return recPtr;
- }
-
- /******************************************************************
- * Read the Timesheet database from dbFile and convert it to CSV file in csvFile.
- * This function may exit the program if a serious problem occurs (truncated files,
- * out of memory etc).
- * NOTE: This is a rather large function that should be split into parts...my apologies.
- ******************************************************************/
- void
- timesheet2csv(FILE * dbFile, FILE * csvFile)
- {
- int i, j;
- int dbMinorVersion = 0;
- int dbMajorVersion = 0;
- short numRecords = 0;
- short year = 0;
- short month = 0;
- short day = 0;
- short chargeable = 0;
- short hours = 0;
- short minutes = 0;
- char ctimeStr[26 + 1] = "\x0";
- PDBHeaderType *headerPtr = NULL;
- PDBRecordType *records = NULL;
- TSAppPrefType_v120 *tsAppPrefs = NULL;
- void *tsClients = NULL;
- void *tsProjects = NULL;
- void *tsTasks = NULL;
- TSDayRecType *tsDay = NULL;
- TSEntryRecType_v120 *tsEntry_v120 = NULL;
- char *entryComment = NULL;
- time_t localTime;
- const char monthNames[12][3 + 1] =
- {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
-
- /* read the DB header */
- if ((headerPtr = loadDBHeader(dbFile)) == NULL)
- {
- fprintf(stderr, "error: input file does not appear to be a PalmPilot PDB database.\n");
- exit(EXIT_FAILURE);
- }
- /* print some database information for the user */
- fprintf(stderr, "Palmpilot PDB database file:\n"
- "\tname : %s\n\ttype : %s\n\tcreator: %s\n",
- headerPtr->name, headerPtr->type, headerPtr->creator);
- /* confirm the database is a Timesheet database this utility can convert */
- if (strcmp(headerPtr->name, TSDBName) != 0)
- {
- fprintf(stderr, "\nerror: this PalmPilot PDB file is not a Timesheet database.\n");
- exit(EXIT_FAILURE);
- }
- /* check the Timesheet database version against the utility version */
- dbMajorVersion = (headerPtr->version) >> 8;
- dbMinorVersion = (headerPtr->version) & 0x00FF;
- fprintf(stderr, "\tversion: %d.%d\n", dbMajorVersion, dbMinorVersion);
- if ((dbMajorVersion > TSDBMajorVersion) ||
- (dbMajorVersion == TSDBMajorVersion && dbMinorVersion > TSDBMinorVersion))
- {
- fprintf(stderr, "\nerror: incompatible Timesheet database version.\n"
- "This version of Timesheet2CSV translates databases version %d.%d and earlier.\n",
- TSDBMajorVersion, TSDBMinorVersion);
- exit(EXIT_FAILURE);
- }
- /* looks good, so load all the record information */
- numRecords = loadDBRecords(dbMinorVersion, dbMajorVersion, headerPtr, dbFile, &records);
- if (numRecords == -1)
- {
- perror("error reading records");
- exit(EXIT_FAILURE);
- }
- fprintf(stderr, "\t%d records in database.\n", numRecords);
- /* check if the database is empty or truncated */
- if (numRecords < TSFirstDayRecIdx)
- {
- fprintf(stderr, "error: Timesheet database appears to be truncated (not enough records).\n");
- exit(EXIT_FAILURE);
- }
- if (numRecords == TSFirstDayRecIdx)
- {
- fprintf(stderr, "error: Timesheet database contains no entry records to convert.\n");
- exit(EXIT_FAILURE);
- }
- /* put a header on the CSV file */
- time(&localTime);
- strcpy(ctimeStr, ctime(&localTime));
- ctimeStr[strlen(ctimeStr) - 1] = '\x0';
- fprintf(csvFile, "\"Date\",\"Year\",\"Month\",\"Day\",\"Client\",\"Project\",\"Task\",\""
- "Hours\",\"Minutes\",\"Duration\",\"Chargeable\",\"Comment\"\n");
- /* begin loading the actual Timesheet database records */
- /* first 4 records in the database are the Timesheet application preference records */
- tsAppPrefs = loadDBRecord(dbFile, 0, records, numRecords);
- if (tsAppPrefs == NULL)
- {
- fprintf(stderr, "error: unable to read record 0.\n");
- exit(EXIT_FAILURE);
- }
- tsClients = loadDBRecord(dbFile, 1, records, numRecords);
- if (tsClients == NULL)
- {
- fprintf(stderr, "error: unable to read record 1.\n");
- exit(EXIT_FAILURE);
- }
- tsProjects = loadDBRecord(dbFile, 2, records, numRecords);
- if (tsProjects == NULL)
- {
- fprintf(stderr, "error: unable to read record 2.\n");
- exit(EXIT_FAILURE);
- }
- tsTasks = loadDBRecord(dbFile, 3, records, numRecords);
- if (tsTasks == NULL)
- {
- fprintf(stderr, "error: unable to read record 3.\n");
- exit(EXIT_FAILURE);
- }
- /* then the actual Timesheet day/entry records begin */
- for (i = TSFirstDayRecIdx; i < numRecords; i++)
- {
- /* get the day record */
- tsDay = loadDBRecord(dbFile, i, records, numRecords);
- if (tsDay == NULL)
- {
- fprintf(stderr, "error: unable to read record %d.\n", i);
- exit(EXIT_FAILURE);
- }
- /* WARNING: INTEL SPECIFIC CODE */
- /* build the date the hard way because I can't figure out how compiler works it's structs... */
- year = ((tsDay->date[1] >> 1) & 0x7F) + 1920;
- month = ((tsDay->date[1] & 0x01) << 3) | ((tsDay->date[2] >> 5) & 0x07);
- day = tsDay->date[2] & 0x1F;
- /* process each entry in the day */
- for (j = 0; j < tsDay->numEntries; j++, i++)
- {
- /* retrieve the entry record in v120 format or earlier */
- tsEntry_v120 = loadDBRecord(dbFile, i + 1, records, numRecords);
- if (tsEntry_v120 == NULL)
- {
- fprintf(stderr, "error: unable to read record %d.\n", i + 1);
- exit(EXIT_FAILURE);
- }
- /* extract the chargeable flag from the high bit of the low attribute byte */
- chargeable = records[i + 1].attributes & 0x08;
- /* extract hours and minutes */
- hours = tsEntry_v120->hours & 0x0F;
- minutes = ((tsEntry_v120->hours >> 4) & 0x0F) * 5;
- /* produce another CSV row in the output file for this entry */
- /* This is an ugly bit of coding. Particularly the way I've treated the tsClient, tsProject and tsTask record pointers.
- * I've just assumed they're version 1.5 or higher. This assumption works for earlier versions because there should be no
- * client/project/task index greater than 22. Told you this utility was a hack ;) */
- fprintf(csvFile, "%d-%s-%d,%d,%d,%d,\"%s\",\"%s\",\"%s\",%d,%d,\"%d:%d\",\"%c\",",
- day, monthNames[month - 1], year,
- year, month, day,
- lookupCatName(tsClients, ((TSCatRecType_v150 *) tsClients)->transTable[tsEntry_v120->clientIdx], dbMajorVersion, dbMinorVersion),
- lookupCatName(tsProjects, ((TSCatRecType_v150 *) tsProjects)->transTable[tsEntry_v120->projectIdx], dbMajorVersion, dbMinorVersion),
- lookupCatName(tsTasks, ((TSCatRecType_v150 *) tsTasks)->transTable[tsEntry_v120->taskIdx], dbMajorVersion, dbMinorVersion),
- hours, minutes, hours, minutes,
- chargeable ? ('Y') : ('N'));
- /* now for a bit of dodgy coding. The rest of the entry record (after the structure ends)
- * contains the text of the entry description, so do some nasty pointer arithmetic */
- if (dbMajorVersion == 1 && dbMinorVersion <= 2)
- {
- /* dealing with v120 or earlier entry record */
- entryComment = (char *) tsEntry_v120 + sizeof(TSEntryRecType_v120);
- }
- else
- {
- /* dealing with v140 or later entry record */
- entryComment = (char *) tsEntry_v120 + sizeof(TSEntryRecType_v140);
- }
- /* fixup comment for csv by converting all embedded newlines into spaces */
- fixupEntryComment(entryComment);
- fprintf(csvFile, "\"%s\"\n", entryComment);
- /* free the entry record now we've finished with it */
- free(tsEntry_v120);
- tsEntry_v120 = NULL;
- }
- /* free the day record now we've finished with it */
- free(tsDay);
- tsDay = NULL;
- }
- /* put trailer on csv file */
- fprintf(csvFile, "\n\"End of database.\"\n");
- /* free all the memory we've allocated so far */
- free(headerPtr);
- headerPtr = NULL;
- free(records);
- records = NULL;
- free(tsAppPrefs);
- tsAppPrefs = NULL;
- free(tsClients);
- tsClients = NULL;
- free(tsProjects);
- tsClients = NULL;
- free(tsTasks);
- tsClients = NULL;
- }
-
- /******************************************************************
- * Returns the category name for the specified category index. Required as the
- * database category format changed somewhat from database v1.0, v1.1 -> v1.2
- * and again from v1.4 -> v1.5.
- ******************************************************************/
- char *
- lookupCatName(void *catPtr, int catIdx, int dbMajorVersion, int dbMinorVersion)
- {
- char *catNames;
-
- if (dbMajorVersion == 1 && dbMinorVersion < 5)
- {
- /* version 1.4 and earlier category format (22 category names max) */
- catNames = &(((TSCatRecType_v140 *) catPtr)->catsStartHere);
- }
- else
- {
- /* version 1.5 and later category format (102 category names max) */
- catNames = &(((TSCatRecType_v150 *) catPtr)->catsStartHere);
- }
- if (dbMajorVersion > 1 || dbMinorVersion > 1)
- {
- /* new v1.2 category format */
- return &(catNames[sizeof(char) * catIdx * TSMaxCatEntryLen_v120]);
- }
- else
- {
- /* old v1.0 category format */
- return &(catNames[sizeof(char) * catIdx * TSMaxCatEntryLen_v110]);
- }
- }
-
- /******************************************************************
- * Fixup comment for csv by converting all embedded newlines into spaces
- ******************************************************************/
- void fixupEntryComment(char *entryComment)
- {
- while((*entryComment) != '\x0')
- {
- if ((*entryComment) == '\n')
- {
- (*entryComment) = ' ';
- }
- entryComment ++;
- }
- }
-
- /******************************************************************
- * Timesheet2CSV main function, just checks the command line is okay,
- * opens the input and output files and starts the translation.
- ******************************************************************/
- int
- main(int argc, char **argv)
- {
- FILE *dbFile = NULL;
- FILE *csvFile = NULL;
-
- fprintf(stderr, "Timesheet2CSV utility, version %d.%d.\n"
- "Converts a saved PalmPilot Timesheet database into a CSV file.\n"
- "(c) Copyright 1998, Stuart Nicholson.\n\n", TSDBMajorVersion, TSDBMinorVersion);
- /* check the command line contains input, output file names */
- if (argc != 3)
- {
- fprintf(stderr, "error: bad command line.\nusage: Timesheet2CSV input_dbFile output_csvFile\n");
- exit(EXIT_FAILURE);
- }
- /* open the input file */
- if ((dbFile = fopen(argv[1], "rb")) == NULL)
- {
- perror("error opening input database file");
- exit(EXIT_FAILURE);
- }
- /* open the output file */
- if ((csvFile = fopen(argv[2], "wt")) == NULL)
- {
- perror("error opening output csv file");
- exit(EXIT_FAILURE);
- }
- /* translate the palmpilot database file into a csv file on stdout */
- timesheet2csv(dbFile, csvFile);
- /* close the input and output file */
- fclose(dbFile);
- dbFile = NULL;
- fclose(csvFile);
- csvFile = NULL;
- /* successfully converted Timesheet database */
- fprintf(stderr, "Finished.\n");
- return EXIT_SUCCESS;
- }
-
- /* End of file */
-