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