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 }
1142