dmime: Remove 'recursive registry key delete' function.
[wine] / programs / xcopy / xcopy.c
1 /*
2  * XCOPY - Wine-compatible xcopy program
3  *
4  * Copyright (C) 2007 J. Edmeades
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19  */
20
21 /*
22  * FIXME:
23  * This should now support all options listed in the xcopy help from
24  * windows XP except:
25  *  /Z - Copy from network drives in restartable mode
26  *  /X - Copy file audit settings (sets /O)
27  *  /O - Copy file ownership + ACL info
28  *  /G - Copy encrypted files to unencrypted destination
29  *  /V - Verifies files
30  */
31
32 /*
33  * Notes:
34  * Apparently, valid return codes are:
35  *   0 - OK
36  *   1 - No files found to copy
37  *   2 - CTRL+C during copy
38  *   4 - Initialization error, or invalid source specification
39  *   5 - Disk write error
40  */
41
42
43 #include <stdio.h>
44 #include <windows.h>
45 #include <wine/debug.h>
46 #include "xcopy.h"
47
48 WINE_DEFAULT_DEBUG_CHANNEL(xcopy);
49
50 /* Prototypes */
51 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
52                                    WCHAR *spec, DWORD flags);
53 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem,
54                                  WCHAR *spec, WCHAR *srcspec, DWORD flags);
55 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
56                         WCHAR *deststem, WCHAR *destspec,
57                         DWORD flags);
58 static BOOL XCOPY_CreateDirectory(const WCHAR* path);
59 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms);
60 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName);
61 static WCHAR *XCOPY_LoadMessage(UINT id);
62 static void XCOPY_FailMessage(DWORD err);
63 static int XCOPY_wprintf(const WCHAR *format, ...);
64
65 /* Typedefs */
66 typedef struct _EXCLUDELIST
67 {
68   struct _EXCLUDELIST *next;
69   WCHAR               *name;
70 } EXCLUDELIST;
71
72
73 /* Global variables */
74 static ULONG filesCopied           = 0;              /* Number of files copied  */
75 static EXCLUDELIST *excludeList    = NULL;           /* Excluded strings list   */
76 static FILETIME dateRange;                           /* Date range to copy after*/
77 static const WCHAR wchr_slash[]   = {'\\', 0};
78 static const WCHAR wchr_star[]    = {'*', 0};
79 static const WCHAR wchr_dot[]     = {'.', 0};
80 static const WCHAR wchr_dotdot[]  = {'.', '.', 0};
81
82 /* Constants (Mostly for widechars) */
83
84
85 /* To minimize stack usage during recursion, some temporary variables
86    made global                                                        */
87 static WCHAR copyFrom[MAX_PATH];
88 static WCHAR copyTo[MAX_PATH];
89
90
91 /* =========================================================================
92    main - Main entrypoint for the xcopy command
93
94      Processes the args, and drives the actual copying
95    ========================================================================= */
96 int wmain (int argc, WCHAR *argvW[])
97 {
98     int     rc = 0;
99     WCHAR   suppliedsource[MAX_PATH] = {0};   /* As supplied on the cmd line */
100     WCHAR   supplieddestination[MAX_PATH] = {0};
101     WCHAR   sourcestem[MAX_PATH] = {0};       /* Stem of source          */
102     WCHAR   sourcespec[MAX_PATH] = {0};       /* Filespec of source      */
103     WCHAR   destinationstem[MAX_PATH] = {0};  /* Stem of destination     */
104     WCHAR   destinationspec[MAX_PATH] = {0};  /* Filespec of destination */
105     WCHAR   copyCmd[MAXSTRING];               /* COPYCMD env var         */
106     DWORD   flags = 0;                        /* Option flags            */
107     const WCHAR PROMPTSTR1[]  = {'/', 'Y', 0};
108     const WCHAR PROMPTSTR2[]  = {'/', 'y', 0};
109     const WCHAR COPYCMD[]  = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
110     const WCHAR EXCLUDE[]  = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
111
112     /*
113      * Parse the command line
114      */
115
116     /* Confirm at least one parameter */
117     if (argc < 2) {
118         XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
119         return RC_INITERROR;
120     }
121
122     /* Preinitialize flags based on COPYCMD */
123     if (GetEnvironmentVariable(COPYCMD, copyCmd, MAXSTRING)) {
124         if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
125             wcsstr(copyCmd, PROMPTSTR2) != NULL) {
126             flags |= OPT_NOPROMPT;
127         }
128     }
129
130     /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
131        wine, but on windows these can be normal files. At least one installer
132        uses files such as .packlist and (validly) expects them to be copied.
133        Under wine, if we do not copy hidden files by default then they get
134        lose                                                                   */
135     flags |= OPT_COPYHIDSYS;
136
137     /* Skip first arg, which is the program name */
138     argvW++;
139
140     while (argc > 1)
141     {
142         argc--;
143         WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(*argvW));
144
145         /* First non-switch parameter is source, second is destination */
146         if (*argvW[0] != '/') {
147             if (suppliedsource[0] == 0x00) {
148                 lstrcpyW(suppliedsource, *argvW);
149             } else if (supplieddestination[0] == 0x00) {
150                 lstrcpyW(supplieddestination, *argvW);
151             } else {
152                 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
153                 return RC_INITERROR;
154             }
155         } else {
156             /* Process all the switch options
157                  Note: Windows docs say /P prompts when dest is created
158                        but tests show it is done for each src file
159                        regardless of the destination                   */
160             switch (toupper(argvW[0][1])) {
161             case 'I': flags |= OPT_ASSUMEDIR;     break;
162             case 'S': flags |= OPT_RECURSIVE;     break;
163             case 'Q': flags |= OPT_QUIET;         break;
164             case 'F': flags |= OPT_FULL;          break;
165             case 'L': flags |= OPT_SIMULATE;      break;
166             case 'W': flags |= OPT_PAUSE;         break;
167             case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
168             case 'Y': flags |= OPT_NOPROMPT;      break;
169             case 'N': flags |= OPT_SHORTNAME;     break;
170             case 'U': flags |= OPT_MUSTEXIST;     break;
171             case 'R': flags |= OPT_REPLACEREAD;   break;
172             case 'H': flags |= OPT_COPYHIDSYS;    break;
173             case 'C': flags |= OPT_IGNOREERRORS;  break;
174             case 'P': flags |= OPT_SRCPROMPT;     break;
175             case 'A': flags |= OPT_ARCHIVEONLY;   break;
176             case 'M': flags |= OPT_ARCHIVEONLY |
177                                OPT_REMOVEARCH;    break;
178
179             /* E can be /E or /EXCLUDE */
180             case 'E': if (CompareString (LOCALE_USER_DEFAULT,
181                                          NORM_IGNORECASE | SORT_STRINGSORT,
182                                          &argvW[0][1], 8,
183                                          EXCLUDE, -1) == 2) {
184                         if (XCOPY_ProcessExcludeList(&argvW[0][9])) {
185                           XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
186                           return RC_INITERROR;
187                         } else flags |= OPT_EXCLUDELIST;
188                       } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
189                       break;
190
191             /* D can be /D or /D: */
192             case 'D': if ((argvW[0][2])==':' && isdigit(argvW[0][3])) {
193                           SYSTEMTIME st;
194                           WCHAR     *pos = &argvW[0][3];
195                           BOOL       isError = FALSE;
196                           memset(&st, 0x00, sizeof(st));
197
198                           /* Parse the arg : Month */
199                           st.wMonth = _wtol(pos);
200                           while (*pos && isdigit(*pos)) pos++;
201                           if (*pos++ != '-') isError = TRUE;
202
203                           /* Parse the arg : Day */
204                           if (!isError) {
205                               st.wDay = _wtol(pos);
206                               while (*pos && isdigit(*pos)) pos++;
207                               if (*pos++ != '-') isError = TRUE;
208                           }
209
210                           /* Parse the arg : Day */
211                           if (!isError) {
212                               st.wYear = _wtol(pos);
213                               if (st.wYear < 100) st.wYear+=2000;
214                           }
215
216                           if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
217                               SYSTEMTIME st;
218                               WCHAR datestring[32], timestring[32];
219
220                               flags |= OPT_DATERANGE;
221
222                               /* Debug info: */
223                               FileTimeToSystemTime (&dateRange, &st);
224                               GetDateFormat (0, DATE_SHORTDATE, &st, NULL, datestring,
225                                           sizeof(datestring));
226                               GetTimeFormat (0, TIME_NOSECONDS, &st,
227                                           NULL, timestring, sizeof(timestring));
228
229                               WINE_TRACE("Date being used is: %s %s\n",
230                                          wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
231                           } else {
232                               XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
233                               return RC_INITERROR;
234                           }
235                       } else {
236                           flags |= OPT_DATENEWER;
237                       }
238                       break;
239
240             case '-': if (toupper(argvW[0][2])=='Y')
241                           flags &= ~OPT_NOPROMPT; break;
242             case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
243                       return RC_OK;
244             default:
245                 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(*argvW));
246                 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), *argvW);
247                 return RC_INITERROR;
248             }
249         }
250         argvW++;
251     }
252
253     /* Default the destination if not supplied */
254     if (supplieddestination[0] == 0x00)
255         lstrcpyW(supplieddestination, wchr_dot);
256
257     /* Trace out the supplied information */
258     WINE_TRACE("Supplied parameters:\n");
259     WINE_TRACE("Source      : '%s'\n", wine_dbgstr_w(suppliedsource));
260     WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
261
262     /* Extract required information from source specification */
263     rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
264
265     /* Extract required information from destination specification */
266     rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
267                                destinationspec, sourcespec, flags);
268
269     /* Trace out the resulting information */
270     WINE_TRACE("Resolved parameters:\n");
271     WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
272     WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
273     WINE_TRACE("Dest   Stem : '%s'\n", wine_dbgstr_w(destinationstem));
274     WINE_TRACE("Dest   Spec : '%s'\n", wine_dbgstr_w(destinationspec));
275
276     /* Pause if necessary */
277     if (flags & OPT_PAUSE) {
278         DWORD count;
279         char pausestr[10];
280
281         XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
282         ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
283                   &count, NULL);
284     }
285
286     /* Now do the hard work... */
287     rc = XCOPY_DoCopy(sourcestem, sourcespec,
288                 destinationstem, destinationspec,
289                 flags);
290
291     /* Clear up exclude list allocated memory */
292     while (excludeList) {
293         EXCLUDELIST *pos = excludeList;
294         excludeList = excludeList -> next;
295         HeapFree(GetProcessHeap(), 0, pos->name);
296         HeapFree(GetProcessHeap(), 0, pos);
297     }
298
299     /* Finished - print trailer and exit */
300     if (flags & OPT_SIMULATE) {
301         XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
302     } else if (!(flags & OPT_NOCOPY)) {
303         XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
304     }
305     if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
306     return rc;
307
308 }
309
310
311 /* =========================================================================
312    XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
313      converts it into a stem and a filespec
314    ========================================================================= */
315 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
316                                    WCHAR *spec, DWORD flags)
317 {
318     WCHAR             actualsource[MAX_PATH];
319     WCHAR            *starPos;
320     WCHAR            *questPos;
321     DWORD             attribs;
322
323     /*
324      * Validate the source, expanding to full path ensuring it exists
325      */
326     if (GetFullPathName(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
327         WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
328         return RC_INITERROR;
329     }
330
331     /* If full names required, convert to using the full path */
332     if (flags & OPT_FULL) {
333         lstrcpyW(suppliedsource, actualsource);
334     }
335
336     /*
337      * Work out the stem of the source
338      */
339
340     /* If a directory is supplied, use that as-is (either fully or
341           partially qualified)
342        If a filename is supplied + a directory or drive path, use that
343           as-is
344        Otherwise
345           If no directory or path specified, add eg. C:
346           stem is Drive/Directory is bit up to last \ (or first :)
347           spec is bit after that                                         */
348
349     starPos = wcschr(suppliedsource, '*');
350     questPos = wcschr(suppliedsource, '?');
351     if (starPos || questPos) {
352         attribs = 0x00;  /* Ensures skips invalid or directory check below */
353     } else {
354         attribs = GetFileAttributes(actualsource);
355     }
356
357     if (attribs == INVALID_FILE_ATTRIBUTES) {
358         XCOPY_FailMessage(GetLastError());
359         return RC_INITERROR;
360
361     /* Directory:
362          stem should be exactly as supplied plus a '\', unless it was
363           eg. C: in which case no slash required */
364     } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
365         WCHAR lastChar;
366
367         WINE_TRACE("Directory supplied\n");
368         lstrcpyW(stem, suppliedsource);
369         lastChar = stem[lstrlenW(stem)-1];
370         if (lastChar != '\\' && lastChar != ':') {
371             lstrcatW(stem, wchr_slash);
372         }
373         lstrcpyW(spec, wchr_star);
374
375     /* File or wildcard search:
376          stem should be:
377            Up to and including last slash if directory path supplied
378            If c:filename supplied, just the c:
379            Otherwise stem should be the current drive letter + ':' */
380     } else {
381         WCHAR *lastDir;
382
383         WINE_TRACE("Filename supplied\n");
384         lastDir   = wcsrchr(suppliedsource, '\\');
385
386         if (lastDir) {
387             lstrcpyW(stem, suppliedsource);
388             stem[(lastDir-suppliedsource) + 1] = 0x00;
389             lstrcpyW(spec, (lastDir+1));
390         } else if (suppliedsource[1] == ':') {
391             lstrcpyW(stem, suppliedsource);
392             stem[2] = 0x00;
393             lstrcpyW(spec, suppliedsource+2);
394         } else {
395             WCHAR curdir[MAXSTRING];
396             GetCurrentDirectory (sizeof(curdir), curdir);
397             stem[0] = curdir[0];
398             stem[1] = curdir[1];
399             stem[2] = 0x00;
400             lstrcpyW(spec, suppliedsource);
401         }
402     }
403
404     return RC_OK;
405 }
406
407 /* =========================================================================
408    XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
409      converts it into a stem
410    ========================================================================= */
411 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
412                                  WCHAR *srcspec, DWORD flags)
413 {
414     WCHAR  actualdestination[MAX_PATH];
415     DWORD attribs;
416     BOOL isDir = FALSE;
417
418     /*
419      * Validate the source, expanding to full path ensuring it exists
420      */
421     if (GetFullPathName(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
422         WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
423         return RC_INITERROR;
424     }
425
426     /* Destination is either a directory or a file */
427     attribs = GetFileAttributes(actualdestination);
428
429     if (attribs == INVALID_FILE_ATTRIBUTES) {
430
431         /* If /I supplied and wildcard copy, assume directory */
432         if (flags & OPT_ASSUMEDIR &&
433             (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) {
434
435             isDir = TRUE;
436
437         } else {
438             DWORD count;
439             char  answer[10] = "";
440             WCHAR fileChar[2];
441             WCHAR dirChar[2];
442
443             /* Read the F and D characters from the resource file */
444             wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
445             wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
446
447             while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
448                 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
449
450                 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
451                 WINE_TRACE("User answer %c\n", answer[0]);
452
453                 answer[0] = toupper(answer[0]);
454             }
455
456             if (answer[0] == dirChar[0]) {
457                 isDir = TRUE;
458             } else {
459                 isDir = FALSE;
460             }
461         }
462     } else {
463         isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
464     }
465
466     if (isDir) {
467         lstrcpyW(stem, actualdestination);
468         *spec = 0x00;
469
470         /* Ensure ends with a '\' */
471         if (stem[lstrlenW(stem)-1] != '\\') {
472             lstrcatW(stem, wchr_slash);
473         }
474
475     } else {
476         WCHAR drive[MAX_PATH];
477         WCHAR dir[MAX_PATH];
478         WCHAR fname[MAX_PATH];
479         WCHAR ext[MAX_PATH];
480         _wsplitpath(actualdestination, drive, dir, fname, ext);
481         lstrcpyW(stem, drive);
482         lstrcatW(stem, dir);
483         lstrcpyW(spec, fname);
484         lstrcatW(spec, ext);
485     }
486     return RC_OK;
487 }
488
489 /* =========================================================================
490    XCOPY_DoCopy - Recursive function to copy files based on input parms
491      of a stem and a spec
492
493       This works by using FindFirstFile supplying the source stem and spec.
494       If results are found, any non-directory ones are processed
495       Then, if /S or /E is supplied, another search is made just for
496       directories, and this function is called again for that directory
497
498    ========================================================================= */
499 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
500                         WCHAR *deststem, WCHAR *destspec,
501                         DWORD flags)
502 {
503     WIN32_FIND_DATA *finddata;
504     HANDLE          h;
505     BOOL            findres = TRUE;
506     WCHAR           *inputpath, *outputpath;
507     BOOL            copiedFile = FALSE;
508     DWORD           destAttribs, srcAttribs;
509     BOOL            skipFile;
510
511     /* Allocate some working memory on heap to minimize footprint */
512     finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATA));
513     inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
514     outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
515
516     /* Build the search info into a single parm */
517     lstrcpyW(inputpath, srcstem);
518     lstrcatW(inputpath, srcspec);
519
520     /* Search 1 - Look for matching files */
521     h = FindFirstFile(inputpath, finddata);
522     while (h != INVALID_HANDLE_VALUE && findres) {
523
524         skipFile = FALSE;
525
526         /* Ignore . and .. */
527         if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
528             lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
529             finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
530
531             WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
532         } else {
533
534             /* Get the filename information */
535             lstrcpyW(copyFrom, srcstem);
536             if (flags & OPT_SHORTNAME) {
537               lstrcatW(copyFrom, finddata->cAlternateFileName);
538             } else {
539               lstrcatW(copyFrom, finddata->cFileName);
540             }
541
542             lstrcpyW(copyTo, deststem);
543             if (*destspec == 0x00) {
544                 if (flags & OPT_SHORTNAME) {
545                     lstrcatW(copyTo, finddata->cAlternateFileName);
546                 } else {
547                     lstrcatW(copyTo, finddata->cFileName);
548                 }
549             } else {
550                 lstrcatW(copyTo, destspec);
551             }
552
553             /* Do the copy */
554             WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
555                                                       wine_dbgstr_w(copyTo));
556             if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
557
558             /* See if allowed to copy it */
559             srcAttribs = GetFileAttributesW(copyFrom);
560             WINE_TRACE("Source attribs: %d\n", srcAttribs);
561
562             if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
563                 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
564
565                 if (!(flags & OPT_COPYHIDSYS)) {
566                     skipFile = TRUE;
567                 }
568             }
569
570             if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
571                 (flags & OPT_ARCHIVEONLY)) {
572                 skipFile = TRUE;
573             }
574
575             /* See if file exists */
576             destAttribs = GetFileAttributesW(copyTo);
577             WINE_TRACE("Dest attribs: %d\n", srcAttribs);
578
579             /* Check date ranges if a destination file already exists */
580             if (!skipFile && (flags & OPT_DATERANGE) &&
581                 (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
582                 WINE_TRACE("Skipping file as modified date too old\n");
583                 skipFile = TRUE;
584             }
585
586             /* If just /D supplied, only overwrite if src newer than dest */
587             if (!skipFile && (flags & OPT_DATENEWER) &&
588                (destAttribs != INVALID_FILE_ATTRIBUTES)) {
589                 HANDLE h = CreateFile(copyTo, GENERIC_READ, FILE_SHARE_READ,
590                                       NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
591                                       NULL);
592                 if (h != INVALID_HANDLE_VALUE) {
593                     FILETIME writeTime;
594                     GetFileTime(h, NULL, NULL, &writeTime);
595
596                     if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
597                         WINE_TRACE("Skipping file as dest newer or same date\n");
598                         skipFile = TRUE;
599                     }
600                     CloseHandle(h);
601                 }
602             }
603
604             /* See if exclude list provided. Note since filenames are case
605                insensitive, need to uppercase the filename before doing
606                strstr                                                     */
607             if (!skipFile && (flags & OPT_EXCLUDELIST)) {
608                 EXCLUDELIST *pos = excludeList;
609                 WCHAR copyFromUpper[MAX_PATH];
610
611                 /* Uppercase source filename */
612                 lstrcpyW(copyFromUpper, copyFrom);
613                 CharUpperBuff(copyFromUpper, lstrlenW(copyFromUpper));
614
615                 /* Loop through testing each exclude line */
616                 while (pos) {
617                     if (wcsstr(copyFromUpper, pos->name) != NULL) {
618                         WINE_TRACE("Skipping file as matches exclude '%s'\n",
619                                    wine_dbgstr_w(pos->name));
620                         skipFile = TRUE;
621                         pos = NULL;
622                     } else {
623                         pos = pos->next;
624                     }
625                 }
626             }
627
628             /* Prompt each file if necessary */
629             if (!skipFile && (flags & OPT_SRCPROMPT)) {
630                 DWORD count;
631                 char  answer[10];
632                 BOOL  answered = FALSE;
633                 WCHAR yesChar[2];
634                 WCHAR noChar[2];
635
636                 /* Read the Y and N characters from the resource file */
637                 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
638                 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
639
640                 while (!answered) {
641                     XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
642                     ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
643                               &count, NULL);
644
645                     answered = TRUE;
646                     if (toupper(answer[0]) == noChar[0])
647                         skipFile = TRUE;
648                     else if (toupper(answer[0]) != yesChar[0])
649                         answered = FALSE;
650                 }
651             }
652
653             if (!skipFile &&
654                 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
655                 DWORD count;
656                 char  answer[10];
657                 BOOL  answered = FALSE;
658                 WCHAR yesChar[2];
659                 WCHAR allChar[2];
660                 WCHAR noChar[2];
661
662                 /* Read the A,Y and N characters from the resource file */
663                 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
664                 wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
665                 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
666
667                 while (!answered) {
668                     XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
669                     ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
670                               &count, NULL);
671
672                     answered = TRUE;
673                     if (toupper(answer[0]) == allChar[0])
674                         flags |= OPT_NOPROMPT;
675                     else if (toupper(answer[0]) == noChar[0])
676                         skipFile = TRUE;
677                     else if (toupper(answer[0]) != yesChar[0])
678                         answered = FALSE;
679                 }
680             }
681
682             /* See if it has to exist! */
683             if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
684                 skipFile = TRUE;
685             }
686
687             /* Output a status message */
688             if (!skipFile) {
689                 if (flags & OPT_QUIET) {
690                     /* Skip message */
691                 } else if (flags & OPT_FULL) {
692                     const WCHAR infostr[]   = {'%', 's', ' ', '-', '>', ' ',
693                                                '%', 's', '\n', 0};
694
695                     XCOPY_wprintf(infostr, copyFrom, copyTo);
696                 } else {
697                     const WCHAR infostr[] = {'%', 's', '\n', 0};
698                     XCOPY_wprintf(infostr, copyFrom);
699                 }
700
701                 /* If allowing overwriting of read only files, remove any
702                    write protection                                       */
703                 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
704                     (flags & OPT_REPLACEREAD)) {
705                     SetFileAttributes(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
706                 }
707
708                 copiedFile = TRUE;
709                 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
710                     /* Skip copy */
711                 } else if (CopyFile(copyFrom, copyTo, FALSE) == 0) {
712
713                     DWORD error = GetLastError();
714                     XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
715                            copyFrom, copyTo, error);
716                     XCOPY_FailMessage(error);
717
718                     if (flags & OPT_IGNOREERRORS) {
719                         skipFile = TRUE;
720                     } else {
721                         return RC_WRITEERROR;
722                     }
723                 }
724
725                 /* If /M supplied, remove the archive bit after successful copy */
726                 if (!skipFile) {
727                     if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
728                         (flags & OPT_REMOVEARCH)) {
729                         SetFileAttributes(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
730                     }
731                     filesCopied++;
732                 }
733             }
734         }
735
736         /* Find next file */
737         findres = FindNextFile(h, finddata);
738     }
739     FindClose(h);
740
741     /* Search 2 - do subdirs */
742     if (flags & OPT_RECURSIVE) {
743         lstrcpyW(inputpath, srcstem);
744         lstrcatW(inputpath, wchr_star);
745         findres = TRUE;
746         WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
747
748         h = FindFirstFile(inputpath, finddata);
749         while (h != INVALID_HANDLE_VALUE && findres) {
750
751             /* Only looking for dirs */
752             if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
753                 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
754                 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
755
756                 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
757
758                 /* Make up recursive information */
759                 lstrcpyW(inputpath, srcstem);
760                 lstrcatW(inputpath, finddata->cFileName);
761                 lstrcatW(inputpath, wchr_slash);
762
763                 lstrcpyW(outputpath, deststem);
764                 if (*destspec == 0x00) {
765                     lstrcatW(outputpath, finddata->cFileName);
766
767                     /* If /E is supplied, create the directory now */
768                     if ((flags & OPT_EMPTYDIR) &&
769                         !(flags & OPT_SIMULATE))
770                         XCOPY_CreateDirectory(outputpath);
771
772                     lstrcatW(outputpath, wchr_slash);
773                 }
774
775                 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
776             }
777
778             /* Find next one */
779             findres = FindNextFile(h, finddata);
780         }
781     }
782
783     /* free up memory */
784     HeapFree(GetProcessHeap(), 0, finddata);
785     HeapFree(GetProcessHeap(), 0, inputpath);
786     HeapFree(GetProcessHeap(), 0, outputpath);
787
788     return 0;
789 }
790
791 /* =========================================================================
792  * Routine copied from cmd.exe md command -
793  * This works recursivly. so creating dir1\dir2\dir3 will create dir1 and
794  * dir2 if they do not already exist.
795  * ========================================================================= */
796 static BOOL XCOPY_CreateDirectory(const WCHAR* path)
797 {
798     int len;
799     WCHAR *new_path;
800     BOOL ret = TRUE;
801
802     new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
803     lstrcpyW(new_path,path);
804
805     while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
806         new_path[len - 1] = 0;
807
808     while (!CreateDirectory(new_path,NULL))
809     {
810         WCHAR *slash;
811         DWORD last_error = GetLastError();
812         if (last_error == ERROR_ALREADY_EXISTS)
813             break;
814
815         if (last_error != ERROR_PATH_NOT_FOUND)
816         {
817             ret = FALSE;
818             break;
819         }
820
821         if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
822         {
823             ret = FALSE;
824             break;
825         }
826
827         len = slash - new_path;
828         new_path[len] = 0;
829         if (!XCOPY_CreateDirectory(new_path))
830         {
831             ret = FALSE;
832             break;
833         }
834         new_path[len] = '\\';
835     }
836     HeapFree(GetProcessHeap(),0,new_path);
837     return ret;
838 }
839
840 /* =========================================================================
841  * Process the /EXCLUDE: file list, building up a list of substrings to
842  * avoid copying
843  * Returns TRUE on any failure
844  * ========================================================================= */
845 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {
846
847     WCHAR *filenameStart = parms;
848
849     WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
850     excludeList = NULL;
851
852     while (*parms && *parms != ' ' && *parms != '/') {
853
854         /* If found '+' then process the file found so far */
855         if (*parms == '+') {
856             if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
857                 return TRUE;
858             }
859             filenameStart = parms+1;
860         }
861         parms++;
862     }
863
864     if (filenameStart != parms) {
865         if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
866             return TRUE;
867         }
868     }
869
870     return FALSE;
871 }
872
873 /* =========================================================================
874  * Process a single file from the /EXCLUDE: file list, building up a list
875  * of substrings to avoid copying
876  * Returns TRUE on any failure
877  * ========================================================================= */
878 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName) {
879
880     WCHAR   endChar = *endOfName;
881     WCHAR   buffer[MAXSTRING];
882     FILE   *inFile  = NULL;
883     const WCHAR readTextMode[]  = {'r', 't', 0};
884
885     /* Null terminate the filename (temporarily updates the filename hence
886          parms not const)                                                 */
887     *endOfName = 0x00;
888
889     /* Open the file */
890     inFile = _wfopen(filename, readTextMode);
891     if (inFile == NULL) {
892         XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
893         *endOfName = endChar;
894         return TRUE;
895     }
896
897     /* Process line by line */
898     while (fgetws(buffer, sizeof(buffer), inFile) != NULL) {
899         EXCLUDELIST *thisEntry;
900         int length = lstrlenW(buffer);
901
902         /* Strip CRLF */
903         buffer[length-1] = 0x00;
904
905         /* If more than CRLF */
906         if (length > 1) {
907           thisEntry = HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST));
908           thisEntry->next = excludeList;
909           excludeList = thisEntry;
910           thisEntry->name = HeapAlloc(GetProcessHeap(), 0,
911                                       (length * sizeof(WCHAR))+1);
912           lstrcpyW(thisEntry->name, buffer);
913           CharUpperBuff(thisEntry->name, length);
914           WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry->name));
915         }
916     }
917
918     /* See if EOF or error occurred */
919     if (!feof(inFile)) {
920         XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
921         *endOfName = endChar;
922         return TRUE;
923     }
924
925     /* Revert the input string to original form, and cleanup + return */
926     *endOfName = endChar;
927     fclose(inFile);
928     return FALSE;
929 }
930
931 /* =========================================================================
932  * Load a string from the resource file, handling any error
933  * Returns string retrieved from resource file
934  * ========================================================================= */
935 static WCHAR *XCOPY_LoadMessage(UINT id) {
936     static WCHAR msg[MAXSTRING];
937     const WCHAR failedMsg[]  = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
938
939     if (!LoadString(GetModuleHandle(NULL), id, msg, sizeof(msg))) {
940        WINE_FIXME("LoadString failed with %d\n", GetLastError());
941        lstrcpyW(msg, failedMsg);
942     }
943     return msg;
944 }
945
946 /* =========================================================================
947  * Load a string for a system error and writes it to the screen
948  * Returns string retrieved from resource file
949  * ========================================================================= */
950 static void XCOPY_FailMessage(DWORD err) {
951     LPWSTR lpMsgBuf;
952     int status;
953
954     status = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER |
955                             FORMAT_MESSAGE_FROM_SYSTEM,
956                             NULL, err, 0,
957                             (LPTSTR) &lpMsgBuf, 0, NULL);
958     if (!status) {
959       WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
960                  err, GetLastError());
961     } else {
962       const WCHAR infostr[] = {'%', 's', '\n', 0};
963       XCOPY_wprintf(infostr, lpMsgBuf);
964       LocalFree ((HLOCAL)lpMsgBuf);
965     }
966 }
967
968 /* =========================================================================
969  * Output a formatted unicode string. Ideally this will go to the console
970  *  and hence required WriteConsoleW to output it, however if file i/o is
971  *  redirected, it needs to be WriteFile'd using OEM (not ANSI) format
972  * ========================================================================= */
973 int XCOPY_wprintf(const WCHAR *format, ...) {
974
975     static WCHAR *output_bufW = NULL;
976     static char  *output_bufA = NULL;
977     static BOOL  toConsole    = TRUE;
978     static BOOL  traceOutput  = FALSE;
979 #define MAX_WRITECONSOLE_SIZE 65535
980
981     va_list parms;
982     DWORD   len, nOut;
983     DWORD   res = 0;
984
985     /*
986      * Allocate buffer to use when writing to console
987      * Note: Not freed - memory will be allocated once and released when
988      *         xcopy ends
989      */
990
991     if (!output_bufW) output_bufW = HeapAlloc(GetProcessHeap(), 0,
992                                               MAX_WRITECONSOLE_SIZE);
993     if (!output_bufW) {
994       WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
995       return 0;
996     }
997
998     /* Use wvsprintf to store output into unicode buffer */
999     va_start(parms, format);
1000     len = vswprintf(output_bufW, format, parms);
1001     va_end(parms);
1002
1003     /* Try to write as unicode all the time we think its a console */
1004     if (toConsole) {
1005       res = WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),
1006                           output_bufW, len, &nOut, NULL);
1007     }
1008
1009     /* If writing to console has failed (ever) we assume its file
1010        i/o so convert to OEM codepage and output                  */
1011     if (!res) {
1012       BOOL usedDefaultChar = FALSE;
1013       DWORD convertedChars;
1014
1015       toConsole = FALSE;
1016
1017       /*
1018        * Allocate buffer to use when writing to file. Not freed, as above
1019        */
1020       if (!output_bufA) output_bufA = HeapAlloc(GetProcessHeap(), 0,
1021                                                 MAX_WRITECONSOLE_SIZE);
1022       if (!output_bufA) {
1023         WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
1024         return 0;
1025       }
1026
1027       /* Convert to OEM, then output */
1028       convertedChars = WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW,
1029                           len, output_bufA, MAX_WRITECONSOLE_SIZE,
1030                           "?", &usedDefaultChar);
1031       WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), output_bufA, convertedChars,
1032                 &nOut, FALSE);
1033     }
1034
1035     /* Trace whether screen or console */
1036     if (!traceOutput) {
1037       WINE_TRACE("Writing to console? (%d)\n", toConsole);
1038       traceOutput = TRUE;
1039     }
1040     return nOut;
1041 }