Define EXTRA_TRACES instead of #if 0.
[wine] / dlls / version / install.c
1 /*
2  * Implementation of VERSION.DLL - File Installer routines
3  *
4  * Copyright 1996,1997 Marcus Meissner
5  * Copyright 1997 David Cuthbert
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20  *
21  * TODO
22  *   o Check the installation functions.
23  */
24
25 #include <stdlib.h>
26 #include <stdarg.h>
27 #include <stdio.h>
28 #include <string.h>
29
30 #include "windef.h"
31 #include "winbase.h"
32 #include "winver.h"
33 #include "winnls.h"
34 #include "wine/unicode.h"
35 #include "winerror.h"
36 #include "lzexpand.h"
37 #include "wine/debug.h"
38
39 WINE_DEFAULT_DEBUG_CHANNEL(ver);
40
41
42 /******************************************************************************
43  *   testFileExistenceA
44  *
45  *   Tests whether a given path/file combination exists.  If the file does
46  *   not exist, the return value is zero.  If it does exist, the return
47  *   value is non-zero.
48  *
49  *   Revision history
50  *      30-May-1997 Dave Cuthbert (dacut@ece.cmu.edu)
51  *         Original implementation
52  *
53  */
54 static int testFileExistenceA( char const * path, char const * file, BOOL excl )
55 {
56     char  filename[1024];
57     int  filenamelen;
58     OFSTRUCT  fileinfo;
59
60     fileinfo.cBytes = sizeof(OFSTRUCT);
61
62     strcpy(filename, path);
63     filenamelen = strlen(filename);
64
65     /* Add a trailing \ if necessary */
66     if(filenamelen) {
67         if(filename[filenamelen - 1] != '\\')
68             strcat(filename, "\\");
69     }
70     else /* specify the current directory */
71         strcpy(filename, ".\\");
72
73     /* Create the full pathname */
74     strcat(filename, file);
75
76     return (OpenFile(filename, &fileinfo,
77                      OF_EXIST | (excl ? OF_SHARE_EXCLUSIVE : 0)) != HFILE_ERROR);
78 }
79
80 /******************************************************************************
81  *   testFileExistenceW
82  */
83 static int testFileExistenceW( const WCHAR *path, const WCHAR *file, BOOL excl )
84 {
85     char *filename;
86     DWORD pathlen, filelen;
87     int ret;
88     OFSTRUCT fileinfo;
89
90     fileinfo.cBytes = sizeof(OFSTRUCT);
91
92     pathlen = WideCharToMultiByte( CP_ACP, 0, path, -1, NULL, 0, NULL, NULL );
93     filelen = WideCharToMultiByte( CP_ACP, 0, file, -1, NULL, 0, NULL, NULL );
94     filename = HeapAlloc( GetProcessHeap(), 0, pathlen+filelen+2 );
95
96     WideCharToMultiByte( CP_ACP, 0, path, -1, filename, pathlen, NULL, NULL );
97     /* Add a trailing \ if necessary */
98     if (pathlen > 1)
99     {
100         if (filename[pathlen-2] != '\\') strcpy( &filename[pathlen-1], "\\" );
101     }
102     else /* specify the current directory */
103         strcpy(filename, ".\\");
104
105     WideCharToMultiByte( CP_ACP, 0, file, -1, filename+strlen(filename), filelen, NULL, NULL );
106
107     ret = (OpenFile(filename, &fileinfo,
108                     OF_EXIST | (excl ? OF_SHARE_EXCLUSIVE : 0)) != HFILE_ERROR);
109     HeapFree( GetProcessHeap(), 0, filename );
110     return ret;
111 }
112
113 /*****************************************************************************
114  *   VerFindFileA [VERSION.@]
115  *
116  *   Determines where to install a file based on whether it locates another
117  *   version of the file in the system.  The values VerFindFile returns are
118  *   used in a subsequent call to the VerInstallFile function.
119  *
120  *   Revision history:
121  *      30-May-1997   Dave Cuthbert (dacut@ece.cmu.edu)
122  *         Reimplementation of VerFindFile from original stub.
123  */
124 DWORD WINAPI VerFindFileA(
125     UINT flags,
126     LPCSTR lpszFilename,
127     LPCSTR lpszWinDir,
128     LPCSTR lpszAppDir,
129     LPSTR lpszCurDir,
130     UINT *lpuCurDirLen,
131     LPSTR lpszDestDir,
132     UINT *lpuDestDirLen )
133 {
134     DWORD  retval = 0;
135     const char *curDir;
136     const char *destDir;
137     unsigned int  curDirSizeReq;
138     unsigned int  destDirSizeReq;
139     char  systemDir[MAX_PATH];
140
141     /* Print out debugging information */
142     TRACE("flags = %x filename=%s windir=%s appdir=%s curdirlen=%p(%u) destdirlen=%p(%u)\n",
143           flags, debugstr_a(lpszFilename), debugstr_a(lpszWinDir), debugstr_a(lpszAppDir),
144           lpuCurDirLen, lpuCurDirLen ? *lpuCurDirLen : 0,
145           lpuDestDirLen, lpuDestDirLen ? *lpuDestDirLen : 0 );
146
147     /* Figure out where the file should go; shared files default to the
148        system directory */
149
150     GetSystemDirectoryA(systemDir, sizeof(systemDir));
151     curDir = "";
152     destDir = "";
153
154     if(flags & VFFF_ISSHAREDFILE)
155     {
156         destDir = systemDir;
157         /* Were we given a filename?  If so, try to find the file. */
158         if(lpszFilename)
159         {
160             if(testFileExistenceA(destDir, lpszFilename, FALSE)) curDir = destDir;
161             else if(lpszAppDir && testFileExistenceA(lpszAppDir, lpszFilename, FALSE))
162             {
163                 curDir = lpszAppDir;
164                 retval |= VFF_CURNEDEST;
165             }
166         }
167     }
168     else /* not a shared file */
169     {
170         if(lpszAppDir)
171         {
172             destDir = lpszAppDir;
173             if(lpszFilename)
174             {
175                 if(testFileExistenceA(destDir, lpszFilename, FALSE)) curDir = destDir;
176                 else if(testFileExistenceA(systemDir, lpszFilename, FALSE))
177                 {
178                     curDir = systemDir;
179                     retval |= VFF_CURNEDEST;
180                 }
181             }
182         }
183     }
184
185     if (lpszFilename && !testFileExistenceA(curDir, lpszFilename, TRUE))
186         retval |= VFF_FILEINUSE;
187
188     curDirSizeReq = strlen(curDir) + 1;
189     destDirSizeReq = strlen(destDir) + 1;
190
191     /* Make sure that the pointers to the size of the buffers are
192        valid; if not, do NOTHING with that buffer.  If that pointer
193        is valid, then make sure that the buffer pointer is valid, too! */
194
195     if(lpuDestDirLen && lpszDestDir)
196     {
197         if (*lpuDestDirLen < destDirSizeReq) retval |= VFF_BUFFTOOSMALL;
198         lstrcpynA(lpszDestDir, destDir, *lpuDestDirLen);
199         *lpuDestDirLen = destDirSizeReq;
200     }
201     if(lpuCurDirLen && lpszCurDir)
202     {
203         if(*lpuCurDirLen < curDirSizeReq) retval |= VFF_BUFFTOOSMALL;
204         lstrcpynA(lpszCurDir, curDir, *lpuCurDirLen);
205         *lpuCurDirLen = curDirSizeReq;
206     }
207
208     TRACE("ret = %lu (%s%s%s) curdir=%s destdir=%s\n", retval,
209           (retval & VFF_CURNEDEST) ? "VFF_CURNEDEST " : "",
210           (retval & VFF_FILEINUSE) ? "VFF_FILEINUSE " : "",
211           (retval & VFF_BUFFTOOSMALL) ? "VFF_BUFFTOOSMALL " : "",
212           debugstr_a(lpszCurDir), debugstr_a(lpszDestDir));
213
214     return retval;
215 }
216
217 /*****************************************************************************
218  * VerFindFileW                                         [VERSION.@]
219  */
220 DWORD WINAPI VerFindFileW( UINT flags,LPCWSTR lpszFilename,LPCWSTR lpszWinDir,
221                            LPCWSTR lpszAppDir, LPWSTR lpszCurDir,UINT *lpuCurDirLen,
222                            LPWSTR lpszDestDir,UINT *lpuDestDirLen )
223 {
224     static const WCHAR emptyW;
225     DWORD retval = 0;
226     const WCHAR *curDir;
227     const WCHAR *destDir;
228     unsigned int curDirSizeReq;
229     unsigned int destDirSizeReq;
230     WCHAR systemDir[MAX_PATH];
231
232     /* Print out debugging information */
233     TRACE("flags = %x filename=%s windir=%s appdir=%s curdirlen=%p(%u) destdirlen=%p(%u)\n",
234           flags, debugstr_w(lpszFilename), debugstr_w(lpszWinDir), debugstr_w(lpszAppDir),
235           lpuCurDirLen, lpuCurDirLen ? *lpuCurDirLen : 0,
236           lpuDestDirLen, lpuDestDirLen ? *lpuDestDirLen : 0 );
237
238     /* Figure out where the file should go; shared files default to the
239        system directory */
240
241     GetSystemDirectoryW(systemDir, sizeof(systemDir)/sizeof(WCHAR));
242     curDir = &emptyW;
243     destDir = &emptyW;
244
245     if(flags & VFFF_ISSHAREDFILE)
246     {
247         destDir = systemDir;
248         /* Were we given a filename?  If so, try to find the file. */
249         if(lpszFilename)
250         {
251             if(testFileExistenceW(destDir, lpszFilename, FALSE)) curDir = destDir;
252             else if(lpszAppDir && testFileExistenceW(lpszAppDir, lpszFilename, FALSE))
253             {
254                 curDir = lpszAppDir;
255                 retval |= VFF_CURNEDEST;
256             }
257         }
258     }
259     else /* not a shared file */
260     {
261         if(lpszAppDir)
262         {
263             destDir = lpszAppDir;
264             if(lpszFilename)
265             {
266                 if(testFileExistenceW(destDir, lpszFilename, FALSE)) curDir = destDir;
267                 else if(testFileExistenceW(systemDir, lpszFilename, FALSE))
268                 {
269                     curDir = systemDir;
270                     retval |= VFF_CURNEDEST;
271                 }
272             }
273         }
274     }
275
276     if (lpszFilename && !testFileExistenceW(curDir, lpszFilename, TRUE))
277         retval |= VFF_FILEINUSE;
278
279     curDirSizeReq = strlenW(curDir) + 1;
280     destDirSizeReq = strlenW(destDir) + 1;
281
282     /* Make sure that the pointers to the size of the buffers are
283        valid; if not, do NOTHING with that buffer.  If that pointer
284        is valid, then make sure that the buffer pointer is valid, too! */
285
286     if(lpuDestDirLen && lpszDestDir)
287     {
288         if (*lpuDestDirLen < destDirSizeReq) retval |= VFF_BUFFTOOSMALL;
289         lstrcpynW(lpszDestDir, destDir, *lpuDestDirLen);
290         *lpuDestDirLen = destDirSizeReq;
291     }
292     if(lpuCurDirLen && lpszCurDir)
293     {
294         if(*lpuCurDirLen < curDirSizeReq) retval |= VFF_BUFFTOOSMALL;
295         lstrcpynW(lpszCurDir, curDir, *lpuCurDirLen);
296         *lpuCurDirLen = curDirSizeReq;
297     }
298
299     TRACE("ret = %lu (%s%s%s) curdir=%s destdir=%s\n", retval,
300           (retval & VFF_CURNEDEST) ? "VFF_CURNEDEST " : "",
301           (retval & VFF_FILEINUSE) ? "VFF_FILEINUSE " : "",
302           (retval & VFF_BUFFTOOSMALL) ? "VFF_BUFFTOOSMALL " : "",
303           debugstr_w(lpszCurDir), debugstr_w(lpszDestDir));
304     return retval;
305 }
306
307 static LPBYTE
308 _fetch_versioninfo(LPSTR fn,VS_FIXEDFILEINFO **vffi) {
309     DWORD       alloclen;
310     LPBYTE      buf;
311     DWORD       ret;
312
313     alloclen = 1000;
314     buf=HeapAlloc(GetProcessHeap(), 0, alloclen);
315     if(buf == NULL) {
316         WARN("Memory exausted while fetching version info!\n");
317         return NULL;
318     }
319     while (1) {
320         ret = GetFileVersionInfoA(fn,0,alloclen,buf);
321         if (!ret) {
322             HeapFree(GetProcessHeap(), 0, buf);
323             return NULL;
324         }
325         if (alloclen<*(WORD*)buf) {
326             alloclen = *(WORD*)buf;
327             HeapFree(GetProcessHeap(), 0, buf);
328             buf = HeapAlloc(GetProcessHeap(), 0, alloclen);
329             if(buf == NULL) {
330                WARN("Memory exausted while fetching version info!\n");
331                return NULL;
332             }
333         } else {
334             *vffi = (VS_FIXEDFILEINFO*)(buf+0x14);
335             if ((*vffi)->dwSignature == 0x004f0049) /* hack to detect unicode */
336                 *vffi = (VS_FIXEDFILEINFO*)(buf+0x28);
337             if ((*vffi)->dwSignature != VS_FFI_SIGNATURE)
338                 WARN("Bad VS_FIXEDFILEINFO signature 0x%08lx\n",(*vffi)->dwSignature);
339             return buf;
340         }
341     }
342 }
343
344 static DWORD
345 _error2vif(DWORD error) {
346     switch (error) {
347     case ERROR_ACCESS_DENIED:
348         return VIF_ACCESSVIOLATION;
349     case ERROR_SHARING_VIOLATION:
350         return VIF_SHARINGVIOLATION;
351     default:
352         return 0;
353     }
354 }
355
356
357 /******************************************************************************
358  * VerInstallFileA [VERSION.@]
359  */
360 DWORD WINAPI VerInstallFileA(
361         UINT flags,LPCSTR srcfilename,LPCSTR destfilename,LPCSTR srcdir,
362         LPCSTR destdir,LPCSTR curdir,LPSTR tmpfile,UINT *tmpfilelen )
363 {
364     LPCSTR pdest;
365     char        destfn[260],tmpfn[260],srcfn[260];
366     HFILE       hfsrc,hfdst;
367     DWORD       attr,ret,xret,tmplast;
368     LPBYTE      buf1,buf2;
369     OFSTRUCT    ofs;
370
371     TRACE("(%x,%s,%s,%s,%s,%s,%p,%d)\n",
372             flags,srcfilename,destfilename,srcdir,destdir,curdir,tmpfile,*tmpfilelen
373     );
374     xret = 0;
375     sprintf(srcfn,"%s\\%s",srcdir,srcfilename);
376     if (!destdir || !*destdir) pdest = srcdir;
377     else pdest = destdir;
378     sprintf(destfn,"%s\\%s",pdest,destfilename);
379     hfsrc=LZOpenFileA(srcfn,&ofs,OF_READ);
380     if (hfsrc < 0)
381         return VIF_CANNOTREADSRC;
382     sprintf(tmpfn,"%s\\%s",pdest,destfilename);
383     tmplast=strlen(pdest)+1;
384     attr = GetFileAttributesA(tmpfn);
385     if (attr != INVALID_FILE_ATTRIBUTES) {
386         if (attr & FILE_ATTRIBUTE_READONLY) {
387             LZClose(hfsrc);
388             return VIF_WRITEPROT;
389         }
390         /* FIXME: check if file currently in use and return VIF_FILEINUSE */
391     }
392     attr = INVALID_FILE_ATTRIBUTES;
393     if (flags & VIFF_FORCEINSTALL) {
394         if (tmpfile[0]) {
395             sprintf(tmpfn,"%s\\%s",pdest,tmpfile);
396             tmplast = strlen(pdest)+1;
397             attr = GetFileAttributesA(tmpfn);
398             /* if it exists, it has been copied by the call before.
399              * we jump over the copy part...
400              */
401         }
402     }
403     if (attr == INVALID_FILE_ATTRIBUTES) {
404         char    *s;
405
406         GetTempFileNameA(pdest,"ver",0,tmpfn); /* should not fail ... */
407         s=strrchr(tmpfn,'\\');
408         if (s)
409             tmplast = s-tmpfn;
410         else
411             tmplast = 0;
412         hfdst = OpenFile(tmpfn,&ofs,OF_CREATE);
413         if (hfdst == HFILE_ERROR) {
414             LZClose(hfsrc);
415             return VIF_CANNOTCREATE; /* | translated dos error */
416         }
417         ret = LZCopy(hfsrc,hfdst);
418         _lclose(hfdst);
419         if (((long) ret) < 0) {
420             /* translate LZ errors into VIF_xxx */
421             switch (ret) {
422             case LZERROR_BADINHANDLE:
423             case LZERROR_READ:
424             case LZERROR_BADVALUE:
425             case LZERROR_UNKNOWNALG:
426                 ret = VIF_CANNOTREADSRC;
427                 break;
428             case LZERROR_BADOUTHANDLE:
429             case LZERROR_WRITE:
430                 ret = VIF_OUTOFSPACE;
431                 break;
432             case LZERROR_GLOBALLOC:
433             case LZERROR_GLOBLOCK:
434                 ret = VIF_OUTOFMEMORY;
435                 break;
436             default: /* unknown error, should not happen */
437                 ret = 0;
438                 break;
439             }
440             if (ret) {
441                 LZClose(hfsrc);
442                 return ret;
443             }
444         }
445     }
446     xret = 0;
447     if (!(flags & VIFF_FORCEINSTALL)) {
448         VS_FIXEDFILEINFO *destvffi,*tmpvffi;
449         buf1 = _fetch_versioninfo(destfn,&destvffi);
450         if (buf1) {
451             buf2 = _fetch_versioninfo(tmpfn,&tmpvffi);
452             if (buf2) {
453                 char    *tbuf1,*tbuf2;
454                 UINT    len1,len2;
455
456                 len1=len2=40;
457
458                 /* compare file versions */
459                 if ((destvffi->dwFileVersionMS > tmpvffi->dwFileVersionMS)||
460                     ((destvffi->dwFileVersionMS==tmpvffi->dwFileVersionMS)&&
461                      (destvffi->dwFileVersionLS > tmpvffi->dwFileVersionLS)
462                     )
463                 )
464                     xret |= VIF_MISMATCH|VIF_SRCOLD;
465                 /* compare filetypes and filesubtypes */
466                 if ((destvffi->dwFileType!=tmpvffi->dwFileType) ||
467                     (destvffi->dwFileSubtype!=tmpvffi->dwFileSubtype)
468                 )
469                     xret |= VIF_MISMATCH|VIF_DIFFTYPE;
470                 if (VerQueryValueA(buf1,"\\VarFileInfo\\Translation",(LPVOID*)&tbuf1,&len1) &&
471                     VerQueryValueA(buf2,"\\VarFileInfo\\Translation",(LPVOID*)&tbuf2,&len2)
472                 ) {
473                     /* irgendwas mit tbuf1 und tbuf2 machen
474                      * generiert DIFFLANG|MISMATCH
475                      */
476                 }
477                 HeapFree(GetProcessHeap(), 0, buf2);
478             } else
479                 xret=VIF_MISMATCH|VIF_SRCOLD;
480             HeapFree(GetProcessHeap(), 0, buf1);
481         }
482     }
483     if (xret) {
484         if (*tmpfilelen<strlen(tmpfn+tmplast)) {
485             xret|=VIF_BUFFTOOSMALL;
486             DeleteFileA(tmpfn);
487         } else {
488             strcpy(tmpfile,tmpfn+tmplast);
489             *tmpfilelen = strlen(tmpfn+tmplast)+1;
490             xret|=VIF_TEMPFILE;
491         }
492     } else {
493         if (-1!=GetFileAttributesA(destfn))
494             if (!DeleteFileA(destfn)) {
495                 xret|=_error2vif(GetLastError())|VIF_CANNOTDELETE;
496                 DeleteFileA(tmpfn);
497                 LZClose(hfsrc);
498                 return xret;
499             }
500         if ((!(flags & VIFF_DONTDELETEOLD))     &&
501             curdir                              &&
502             *curdir                             &&
503             lstrcmpiA(curdir,pdest)
504         ) {
505             char curfn[260];
506
507             sprintf(curfn,"%s\\%s",curdir,destfilename);
508             if (INVALID_FILE_ATTRIBUTES != GetFileAttributesA(curfn)) {
509                 /* FIXME: check if in use ... if it is, VIF_CANNOTDELETECUR */
510                 if (!DeleteFileA(curfn))
511                     xret|=_error2vif(GetLastError())|VIF_CANNOTDELETECUR;
512             }
513         }
514         if (!MoveFileA(tmpfn,destfn)) {
515             xret|=_error2vif(GetLastError())|VIF_CANNOTRENAME;
516             DeleteFileA(tmpfn);
517         }
518     }
519     LZClose(hfsrc);
520     return xret;
521 }
522
523
524 /******************************************************************************
525  * VerInstallFileW                              [VERSION.@]
526  */
527 DWORD WINAPI VerInstallFileW(
528         UINT flags,LPCWSTR srcfilename,LPCWSTR destfilename,LPCWSTR srcdir,
529         LPCWSTR destdir,LPCWSTR curdir,LPWSTR tmpfile,UINT *tmpfilelen )
530 {
531     LPSTR wsrcf = NULL, wsrcd = NULL, wdestf = NULL, wdestd = NULL, wtmpf = NULL, wcurd = NULL;
532     DWORD ret;
533     UINT len;
534
535     if (srcfilename)
536     {
537         len = WideCharToMultiByte( CP_ACP, 0, srcfilename, -1, NULL, 0, NULL, NULL );
538         if ((wsrcf = HeapAlloc( GetProcessHeap(), 0, len )))
539             WideCharToMultiByte( CP_ACP, 0, srcfilename, -1, wsrcf, len, NULL, NULL );
540     }
541     if (srcdir)
542     {
543         len = WideCharToMultiByte( CP_ACP, 0, srcdir, -1, NULL, 0, NULL, NULL );
544         if ((wsrcd = HeapAlloc( GetProcessHeap(), 0, len )))
545             WideCharToMultiByte( CP_ACP, 0, srcdir, -1, wsrcd, len, NULL, NULL );
546     }
547     if (destfilename)
548     {
549         len = WideCharToMultiByte( CP_ACP, 0, destfilename, -1, NULL, 0, NULL, NULL );
550         if ((wdestf = HeapAlloc( GetProcessHeap(), 0, len )))
551             WideCharToMultiByte( CP_ACP, 0, destfilename, -1, wdestf, len, NULL, NULL );
552     }
553     if (destdir)
554     {
555         len = WideCharToMultiByte( CP_ACP, 0, destdir, -1, NULL, 0, NULL, NULL );
556         if ((wdestd = HeapAlloc( GetProcessHeap(), 0, len )))
557             WideCharToMultiByte( CP_ACP, 0, destdir, -1, wdestd, len, NULL, NULL );
558     }
559     if (curdir)
560     {
561         len = WideCharToMultiByte( CP_ACP, 0, curdir, -1, NULL, 0, NULL, NULL );
562         if ((wcurd = HeapAlloc( GetProcessHeap(), 0, len )))
563             WideCharToMultiByte( CP_ACP, 0, curdir, -1, wcurd, len, NULL, NULL );
564     }
565     len = *tmpfilelen * sizeof(WCHAR);
566     wtmpf = HeapAlloc( GetProcessHeap(), 0, len );
567     ret = VerInstallFileA(flags,wsrcf,wdestf,wsrcd,wdestd,wcurd,wtmpf,&len);
568     if (!ret)
569         *tmpfilelen = MultiByteToWideChar( CP_ACP, 0, wtmpf, -1, tmpfile, *tmpfilelen );
570     else if (ret & VIF_BUFFTOOSMALL)
571         *tmpfilelen = len;  /* FIXME: not correct */
572
573     HeapFree( GetProcessHeap(), 0, wsrcf );
574     HeapFree( GetProcessHeap(), 0, wsrcd );
575     HeapFree( GetProcessHeap(), 0, wdestf );
576     HeapFree( GetProcessHeap(), 0, wdestd );
577     HeapFree( GetProcessHeap(), 0, wtmpf );
578     HeapFree( GetProcessHeap(), 0, wcurd );
579     return ret;
580 }