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