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