/****************************************************************** * 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 #include #include #include /****************************************************************** * #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 */