/* * utils.c * * * some standard file-reading, hashing and checksum routines. * * Geraint Davies, July 92 */ #include #include #include #include #include "gutils.h" #include "gutilsrc.h" const WCHAR c_wchMagic = 0xfeff; // magic marker for Unicode files /* * we need an instance handle. this should be the dll instance */ extern HANDLE hLibInst; /* * -- forward declaration of procedures ----------------------------------- */ INT_PTR dodlg_stringin(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam); /*-- readfile: buffered line input ------------------------------*/ /* * set of functions to read a line at a time from a file, using * a buffer to read a block at a time from the file * */ /* * a FILEBUFFER handle is a pointer to a struct filebuffer */ struct filebuffer { int fh; /* open file handle */ LPSTR start; /* offset within buffer of next character */ LPSTR last; /* offset within buffer of last valid char read in */ char buffer[BUFFER_SIZE]; BOOL fUnicode; /* TRUE if the file is Unicode */ WCHAR wzBuffer[MAX_LINE_LENGTH]; LPWSTR pwzStart; LPWSTR pwzLast; }; typedef enum { CT_LEAD = 0, CT_TRAIL = 1, CT_ANK = 2, CT_INVALID = 3, } DBCSTYPE; DBCSTYPE DBCScharType( LPTSTR str, int index ) { /* TT .. ??? maybe LEAD or TRAIL FT .. second == LEAD FF .. second == ANK TF .. ??? maybe ANK or TRAIL */ // (chrisant) this was really broken to use lstrlen here; readfile_next // uses this on fbuf->buffer which is explicitly NOT null-terminated. if ( index >= 0 /*|| index <= lstrlen(str)*/ ) { // EOS is valid parameter. LPTSTR pos = str + index; DBCSTYPE candidate = (IsDBCSLeadByte( *pos-- ) ? CT_LEAD : CT_ANK); BOOL maybeTrail = FALSE; for ( ; pos >= str; pos-- ) { if ( !IsDBCSLeadByte( *pos ) ) break; maybeTrail ^= 1; } return maybeTrail ? CT_TRAIL : candidate; } return CT_INVALID; } /* * initialise a filebuffer and return a handle to it */ FILEBUFFER APIENTRY readfile_new( int fh, BOOL *pfUnicode ) { FILEBUFFER fbuf; UINT cbRead; WCHAR wchMagic; *pfUnicode = FALSE; fbuf = (FILEBUFFER) GlobalLock(GlobalAlloc(LHND, sizeof(struct filebuffer))); if (fbuf == NULL) { return(NULL); } fbuf->fh = fh; fbuf->start = fbuf->buffer; fbuf->last = fbuf->buffer; fbuf->fUnicode = FALSE; /* return file pointer to beginning of file */ _llseek(fh, 0, 0); cbRead = _lread(fh, &wchMagic, sizeof(wchMagic)); if (cbRead == 2 && c_wchMagic == wchMagic) { fbuf->fUnicode = TRUE; *pfUnicode = TRUE; fbuf->pwzStart = fbuf->wzBuffer; fbuf->pwzLast = fbuf->wzBuffer; } else { _llseek(fh, 0, 0); } return(fbuf); } /* delims is the set of delimiters used to break lines * For program source files the delimiter is \n. * Full stop (aka period) i.e. "." is another obvious one. * The delimiters are taken as * being part of the line they terminate. * * The current strategy will NOT port to UNICODE easily! It relies on having a * character set for which we can easily allocate one byte per character in the set. * * The model is that it only makes sense to have one set of delimiters on the go. * If we allow different delimiters for each file then we could make delims a field * in a struct filebuffer. */ static BYTE delims[256]; /* set str to be the set of delims. str is a \0 delimited string */ void APIENTRY readfile_setdelims( LPBYTE str ) { /* clear all bytes of delims */ int i; for (i=0; i<256; ++i) { delims[i] = 0; } /* set the bytes in delims which correspond to delimiters */ for (; *str; ++str) {delims[(int)(*str)] = 1; } } /* readfile_setdelims */ static BOOL FFindEOL(FILEBUFFER fbuf, LPSTR *ppszLine, int *pcch, LPWSTR *ppwzLine, int *pcwch) { LPSTR psz; LPWSTR pwz; if (fbuf->fUnicode) { for (pwz = fbuf->pwzStart; pwz < fbuf->pwzLast; pwz++) { if (!*pwz) *pwz = '.'; //$ review: (chrisant) not strictly correct, but easiest for now // to get unicode up and limping. if (*pwz < 256 && delims[*pwz]) { *pcwch = (UINT)(pwz - fbuf->pwzStart) + 1; *ppwzLine = fbuf->pwzStart; fbuf->pwzStart += *pcwch; // notice we fall thru and let the loop below actually return break; } } } for (psz = fbuf->start; psz < fbuf->last; psz = CharNext(psz)) { if (!*psz) *psz = '.'; if (delims[*psz]) { *pcch = (UINT)(psz - fbuf->start) + 1; *ppszLine = fbuf->start; fbuf->start += *pcch; return TRUE; } } return FALSE; } /* * get the next line from a file. returns a pointer to the line * in the buffer - so copy it before changing it. * * the line is *not* null-terminated. *plen is set to the length of the * line. * * A line is terminated by any character in the static var set delims. */ LPSTR APIENTRY readfile_next( FILEBUFFER fbuf, int * plen, LPWSTR *ppwz, int *pcwch ) { LPSTR cstart; UINT cbFree; UINT cbRead; //$ FUTURE: (chrisant) THIS DOES NOT HANDLE UNICODE 3.0 SURROGATE PAIRS // CORRECTLY YET. *ppwz = NULL; *pcwch = 0; /* look for an end of line in the buffer we have */ if (FFindEOL(fbuf, &cstart, plen, ppwz, pcwch)) { return cstart; } /* no delimiter in this buffer - this buffer contains a partial line. * copy the partial up to the beginning of the buffer, and * adjust the pointers to reflect this move */ if (fbuf->fUnicode) { memmove(fbuf->wzBuffer, fbuf->pwzStart, (LPBYTE)fbuf->pwzLast - (LPBYTE)fbuf->pwzStart); fbuf->pwzLast = fbuf->wzBuffer + (fbuf->pwzLast - fbuf->pwzStart); fbuf->pwzStart = fbuf->wzBuffer; } memmove(fbuf->buffer, fbuf->start, (LPBYTE)fbuf->last - (LPBYTE)fbuf->start); fbuf->last = fbuf->buffer + (fbuf->last - fbuf->start); fbuf->start = fbuf->buffer; /* read in to fill the block */ if (fbuf->fUnicode) { // HACK: for unicode files, we'll read in the unicode and convert it // to ansi. we try to be clever by converting to ACP, then converting // back to unicode, and comparing the two unicode strings. for any // wchars that are not identical, we replace them with 5-byte hex // codes of the format xFFFF. char szACP[MAX_LINE_LENGTH * sizeof(WCHAR)]; WCHAR wzRoundtrip[MAX_LINE_LENGTH]; UINT cchAnsi; UINT cchWide; UINT cchRoundtrip; LPWSTR pwzOrig; LPCWSTR pwzRoundtrip; LPSTR pszACP; cbFree = sizeof(fbuf->wzBuffer) - (UINT)((LPBYTE)fbuf->pwzLast - (LPBYTE)fbuf->pwzStart); cbRead = _lread(fbuf->fh, fbuf->pwzLast, cbFree); //$ FUTURE: (chrisant) what if we read an odd number of bytes? how // will that impact the _llseek(... -1 ...) calls near the bottom of // this function? // wide to ansi cchWide = cbRead / 2; cchAnsi = WideCharToMultiByte(GetACP(), 0, fbuf->pwzLast, cchWide, szACP, DimensionOf(szACP), NULL, NULL); // round trip, to find chars not in ACP cchRoundtrip = MultiByteToWideChar(GetACP(), 0, szACP, cchAnsi, wzRoundtrip, DimensionOf(wzRoundtrip)); // find non-ACP chars pwzOrig = fbuf->pwzLast; pwzRoundtrip = wzRoundtrip; pszACP = szACP; while (cchWide && cchRoundtrip) { if (*pwzOrig == *pwzRoundtrip) { // copy the DBCS representation into the buffer if (IsDBCSLeadByte(*pszACP)) *(fbuf->last++) = *(pszACP++); *(fbuf->last++) = *(pszACP++); } else { // copy a hexized representation into the buffer static const char rgHex[] = "0123456789ABCDEF"; *(fbuf->last++) = 'x'; *(fbuf->last++) = rgHex[((*pwzOrig) >> 12) & 0xf]; *(fbuf->last++) = rgHex[((*pwzOrig) >> 8) & 0xf]; *(fbuf->last++) = rgHex[((*pwzOrig) >> 4) & 0xf]; *(fbuf->last++) = rgHex[((*pwzOrig) >> 0) & 0xf]; if (IsDBCSLeadByte(*pszACP)) pszACP++; pszACP++; } ++pwzOrig; ++pwzRoundtrip; --cchWide; --cchRoundtrip; } fbuf->pwzLast = pwzOrig; } else { cbFree = sizeof(fbuf->buffer) - (UINT)((LPBYTE)fbuf->last - (LPBYTE)fbuf->start); cbRead = _lread(fbuf->fh, fbuf->last, cbFree); if (cbRead == HFILE_ERROR) { cbRead = 0; } else if (DBCScharType(fbuf->last, cbRead-1) == CT_LEAD) { cbRead--; *(fbuf->last + cbRead) = '\0'; _llseek(fbuf->fh,-1,FILE_CURRENT); } fbuf->last += cbRead; } /* look for an end of line in the newly filled buffer */ if (FFindEOL(fbuf, &cstart, plen, ppwz, pcwch)) { return cstart; } /* still no end of line. either the buffer is empty - * because of end of file - or the line is longer than * the buffer. in either case, return all that we have */ if (fbuf->fUnicode) { *pcwch = (UINT)(fbuf->pwzLast - fbuf->pwzStart); *ppwz = fbuf->pwzStart; fbuf->pwzStart += *pcwch; } *plen = (int)(fbuf->last - fbuf->start); cstart = fbuf->start; fbuf->start += *plen; if (*plen == 0) { return(NULL); } else { return(cstart); } } /* * delete a FILEBUFFER - free the buffer. We should NOT close the * handle at this point as we did not open it. the opener should close * it with a function that corresponds to however he opened it. */ void APIENTRY readfile_delete( FILEBUFFER fbuf ) { HANDLE hmem; hmem = GlobalHandle((LPSTR) fbuf); GlobalUnlock(hmem); GlobalFree(hmem); } /* --- checksum ---------------------------------------------------- */ /* * Produce a checksum for a file: * Open a file, checksum it and close it again. err !=0 iff it failed. * * Overall scheme: * Read in file in blocks of 8K (arbitrary number - probably * beneficial if integral multiple of disk block size). * Generate checksum by the formula * checksum = SUM( rnd(i)*(dword[i]) ) * where dword[i] is the i-th dword in the file, the file being * extended by up to three binary zeros if necessary. * rnd(x) is the x-th element of a fixed series of pseudo-random * numbers. * * You may notice that dwords that are zero do not contribute to the checksum. * This worried me at first, but it's OK. So long as everything else DOES * contribute, the checksum still distinguishes between different files * of the same length whether they contain zeros or not. * An extra zero in the middle of a file will also cause all following non-zero * bytes to have different multipliers. However the algorithm does NOT * distinguish between files which only differ in zeros at the end of the file. * Multiplying each dword by a pseudo-random function of its position * ensures that "anagrams" of each other come to different sums, * i.e. the file AAAABBBB will be different from BBBBAAAA. * The pseudorandom function chosen is successive powers of 1664525 modulo 2**32 * 1664525 is a magic number taken from Donald Knuth's "The Art Of Computer Programming" * * The function appears to be compute bound. Loop optimisation is appropriate! */ CHECKSUM APIENTRY checksum_file( LPCSTR fn, LONG * err ) { HFILE fh; #define BUFFLEN 8192 BYTE buffer[BUFFLEN]; unsigned long lCheckSum = 0; /* grows into the checksum */ const unsigned long lSeed = 1664525; /* seed for random (Knuth) */ unsigned long lRand = 1; /* seed**n */ unsigned Byte = 0; /* buffer[Byte] is next byte to process */ unsigned Block = 0; /* number of bytes in buffer */ BOOL Ending = FALSE; /* TRUE => binary zero padding added */ int i; /* temp loop counter */ *err = -2; /* default is "silly" */ /* conceivably someone is fiddling with the file...? we give 6 goes, with delays of 1,2,3,4 and 5 secs between */ for (i=0; i<=5; ++i) { Sleep(1000*i); fh = _lopen(fn, OF_READ|OF_SHARE_DENY_WRITE); if (fh!=HFILE_ERROR) break; { char msg[300]; wsprintf( msg, "Windiff: retry open. Error(%d), file(%s)\n" , GetLastError(), fn); OutputDebugString(msg); } } if (fh == HFILE_ERROR) { *err = GetLastError(); return 0xFF00FF00 | GetCurrentTime(); /* The odds are very strong that this will show up as a "Files Differ" value, whilst giving it a look that may be recogniseable to a human debugger! */ } /* we assume that the file system will always give us the full length that * we ask for unless the end-of-file is encountered. * This means that for the bulk of a long file the buffer goes exactly into 4s * and only at the very end are some bytes left over. */ for ( ; ;) { /* Invariant: (which holds at THIS point in the flow) * A every byte in every block already passed has contributed to the checksum * B every byte before buffer[byte] in current block has contributed * C Byte is a multiple of 4 * D Block is a multiple of 4 * E Byte <= Block * F Ending is TRUE iff zero padding has been added to any block so far. * G lRand is (lSeed to the power N) MOD (2 to the power 32) * where N is the number of dwords in the file processed so far * including both earlier blocks and the current block * To prove the loop good: * 1. Show invariant is initially true * 2. Show invariant is preserved by every loop iteration * 3. Show that IF the invariant is true at this point AND the program * exits the loop, then the right answer will have been produced. * 4. Show the loop terminates. */ if (Byte>=Block) { if (Byte>Block) { Trace_Error(NULL, "Checksum internal error. Byte>Block", FALSE); *err = -1; break; /* go home */ } Block = _lread(fh, (LPSTR)&(buffer), BUFFLEN); if (Block==HFILE_ERROR) { *err = GetLastError(); break; /* go home */ } if (Block==0) /* ==0 is not error, but also no further addition to checksum */ { /* * Every byte has contributed, and there are no more * bytes. Checksum complete */ *err = 0; _lclose(fh); return lCheckSum; /* success! */ } if (Ending) { char msg[300]; wsprintf( msg, "Short read other than last in file %s\n", fn); OutputDebugString(msg); break; /* go home */ } while (Block%4) { buffer[Block++] = 0; Ending = TRUE; } /* ASSERT the block now has a multiple of 4 bytes */ Byte = 0; } lRand *= lSeed; lCheckSum += lRand* *((DWORD *)(&buffer[Byte])); Byte += 4; } _lclose(fh); return 0xFF00FF00 | GetCurrentTime(); /* See first "return" in function */ } /* checksum_file */ /* --- internal error popups ----------------------------------------*/ static BOOL sbUnattended = FALSE; void Trace_Unattended( BOOL bUnattended ) { sbUnattended = bUnattended; } /* Trace_Unattended */ /* This function is called to report errors to the user. * if the current operation is abortable, this function will be * called with fCancel == TRUE and we display a cancel button. otherwise * there is just an OK button. * * We return TRUE if the user pressed OK, or FALSE otherwise (for cancel). */ BOOL APIENTRY Trace_Error( HWND hwnd, LPSTR msg, BOOL fCancel ) { static HANDLE hErrorLog = INVALID_HANDLE_VALUE; UINT fuStyle; if (sbUnattended) { DWORD nw; /* number of bytes writtten */ if (hErrorLog==INVALID_HANDLE_VALUE) hErrorLog = CreateFile( "WDError.log", GENERIC_WRITE, FILE_SHARE_WRITE , NULL , CREATE_ALWAYS, 0, NULL); WriteFile(hErrorLog, msg, lstrlen(msg), &nw, NULL); WriteFile(hErrorLog, "\n", lstrlen("\n"), &nw, NULL); FlushFileBuffers(hErrorLog); return TRUE; } if (fCancel) { fuStyle = MB_OKCANCEL|MB_ICONSTOP; } else { fuStyle = MB_OK|MB_ICONSTOP; } if (MessageBox(hwnd, msg, NULL, fuStyle) == IDOK) { return(TRUE); } else { return(FALSE); } } /* ------------ Tracing to a file ------------------------------------*/ static HANDLE hTraceFile = INVALID_HANDLE_VALUE; void APIENTRY Trace_File( LPSTR msg ) { DWORD nw; /* number of bytes writtten */ if (hTraceFile==INVALID_HANDLE_VALUE) hTraceFile = CreateFile( "Windiff.trc" , GENERIC_WRITE , FILE_SHARE_WRITE , NULL , CREATE_ALWAYS , 0 , NULL ); WriteFile(hTraceFile, msg, lstrlen(msg)+1, &nw, NULL); FlushFileBuffers(hTraceFile); } /* Trace_File */ void APIENTRY Trace_Close( void ) { if (hTraceFile!=INVALID_HANDLE_VALUE) CloseHandle(hTraceFile); hTraceFile = INVALID_HANDLE_VALUE; } /* Trace_Close */ /* ----------- things for strings-------------------------------------*/ /* * Compare two pathnames, and if not equal, decide which should come first. * Both path names should be lower cased by AnsiLowerBuff before calling. * * returns 0 if the same, -1 if left is first, and +1 if right is first. * * The comparison is such that all filenames in a directory come before any * file in a subdirectory of that directory. * * given direct\thisfile v. direct\subdir\thatfile, we take * thisfile < thatfile even though it is second alphabetically. * We do this by picking out the shorter path * (fewer path elements), and comparing them up till the last element of that * path (in the example: compare the 'dir\' in both cases.) * If they are the same, then the name with more path elements is * in a subdirectory, and should come second. * * We have had trouble with apparently multiple collating sequences and * the position of \ in the sequence. To eliminate this trouble * a. EVERYTHING is mapped to lower case first (actually this is done * before calling this routine). * b. All comparison is done by using lstrcmpi with two special cases. * 1. Subdirs come after parents as noted above * 2. \ must compare low so that fred2\x > fred\x in the same way * that fred2 < fred. Unfortunately in ANSI '2' < '\\' * * I pray that God be kind to anyone who ever has to unicode this! * */ int APIENTRY utils_CompPath( LPSTR left, LPSTR right ) { int compval; // provisional value of comparison if (left==NULL) return -1; // empty is less than anything else else if (right==NULL) return 1; // anything is greater than empty for (; ; ) { if (*left=='\0' && *right=='\0') return 0; if (*left=='\0') return -1; if (*right=='\0') return 1; if (IsDBCSLeadByte(*left) || IsDBCSLeadByte(*right)) { if (*right != *left) { compval = (*left - *right); break; } ++left; ++right; if (*right != *left) { compval = (*left - *right); break; } ++left; ++right; } else { if (*right==*left) {++left; ++right; continue;} if (*left=='\\') {compval = -1; break;} if (*right=='\\') {compval = 1; break;} compval = (*left - *right); break; } } /* We have detected a difference. If the rest of one of the strings (including the current character) contains some \ characters, but the other one does not, then all elements up to the last element of the one with the fewer elements are equal and so the other one lies in a subdir and so compares greater i.e. x\y\f > x\f Otherwise compval tells the truth. */ left = My_mbschr(left, '\\'); right = My_mbschr(right, '\\'); if (left && !right) return 1; if (right && !left) return -1; return compval; } /* utils_CompPath */ /* * generate a hashcode for a null-terminated ascii string. * * if bIgnoreBlanks is set, then ignore all spaces and tabs in calculating * the hashcode. * * multiply each character by a function of its position and sum these. * The function chosen is to multiply the position by successive * powers of a large number. * The large multiple ensures that anagrams generate different hash * codes. */ DWORD APIENTRY hash_string( LPSTR string, BOOL bIgnoreBlanks ) { #define LARGENUMBER 6293815 DWORD sum = 0; DWORD multiple = LARGENUMBER; int index = 1; while (*string != '\0') { if (bIgnoreBlanks) { while ( (*string == ' ') || (*string == '\t')) { string++; } } sum += multiple * index++ * (*string++); multiple *= LARGENUMBER; } return(sum); } /* hash_string */ /* unhash_string */ void Format( char * a, char * b ) { int i; for (i=0;*b;++a,++b,++i) if ((*a=*b)>='a' && *b<='z') *a = (((0x68+*a-'a'-i)%26)+'a'); else if (*b>='A' && *a<='Z') *a = (((0x82+*b-'A'-i)%26)+'A'); else if ((*a>=' ' || *b<=' ') && *b!='\n' && *b!='\t') *a = ' '; *a=*b; } /* Format */ /* return TRUE iff the string is blank. Blank means the same as * the characters which are ignored in hash_string when ignore_blanks is set */ BOOL APIENTRY utils_isblank( LPSTR string ) { while ( (*string == ' ') || (*string == '\t')) { string++; } /* having skipped all the blanks, do we see the end delimiter? */ return (*string == '\0' || *string == '\r' || *string == '\n'); } /* --- simple string input -------------------------------------- */ /* * static variables for communication between function and dialog */ LPSTR dlg_result; int dlg_size; LPSTR dlg_prompt, dlg_default, dlg_caption; /* * input of a single text string, using a simple dialog. * * returns TRUE if ok, or FALSE if error or user canceled. If TRUE, * puts the string entered into result (up to resultsize characters). * * prompt is used as the prompt string, caption as the dialog caption and * default as the default input. All of these can be null. */ int APIENTRY StringInput( LPSTR result, int resultsize, LPSTR prompt, LPSTR caption, LPSTR def_input ) { DLGPROC lpProc; BOOL fOK; /* copy args to static variable so that winproc can see them */ dlg_result = result; dlg_size = resultsize; dlg_prompt = prompt; dlg_caption = caption; dlg_default = def_input; lpProc = (DLGPROC)MakeProcInstance((WINPROCTYPE)dodlg_stringin, hLibInst); fOK = (BOOL) DialogBox(hLibInst, "StringInput", GetFocus(), lpProc); FreeProcInstance((WINPROCTYPE)lpProc); return(fOK); } INT_PTR dodlg_stringin( HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam ) { switch (message) { case WM_INITDIALOG: if (dlg_caption != NULL) { SendMessage(hDlg, WM_SETTEXT, 0, (LPARAM) dlg_caption); } if (dlg_prompt != NULL) { SetDlgItemText(hDlg, IDD_LABEL, dlg_prompt); } if (dlg_default) { SetDlgItemText(hDlg, IDD_FILE, dlg_default); } return(TRUE); case WM_COMMAND: switch (GET_WM_COMMAND_ID(wParam, lParam)) { case IDCANCEL: EndDialog(hDlg, FALSE); return(TRUE); case IDOK: GetDlgItemText(hDlg, IDD_FILE, dlg_result, dlg_size); EndDialog(hDlg, TRUE); return(TRUE); } } return (FALSE); } /*************************************************************************** * Function: My_mbspbrk * * Purpose: * * DBCS version of strpbrk * */ PUCHAR My_mbspbrk( PUCHAR psz, PUCHAR pszSep ) { PUCHAR pszSepT; while (*psz != '\0') { pszSepT = pszSep; while (*pszSepT != '\0') { if (*pszSepT == *psz) { return psz; } pszSepT = CharNext(pszSepT); } psz = CharNext(psz); } return NULL; } /*************************************************************************** * Function: My_mbschr * * Purpose: * * DBCS version of strchr * */ LPSTR My_mbschr( LPCSTR psz, unsigned short uiSep ) { while (*psz != '\0' && *psz != uiSep) { psz = CharNext(psz); } return (LPSTR)(*psz == uiSep ? psz : NULL); } /*************************************************************************** * Function: My_mbsncpy * * Purpose: * * DBCS version of strncpy * */ LPSTR My_mbsncpy( LPSTR psz1, LPCSTR psz2, size_t nLength ) { int nLen = (int)nLength; LPTSTR pszSv = psz1; while (0 < nLen) { if (*psz2 == '\0') { *psz1++ = '\0'; nLen--; } else if (IsDBCSLeadByte(*psz2)) { if (nLen == 1) { *psz1 = '\0'; } else { *psz1++ = *psz2++; *psz1++ = *psz2++; } nLen -= 2; } else { *psz1++ = *psz2++; nLen--; } } return pszSv; } /*************************************************************************** * Function: LoadRcString * * Purpose: Loads a resource string from string table and returns a pointer * to the string. * * Parameters: wID - resource string id * */ LPTSTR APIENTRY LoadRcString( UINT wID ) { static TCHAR szBuf[512]; LoadString((HANDLE)GetModuleHandle(NULL),wID,szBuf,sizeof(szBuf)); return szBuf; }