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