winecfg: Remove spaces before '\n' in resource strings.
[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     int             ret = 0;
511
512     /* Allocate some working memory on heap to minimize footprint */
513     finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATA));
514     inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
515     outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
516
517     /* Build the search info into a single parm */
518     lstrcpyW(inputpath, srcstem);
519     lstrcatW(inputpath, srcspec);
520
521     /* Search 1 - Look for matching files */
522     h = FindFirstFile(inputpath, finddata);
523     while (h != INVALID_HANDLE_VALUE && findres) {
524
525         skipFile = FALSE;
526
527         /* Ignore . and .. */
528         if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
529             lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
530             finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
531
532             WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
533         } else {
534
535             /* Get the filename information */
536             lstrcpyW(copyFrom, srcstem);
537             if (flags & OPT_SHORTNAME) {
538               lstrcatW(copyFrom, finddata->cAlternateFileName);
539             } else {
540               lstrcatW(copyFrom, finddata->cFileName);
541             }
542
543             lstrcpyW(copyTo, deststem);
544             if (*destspec == 0x00) {
545                 if (flags & OPT_SHORTNAME) {
546                     lstrcatW(copyTo, finddata->cAlternateFileName);
547                 } else {
548                     lstrcatW(copyTo, finddata->cFileName);
549                 }
550             } else {
551                 lstrcatW(copyTo, destspec);
552             }
553
554             /* Do the copy */
555             WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
556                                                       wine_dbgstr_w(copyTo));
557             if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
558
559             /* See if allowed to copy it */
560             srcAttribs = GetFileAttributesW(copyFrom);
561             WINE_TRACE("Source attribs: %d\n", srcAttribs);
562
563             if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
564                 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
565
566                 if (!(flags & OPT_COPYHIDSYS)) {
567                     skipFile = TRUE;
568                 }
569             }
570
571             if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
572                 (flags & OPT_ARCHIVEONLY)) {
573                 skipFile = TRUE;
574             }
575
576             /* See if file exists */
577             destAttribs = GetFileAttributesW(copyTo);
578             WINE_TRACE("Dest attribs: %d\n", srcAttribs);
579
580             /* Check date ranges if a destination file already exists */
581             if (!skipFile && (flags & OPT_DATERANGE) &&
582                 (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
583                 WINE_TRACE("Skipping file as modified date too old\n");
584                 skipFile = TRUE;
585             }
586
587             /* If just /D supplied, only overwrite if src newer than dest */
588             if (!skipFile && (flags & OPT_DATENEWER) &&
589                (destAttribs != INVALID_FILE_ATTRIBUTES)) {
590                 HANDLE h = CreateFile(copyTo, GENERIC_READ, FILE_SHARE_READ,
591                                       NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
592                                       NULL);
593                 if (h != INVALID_HANDLE_VALUE) {
594                     FILETIME writeTime;
595                     GetFileTime(h, NULL, NULL, &writeTime);
596
597                     if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
598                         WINE_TRACE("Skipping file as dest newer or same date\n");
599                         skipFile = TRUE;
600                     }
601                     CloseHandle(h);
602                 }
603             }
604
605             /* See if exclude list provided. Note since filenames are case
606                insensitive, need to uppercase the filename before doing
607                strstr                                                     */
608             if (!skipFile && (flags & OPT_EXCLUDELIST)) {
609                 EXCLUDELIST *pos = excludeList;
610                 WCHAR copyFromUpper[MAX_PATH];
611
612                 /* Uppercase source filename */
613                 lstrcpyW(copyFromUpper, copyFrom);
614                 CharUpperBuff(copyFromUpper, lstrlenW(copyFromUpper));
615
616                 /* Loop through testing each exclude line */
617                 while (pos) {
618                     if (wcsstr(copyFromUpper, pos->name) != NULL) {
619                         WINE_TRACE("Skipping file as matches exclude '%s'\n",
620                                    wine_dbgstr_w(pos->name));
621                         skipFile = TRUE;
622                         pos = NULL;
623                     } else {
624                         pos = pos->next;
625                     }
626                 }
627             }
628
629             /* Prompt each file if necessary */
630             if (!skipFile && (flags & OPT_SRCPROMPT)) {
631                 DWORD count;
632                 char  answer[10];
633                 BOOL  answered = FALSE;
634                 WCHAR yesChar[2];
635                 WCHAR noChar[2];
636
637                 /* Read the Y and N characters from the resource file */
638                 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
639                 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
640
641                 while (!answered) {
642                     XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
643                     ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
644                               &count, NULL);
645
646                     answered = TRUE;
647                     if (toupper(answer[0]) == noChar[0])
648                         skipFile = TRUE;
649                     else if (toupper(answer[0]) != yesChar[0])
650                         answered = FALSE;
651                 }
652             }
653
654             if (!skipFile &&
655                 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
656                 DWORD count;
657                 char  answer[10];
658                 BOOL  answered = FALSE;
659                 WCHAR yesChar[2];
660                 WCHAR allChar[2];
661                 WCHAR noChar[2];
662
663                 /* Read the A,Y and N characters from the resource file */
664                 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
665                 wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
666                 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
667
668                 while (!answered) {
669                     XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
670                     ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
671                               &count, NULL);
672
673                     answered = TRUE;
674                     if (toupper(answer[0]) == allChar[0])
675                         flags |= OPT_NOPROMPT;
676                     else if (toupper(answer[0]) == noChar[0])
677                         skipFile = TRUE;
678                     else if (toupper(answer[0]) != yesChar[0])
679                         answered = FALSE;
680                 }
681             }
682
683             /* See if it has to exist! */
684             if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
685                 skipFile = TRUE;
686             }
687
688             /* Output a status message */
689             if (!skipFile) {
690                 if (flags & OPT_QUIET) {
691                     /* Skip message */
692                 } else if (flags & OPT_FULL) {
693                     const WCHAR infostr[]   = {'%', 's', ' ', '-', '>', ' ',
694                                                '%', 's', '\n', 0};
695
696                     XCOPY_wprintf(infostr, copyFrom, copyTo);
697                 } else {
698                     const WCHAR infostr[] = {'%', 's', '\n', 0};
699                     XCOPY_wprintf(infostr, copyFrom);
700                 }
701
702                 /* If allowing overwriting of read only files, remove any
703                    write protection                                       */
704                 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
705                     (flags & OPT_REPLACEREAD)) {
706                     SetFileAttributes(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
707                 }
708
709                 copiedFile = TRUE;
710                 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
711                     /* Skip copy */
712                 } else if (CopyFile(copyFrom, copyTo, FALSE) == 0) {
713
714                     DWORD error = GetLastError();
715                     XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
716                            copyFrom, copyTo, error);
717                     XCOPY_FailMessage(error);
718
719                     if (flags & OPT_IGNOREERRORS) {
720                         skipFile = TRUE;
721                     } else {
722                         ret = RC_WRITEERROR;
723                         goto cleanup;
724                     }
725                 }
726
727                 /* If /M supplied, remove the archive bit after successful copy */
728                 if (!skipFile) {
729                     if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
730                         (flags & OPT_REMOVEARCH)) {
731                         SetFileAttributes(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
732                     }
733                     filesCopied++;
734                 }
735             }
736         }
737
738         /* Find next file */
739         findres = FindNextFile(h, finddata);
740     }
741     FindClose(h);
742
743     /* Search 2 - do subdirs */
744     if (flags & OPT_RECURSIVE) {
745         lstrcpyW(inputpath, srcstem);
746         lstrcatW(inputpath, wchr_star);
747         findres = TRUE;
748         WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
749
750         h = FindFirstFile(inputpath, finddata);
751         while (h != INVALID_HANDLE_VALUE && findres) {
752
753             /* Only looking for dirs */
754             if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
755                 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
756                 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
757
758                 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
759
760                 /* Make up recursive information */
761                 lstrcpyW(inputpath, srcstem);
762                 lstrcatW(inputpath, finddata->cFileName);
763                 lstrcatW(inputpath, wchr_slash);
764
765                 lstrcpyW(outputpath, deststem);
766                 if (*destspec == 0x00) {
767                     lstrcatW(outputpath, finddata->cFileName);
768
769                     /* If /E is supplied, create the directory now */
770                     if ((flags & OPT_EMPTYDIR) &&
771                         !(flags & OPT_SIMULATE))
772                         XCOPY_CreateDirectory(outputpath);
773
774                     lstrcatW(outputpath, wchr_slash);
775                 }
776
777                 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
778             }
779
780             /* Find next one */
781             findres = FindNextFile(h, finddata);
782         }
783     }
784
785 cleanup:
786
787     /* free up memory */
788     HeapFree(GetProcessHeap(), 0, finddata);
789     HeapFree(GetProcessHeap(), 0, inputpath);
790     HeapFree(GetProcessHeap(), 0, outputpath);
791
792     return ret;
793 }
794
795 /* =========================================================================
796  * Routine copied from cmd.exe md command -
797  * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
798  * dir2 if they do not already exist.
799  * ========================================================================= */
800 static BOOL XCOPY_CreateDirectory(const WCHAR* path)
801 {
802     int len;
803     WCHAR *new_path;
804     BOOL ret = TRUE;
805
806     new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
807     lstrcpyW(new_path,path);
808
809     while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
810         new_path[len - 1] = 0;
811
812     while (!CreateDirectory(new_path,NULL))
813     {
814         WCHAR *slash;
815         DWORD last_error = GetLastError();
816         if (last_error == ERROR_ALREADY_EXISTS)
817             break;
818
819         if (last_error != ERROR_PATH_NOT_FOUND)
820         {
821             ret = FALSE;
822             break;
823         }
824
825         if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
826         {
827             ret = FALSE;
828             break;
829         }
830
831         len = slash - new_path;
832         new_path[len] = 0;
833         if (!XCOPY_CreateDirectory(new_path))
834         {
835             ret = FALSE;
836             break;
837         }
838         new_path[len] = '\\';
839     }
840     HeapFree(GetProcessHeap(),0,new_path);
841     return ret;
842 }
843
844 /* =========================================================================
845  * Process the /EXCLUDE: file list, building up a list of substrings to
846  * avoid copying
847  * Returns TRUE on any failure
848  * ========================================================================= */
849 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {
850
851     WCHAR *filenameStart = parms;
852
853     WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
854     excludeList = NULL;
855
856     while (*parms && *parms != ' ' && *parms != '/') {
857
858         /* If found '+' then process the file found so far */
859         if (*parms == '+') {
860             if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
861                 return TRUE;
862             }
863             filenameStart = parms+1;
864         }
865         parms++;
866     }
867
868     if (filenameStart != parms) {
869         if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
870             return TRUE;
871         }
872     }
873
874     return FALSE;
875 }
876
877 /* =========================================================================
878  * Process a single file from the /EXCLUDE: file list, building up a list
879  * of substrings to avoid copying
880  * Returns TRUE on any failure
881  * ========================================================================= */
882 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName) {
883
884     WCHAR   endChar = *endOfName;
885     WCHAR   buffer[MAXSTRING];
886     FILE   *inFile  = NULL;
887     const WCHAR readTextMode[]  = {'r', 't', 0};
888
889     /* Null terminate the filename (temporarily updates the filename hence
890          parms not const)                                                 */
891     *endOfName = 0x00;
892
893     /* Open the file */
894     inFile = _wfopen(filename, readTextMode);
895     if (inFile == NULL) {
896         XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
897         *endOfName = endChar;
898         return TRUE;
899     }
900
901     /* Process line by line */
902     while (fgetws(buffer, sizeof(buffer), inFile) != NULL) {
903         EXCLUDELIST *thisEntry;
904         int length = lstrlenW(buffer);
905
906         /* Strip CRLF */
907         buffer[length-1] = 0x00;
908
909         /* If more than CRLF */
910         if (length > 1) {
911           thisEntry = HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST));
912           thisEntry->next = excludeList;
913           excludeList = thisEntry;
914           thisEntry->name = HeapAlloc(GetProcessHeap(), 0,
915                                       (length * sizeof(WCHAR))+1);
916           lstrcpyW(thisEntry->name, buffer);
917           CharUpperBuff(thisEntry->name, length);
918           WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry->name));
919         }
920     }
921
922     /* See if EOF or error occurred */
923     if (!feof(inFile)) {
924         XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
925         *endOfName = endChar;
926         return TRUE;
927     }
928
929     /* Revert the input string to original form, and cleanup + return */
930     *endOfName = endChar;
931     fclose(inFile);
932     return FALSE;
933 }
934
935 /* =========================================================================
936  * Load a string from the resource file, handling any error
937  * Returns string retrieved from resource file
938  * ========================================================================= */
939 static WCHAR *XCOPY_LoadMessage(UINT id) {
940     static WCHAR msg[MAXSTRING];
941     const WCHAR failedMsg[]  = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
942
943     if (!LoadString(GetModuleHandle(NULL), id, msg, sizeof(msg))) {
944        WINE_FIXME("LoadString failed with %d\n", GetLastError());
945        lstrcpyW(msg, failedMsg);
946     }
947     return msg;
948 }
949
950 /* =========================================================================
951  * Load a string for a system error and writes it to the screen
952  * Returns string retrieved from resource file
953  * ========================================================================= */
954 static void XCOPY_FailMessage(DWORD err) {
955     LPWSTR lpMsgBuf;
956     int status;
957
958     status = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER |
959                             FORMAT_MESSAGE_FROM_SYSTEM,
960                             NULL, err, 0,
961                             (LPTSTR) &lpMsgBuf, 0, NULL);
962     if (!status) {
963       WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
964                  err, GetLastError());
965     } else {
966       const WCHAR infostr[] = {'%', 's', '\n', 0};
967       XCOPY_wprintf(infostr, lpMsgBuf);
968       LocalFree ((HLOCAL)lpMsgBuf);
969     }
970 }
971
972 /* =========================================================================
973  * Output a formatted unicode string. Ideally this will go to the console
974  *  and hence required WriteConsoleW to output it, however if file i/o is
975  *  redirected, it needs to be WriteFile'd using OEM (not ANSI) format
976  * ========================================================================= */
977 int XCOPY_wprintf(const WCHAR *format, ...) {
978
979     static WCHAR *output_bufW = NULL;
980     static char  *output_bufA = NULL;
981     static BOOL  toConsole    = TRUE;
982     static BOOL  traceOutput  = FALSE;
983 #define MAX_WRITECONSOLE_SIZE 65535
984
985     va_list parms;
986     DWORD   len, nOut;
987     DWORD   res = 0;
988
989     /*
990      * Allocate buffer to use when writing to console
991      * Note: Not freed - memory will be allocated once and released when
992      *         xcopy ends
993      */
994
995     if (!output_bufW) output_bufW = HeapAlloc(GetProcessHeap(), 0,
996                                               MAX_WRITECONSOLE_SIZE);
997     if (!output_bufW) {
998       WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
999       return 0;
1000     }
1001
1002     /* Use wvsprintf to store output into unicode buffer */
1003     va_start(parms, format);
1004     len = vswprintf(output_bufW, format, parms);
1005     va_end(parms);
1006
1007     /* Try to write as unicode all the time we think its a console */
1008     if (toConsole) {
1009       res = WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),
1010                           output_bufW, len, &nOut, NULL);
1011     }
1012
1013     /* If writing to console has failed (ever) we assume its file
1014        i/o so convert to OEM codepage and output                  */
1015     if (!res) {
1016       BOOL usedDefaultChar = FALSE;
1017       DWORD convertedChars;
1018
1019       toConsole = FALSE;
1020
1021       /*
1022        * Allocate buffer to use when writing to file. Not freed, as above
1023        */
1024       if (!output_bufA) output_bufA = HeapAlloc(GetProcessHeap(), 0,
1025                                                 MAX_WRITECONSOLE_SIZE);
1026       if (!output_bufA) {
1027         WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
1028         return 0;
1029       }
1030
1031       /* Convert to OEM, then output */
1032       convertedChars = WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW,
1033                           len, output_bufA, MAX_WRITECONSOLE_SIZE,
1034                           "?", &usedDefaultChar);
1035       WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), output_bufA, convertedChars,
1036                 &nOut, FALSE);
1037     }
1038
1039     /* Trace whether screen or console */
1040     if (!traceOutput) {
1041       WINE_TRACE("Writing to console? (%d)\n", toConsole);
1042       traceOutput = TRUE;
1043     }
1044     return nOut;
1045 }