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