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