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