/* *@@sourcefile tmsgfile.c: * replacement code for DosGetMessage for decent NLS support. * * This code has the following advantages over DosGetMessage: * * 1) No external utility is necessary to change message * files. Simply edit the text, and on the next call, * the file is recompiled at run-time. * * 2) To identify messages, any string can be used, not * only numerical IDs. * * The .TMF file must have the following format: * * 1) Any message must start on the beginning of a line * and look like this: * + <--MSGID-->: message text * * "MSGID" can be any string and will serve as the * message identifier for tmfGetMessage. * Leading spaces after "-->:" will be ignored. * * 2) The message text may span across several lines. * If so, the line breaks are returned as plain * newline (\n) characters by tmfGetMessage. * * 3) Comments in the file are supported if they start * with a semicolon (";") at the beginning of a line. * Any following text is ignored until the next * message ID. * * This file was originally added V0.9.0. The original * code was contributed by Christian Langanke, but this * has been completely rewritten with V0.9.16 to use my * fast string functions now. Also, tmfGetMessage now * requires tmfOpenMessageFile to be called beforehand * and keeps all messages in memory for speed. * * Usage: All OS/2 programs. * * Function prefixes: * -- tmf* text message file functions * * Note: Version numbering in this file relates to XWorkplace version * numbering. * *@@header "helpers\tmsgfile.h" *@@added V0.9.0 [umoeller] */ /* * Copyright (C) 2001-2002 Ulrich M”ller. * This file is part of the "XWorkplace helpers" source package. * This 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, in version 2 as it comes in the * "COPYING" file of the XWorkplace main distribution. * This program 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. */ #define OS2EMX_PLAIN_CHAR // this is needed for "os2emx.h"; if this is defined, // emx will define PSZ as _signed_ char, otherwise // as unsigned char #define INCL_DOSFILEMGR #define INCL_DOSMISC #define INCL_DOSERRORS #include #include #include #include #include #include "setup.h" // code generation and debugging options #include "helpers\datetime.h" #include "helpers\dosh.h" #include "helpers\eah.h" #include "helpers\standards.h" #include "helpers\stringh.h" #include "helpers\tree.h" #include "helpers\xstring.h" #include "helpers\tmsgfile.h" /* *@@category: Helpers\Control program helpers\Text message files (TMF) * see tmsgfile.c. */ /* ****************************************************************** * * Declarations * ********************************************************************/ /* *@@ MSGENTRY: * private representation of a message in a text * message file. Each tree entry in * TMFMSGFILE.IDsTreeRoot points to one of these * structures. * *@@added V0.9.16 (2001-10-08) [umoeller] */ typedef struct _MSGENTRY { TREE Tree; // ulKey points to strID.psz XSTRING strID; // message ID ULONG ulOfsText; // offset of start of message text in file ULONG cbText; // length of text in msg file } MSGENTRY, *PMSGENTRY; // globals STATIC PCSZ G_pcszStartMarker = "\n<--", G_pcszEndMarker = "-->:"; /* ****************************************************************** * * Functions * ********************************************************************/ /* *@@ LoadAndCompile: * loads and compiles the message file into the * given TMFMSGFILE struct. * * This has been extracted from tmfOpenMessageFile * to allow for recompiling on the fly if the * message file's last-write date/time changed. * * Preconditions: * * -- pFile->pszFilename must have been set. * * All other fields get initialized here. * *@@added V0.9.18 (2002-03-24) [umoeller] *@@changed V0.9.20 (2002-07-19) [umoeller]: optimized, no longer holding all msgs im mem *@@changed V1.0.1 (2002-12-16) [pr]: Ctrl-Z fix */ APIRET LoadAndCompile(PTMFMSGFILE pTmf, // in: TMF struct to set up PXFILE pFile) // in: opened XFILE for msg file { APIRET arc; PSZ pszContent = NULL; ULONG cbRead; if (!(arc = doshReadText(pFile, &pszContent, &cbRead))) // bytes read including null char { // file loaded: PCSZ pStartOfFile, pStartOfMarker; ULONG ulStartMarkerLength = strlen(G_pcszStartMarker), ulEndMarkerLength = strlen(G_pcszEndMarker); XSTRING strContents; // initialize TMFMSGFILE struct treeInit(&pTmf->IDsTreeRoot, NULL); xstrInit(&strContents, cbRead); // including null byte xstrcpy(&strContents, pszContent, cbRead - 1); // not including null byte // go build a tree of all message IDs... pStartOfFile = strContents.psz; // find first start message marker pStartOfMarker = strstr(pStartOfFile, G_pcszStartMarker); // start-of-line marker while ( (pStartOfMarker) && (!arc) ) { // start marker found: PCSZ pStartOfMsgID = pStartOfMarker + ulStartMarkerLength; // search next start marker PCSZ pStartOfNextMarker = strstr(pStartOfMsgID + 1, G_pcszStartMarker); // "\n<--" // and the end-marker PCSZ pEndOfMarker = strstr(pStartOfMsgID + 1, G_pcszEndMarker); // "-->:" PMSGENTRY pNew; // sanity checks... if ( (pStartOfNextMarker) && (pStartOfNextMarker < pEndOfMarker) ) { // next start marker before end marker: // that doesn't look correct, skip this entry pStartOfMarker = pStartOfNextMarker; continue; } if (!pEndOfMarker) // no end marker found: // that's invalid too, and there can't be any // message left in the file then... break; // alright, this ID looks correct now if (!(pNew = NEW(MSGENTRY))) arc = ERROR_NOT_ENOUGH_MEMORY; else { // length of the ID ULONG ulIDLength = pEndOfMarker - pStartOfMsgID; PCSZ pStartOfText = pEndOfMarker + ulEndMarkerLength, pNextComment; ZERO(pNew); // copy the string ID (between start and end markers) xstrInit(&pNew->strID, 0); xstrcpy(&pNew->strID, pStartOfMsgID, ulIDLength); // make ulKey point to the string ID for tree sorting pNew->Tree.ulKey = (ULONG)pNew->strID.psz; // skip leading spaces while (*pStartOfText == ' ') pStartOfText++; // store offset of start of text pNew->ulOfsText = pStartOfText - pStartOfFile; // check if there's a comment before the // next item if (pNextComment = strstr(pStartOfText, "\n;")) { if ( (!pStartOfNextMarker) || (pNextComment < pStartOfNextMarker) ) pNew->cbText = // offset of comment marker (pNextComment - pStartOfFile) - pNew->ulOfsText; else // we have a next marker AND // the comment comes after it pNew->cbText = // offset of next marker (pStartOfNextMarker - pStartOfFile) - pNew->ulOfsText; } else if (pStartOfNextMarker) // other markers left: pNew->cbText = // offset of next marker (pStartOfNextMarker - pStartOfFile) - pNew->ulOfsText; else // this was the last message: pNew->cbText = strlen(pStartOfText); // remove trailing newlines and Ctrl-Z while ( (pNew->cbText) && ( (pStartOfText[pNew->cbText - 1] == '\n') || (pStartOfText[pNew->cbText - 1] == '\r') || (pStartOfText[pNew->cbText - 1] == '\x1a') // V1.0.1 (2002-12-16) [pr] ) ) (pNew->cbText)--; // store this thing if (!treeInsert(&pTmf->IDsTreeRoot, NULL, (TREE*)pNew, treeCompareStrings)) // successfully inserted: (pTmf->cIDs)++; } // go on with next start marker (can be NULL) pStartOfMarker = pStartOfNextMarker; } // end while ( (pStartOfMarker) ... free(pszContent); } // end else if (!(pTmf = NEW(TMFMSGFILE))) return arc; } /* *@@ tmfOpenMessageFile: * opens a .TMF message file for future use * with tmfGetMessage. * * Use tmfCloseMessageFile to close the file * again and free all resources. * * Returns: * * -- NO_ERROR: *ppMsgFile has received the * new TMFMSGFILE structure. * * -- ERROR_NOT_ENOUGH_MEMORY * * plus any of the errors of doshLoadTextFile, * such as ERROR_FILE_NOT_FOUND. * *@@added V0.9.16 (2001-10-08) [umoeller] *@@changed V0.9.20 (2002-07-19) [umoeller]: optimized, no longer holding all msgs im mem */ APIRET tmfOpenMessageFile(const char *pcszMessageFile, // in: fully q'fied .TMF file name PTMFMSGFILE *ppMsgFile) // out: TMFMSGFILE struct { APIRET arc; ULONG cbFile = 0; // V1.0.7 (2005-05-21) [pr]: Workaround for kernel bug PXFILE pFile; if (!(arc = doshOpen(pcszMessageFile, XOPEN_READ_EXISTING, &cbFile, &pFile))) { // create a TMFMSGFILE entry PTMFMSGFILE pTmf; if (!(pTmf = NEW(TMFMSGFILE))) arc = ERROR_NOT_ENOUGH_MEMORY; else { ZERO(pTmf); pTmf->pszFilename = strdup(pcszMessageFile); // TMFMSGFILE created: if (!(arc = LoadAndCompile(pTmf, pFile))) { // set timestamp to that of the file FILESTATUS3 fs3; if (!(arc = DosQueryFileInfo(pFile->hf, FIL_STANDARD, &fs3, sizeof(fs3)))) { dtCreateFileTimeStamp(pTmf->szTimestamp, &fs3.fdateLastWrite, &fs3.ftimeLastWrite); // output *ppMsgFile = pTmf; } } if (arc) // error: tmfCloseMessageFile(&pTmf); } doshClose(&pFile); } return arc; } /* *@@ FreeInternalMem: * cleans out the internal message file compilation * and the content string. Used by both tmfCloseMessageFile * and tmfGetMessage to allow for recompiles when the * last-write date/time changed. * *@@added V0.9.18 (2002-03-24) [umoeller] */ STATIC VOID FreeInternalMem(PTMFMSGFILE pTmf) { LONG cItems; TREE** papNodes; if ( (cItems = pTmf->cIDs) && (papNodes = treeBuildArray(pTmf->IDsTreeRoot, &cItems)) ) { ULONG ul; for (ul = 0; ul < cItems; ul++) { PMSGENTRY pNodeThis = (PMSGENTRY)(papNodes[ul]); xstrClear(&pNodeThis->strID); free(pNodeThis); } free(papNodes); } } /* *@@ tmfCloseMessageFile: * closes a message file opened by * tmfOpenMessageFile, frees all resources, * and sets *ppMsgFile to NULL for safety. * *@@added V0.9.16 (2001-10-08) [umoeller] */ APIRET tmfCloseMessageFile(PTMFMSGFILE *ppMsgFile) { if (ppMsgFile && *ppMsgFile) { PTMFMSGFILE pTmf = *ppMsgFile; if (pTmf->pszFilename) free(pTmf->pszFilename); FreeInternalMem(pTmf); free(pTmf); *ppMsgFile = NULL; return NO_ERROR; } return ERROR_INVALID_PARAMETER; } /* *@@ tmfGetMessage: * replacement for DosGetMessage. * * After you have opened a .TMF file with tmfOpenMessageFile, * you can pass it to this function to retrieve a message * with the given string (!) ID. See tmsgfile.c for details. * * Note that this will invoke xstrcpy on the given XSTRING * buffer. In other words, the string must be initialized * (see xstrInit), but will be replaced. * * This does perform the same brain-dead string replacements * as DosGetMessage, that is, the string "%1" will be * replaced with pTable[0], "%2" will be replaced with * pTable[1], and so on. * * Returns: * * -- NO_ERROR; * * -- ERROR_INVALID_PARAMETER * * -- ERROR_MR_MID_NOT_FOUND * *@@added V0.9.16 (2001-10-08) [umoeller] *@@changed V0.9.18 (2002-03-24) [umoeller]: now recompiling if last write date changed *@@changed V0.9.20 (2002-07-19) [umoeller]: optimized, no longer holding all msgs im mem */ APIRET tmfGetMessage(PTMFMSGFILE pMsgFile, // in: msg file opened by tmfOpenMessageFile PCSZ pcszMessageName, // in: msg name to look for (case-sensitive!) PXSTRING pstr, // out: message string, if found (XSTRING must be initialized) PCSZ *pTable, // in: replacement table or NULL ULONG cTableEntries) // in: count of items in pTable or null { APIRET arc; ULONG cbFile = 0; // V1.0.7 (2005-05-21) [pr]: Workaround for kernel bug PXFILE pFile; if ( (!pMsgFile) || (!pMsgFile->pszFilename) ) return ERROR_INVALID_PARAMETER; // open the file again V0.9.20 (2002-07-19) [umoeller] if (!(arc = doshOpen(pMsgFile->pszFilename, XOPEN_READ_EXISTING, &cbFile, &pFile))) { // check if last-write date/time changed compared // to the last time we opened the thing... // V0.9.18 (2002-03-24) [umoeller] FILESTATUS3 fs3; if (!(arc = DosQueryFileInfo(pFile->hf, FIL_STANDARD, &fs3, sizeof(fs3)))) { CHAR szTemp[30]; dtCreateFileTimeStamp(szTemp, &fs3.fdateLastWrite, &fs3.ftimeLastWrite); if (strcmp(szTemp, pMsgFile->szTimestamp)) { // last write date changed: _Pmpf((__FUNCTION__ ": timestamp changed, recompiling")); FreeInternalMem(pMsgFile); if (!(arc = LoadAndCompile(pMsgFile, pFile))) strcpy(pMsgFile->szTimestamp, szTemp); } } if (!arc) { // go find the message in the tree PMSGENTRY pEntry; if (!(pEntry = (PMSGENTRY)treeFind(pMsgFile->IDsTreeRoot, (ULONG)pcszMessageName, treeCompareStrings))) arc = ERROR_MR_MID_NOT_FOUND; else { PSZ pszMsg; ULONG cbRead = pEntry->cbText; if (!(pszMsg = (PSZ)malloc(cbRead + 1))) arc = ERROR_NOT_ENOUGH_MEMORY; else if (!(arc = doshReadAt(pFile, pEntry->ulOfsText, &cbRead, pszMsg, DRFL_NOCACHE | DRFL_FAILIFLESS))) { // null-terminate pszMsg[cbRead] = '\0'; xstrset2(pstr, pszMsg, cbRead); // kick out \r\n xstrConvertLineFormat(pstr, CRLF2LF); // now replace strings from the table if (cTableEntries && pTable) { CHAR szFind[10] = "%0"; ULONG ul; for (ul = 0; ul < cTableEntries; ul++) { ULONG ulOfs = 0; _ultoa(ul + 1, szFind + 1, 10); while (xstrFindReplaceC(pstr, &ulOfs, szFind, pTable[ul])) ; } } } } } doshClose(&pFile); } return arc; } /* test case */ #ifdef __TMFDEBUG__ int main(int argc, char *argv[]) { APIRET arc; PTMFMSGFILE pMsgFile; if (argc < 3) printf("tmsgfile \n"); else { if (!(arc = tmfOpenMessageFile(argv[1], &pMsgFile))) { XSTRING str; xstrInit(&str, 0); if (!(arc = tmfGetMessage(pMsgFile, argv[2], &str, NULL, 0))) { printf("String:\n%s", str.psz); } else printf("tmfGetMessage returned %d\n", arc); xstrClear(&str); tmfCloseMessageFile(&pMsgFile); } else printf("tmfOpenMessageFile returned %d\n", arc); } } #endif