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