dwrite: Implement GetWeight() for IDWriteFont.
[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     }
631
632 cleanup:
633
634     /* free up memory */
635     HeapFree(GetProcessHeap(), 0, finddata);
636     HeapFree(GetProcessHeap(), 0, inputpath);
637     HeapFree(GetProcessHeap(), 0, outputpath);
638
639     return ret;
640 }
641
642
643 /* =========================================================================
644    XCOPY_ParseCommandLine - Parses the command line
645    ========================================================================= */
646 static BOOL is_whitespace(WCHAR c)
647 {
648     return c == ' ' || c == '\t';
649 }
650
651 static WCHAR *skip_whitespace(WCHAR *p)
652 {
653     for (; *p && is_whitespace(*p); p++);
654     return p;
655 }
656
657 /* Windows XCOPY uses a simplified command line parsing algorithm
658    that lacks the escaped-quote logic of build_argv(), because
659    literal double quotes are illegal in any of its arguments.
660    Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
661 static int find_end_of_word(const WCHAR *word, WCHAR **end)
662 {
663     BOOL in_quotes = 0;
664     const WCHAR *ptr = word;
665     for (;;) {
666         for (; *ptr != '\0' && *ptr != '"' &&
667                  (in_quotes || !is_whitespace(*ptr)); ptr++);
668         if (*ptr == '"') {
669             in_quotes = !in_quotes;
670             ptr++;
671         }
672         /* Odd number of double quotes is illegal for XCOPY */
673         if (in_quotes && *ptr == '\0')
674             return RC_INITERROR;
675         if (*ptr == '\0' || (!in_quotes && is_whitespace(*ptr)))
676             break;
677     }
678     *end = (WCHAR*)ptr;
679     return RC_OK;
680 }
681
682 /* Remove all double quotes from a word */
683 static void strip_quotes(WCHAR *word, WCHAR **end)
684 {
685     WCHAR *rp, *wp;
686     for (rp = word, wp = word; *rp != '\0'; rp++) {
687         if (*rp == '"')
688             continue;
689         if (wp < rp)
690             *wp = *rp;
691         wp++;
692     }
693     *wp = '\0';
694     *end = wp;
695 }
696
697 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource,
698                                   WCHAR *supplieddestination, DWORD *pflags)
699 {
700     const WCHAR EXCLUDE[]  = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
701     DWORD flags = *pflags;
702     WCHAR *cmdline, *word, *end, *next;
703     int rc = RC_INITERROR;
704
705     cmdline = _wcsdup(GetCommandLineW());
706     if (cmdline == NULL)
707         return rc;
708
709     /* Skip first arg, which is the program name */
710     if ((rc = find_end_of_word(cmdline, &word)) != RC_OK)
711         goto out;
712     word = skip_whitespace(word);
713
714     while (*word)
715     {
716         WCHAR first;
717         if ((rc = find_end_of_word(word, &end)) != RC_OK)
718             goto out;
719
720         next = skip_whitespace(end);
721         first = word[0];
722         *end = '\0';
723         strip_quotes(word, &end);
724         WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word));
725
726         /* First non-switch parameter is source, second is destination */
727         if (first != '/') {
728             if (suppliedsource[0] == 0x00) {
729                 lstrcpyW(suppliedsource, word);
730             } else if (supplieddestination[0] == 0x00) {
731                 lstrcpyW(supplieddestination, word);
732             } else {
733                 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
734                 goto out;
735             }
736         } else {
737             /* Process all the switch options
738                  Note: Windows docs say /P prompts when dest is created
739                        but tests show it is done for each src file
740                        regardless of the destination                   */
741             switch (toupper(word[1])) {
742             case 'I': flags |= OPT_ASSUMEDIR;     break;
743             case 'S': flags |= OPT_RECURSIVE;     break;
744             case 'Q': flags |= OPT_QUIET;         break;
745             case 'F': flags |= OPT_FULL;          break;
746             case 'L': flags |= OPT_SIMULATE;      break;
747             case 'W': flags |= OPT_PAUSE;         break;
748             case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
749             case 'Y': flags |= OPT_NOPROMPT;      break;
750             case 'N': flags |= OPT_SHORTNAME;     break;
751             case 'U': flags |= OPT_MUSTEXIST;     break;
752             case 'R': flags |= OPT_REPLACEREAD;   break;
753             case 'H': flags |= OPT_COPYHIDSYS;    break;
754             case 'C': flags |= OPT_IGNOREERRORS;  break;
755             case 'P': flags |= OPT_SRCPROMPT;     break;
756             case 'A': flags |= OPT_ARCHIVEONLY;   break;
757             case 'M': flags |= OPT_ARCHIVEONLY |
758                                OPT_REMOVEARCH;    break;
759
760             /* E can be /E or /EXCLUDE */
761             case 'E': if (CompareStringW(LOCALE_USER_DEFAULT,
762                                          NORM_IGNORECASE | SORT_STRINGSORT,
763                                          &word[1], 8,
764                                          EXCLUDE, -1) == CSTR_EQUAL) {
765                         if (XCOPY_ProcessExcludeList(&word[9])) {
766                           XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
767                           goto out;
768                         } else flags |= OPT_EXCLUDELIST;
769                       } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
770                       break;
771
772             /* D can be /D or /D: */
773             case 'D': if (word[2]==':' && isdigit(word[3])) {
774                           SYSTEMTIME st;
775                           WCHAR     *pos = &word[3];
776                           BOOL       isError = FALSE;
777                           memset(&st, 0x00, sizeof(st));
778
779                           /* Parse the arg : Month */
780                           st.wMonth = _wtol(pos);
781                           while (*pos && isdigit(*pos)) pos++;
782                           if (*pos++ != '-') isError = TRUE;
783
784                           /* Parse the arg : Day */
785                           if (!isError) {
786                               st.wDay = _wtol(pos);
787                               while (*pos && isdigit(*pos)) pos++;
788                               if (*pos++ != '-') isError = TRUE;
789                           }
790
791                           /* Parse the arg : Year */
792                           if (!isError) {
793                               st.wYear = _wtol(pos);
794                               while (*pos && isdigit(*pos)) pos++;
795                               if (st.wYear < 100) st.wYear+=2000;
796                           }
797
798                           if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
799                               SYSTEMTIME st;
800                               WCHAR datestring[32], timestring[32];
801
802                               flags |= OPT_DATERANGE;
803
804                               /* Debug info: */
805                               FileTimeToSystemTime (&dateRange, &st);
806                               GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring,
807                                              sizeof(datestring)/sizeof(WCHAR));
808                               GetTimeFormatW(0, TIME_NOSECONDS, &st,
809                                              NULL, timestring, sizeof(timestring)/sizeof(WCHAR));
810
811                               WINE_TRACE("Date being used is: %s %s\n",
812                                          wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
813                           } else {
814                               XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
815                               goto out;
816                           }
817                       } else {
818                           flags |= OPT_DATENEWER;
819                       }
820                       break;
821
822             case '-': if (toupper(word[2])=='Y')
823                           flags &= ~OPT_NOPROMPT; break;
824             case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
825                       rc = RC_HELP;
826                       goto out;
827             default:
828                 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word));
829                 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), word);
830                 goto out;
831             }
832         }
833         word = next;
834     }
835
836     /* Default the destination if not supplied */
837     if (supplieddestination[0] == 0x00)
838         lstrcpyW(supplieddestination, wchr_dot);
839
840     *pflags = flags;
841     rc = RC_OK;
842
843  out:
844     free(cmdline);
845     return rc;
846 }
847
848
849 /* =========================================================================
850    XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
851      converts it into a stem and a filespec
852    ========================================================================= */
853 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
854                                    WCHAR *spec, DWORD flags)
855 {
856     WCHAR             actualsource[MAX_PATH];
857     WCHAR            *starPos;
858     WCHAR            *questPos;
859     DWORD             attribs;
860
861     /*
862      * Validate the source, expanding to full path ensuring it exists
863      */
864     if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
865         WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
866         return RC_INITERROR;
867     }
868
869     /* If full names required, convert to using the full path */
870     if (flags & OPT_FULL) {
871         lstrcpyW(suppliedsource, actualsource);
872     }
873
874     /*
875      * Work out the stem of the source
876      */
877
878     /* If a directory is supplied, use that as-is (either fully or
879           partially qualified)
880        If a filename is supplied + a directory or drive path, use that
881           as-is
882        Otherwise
883           If no directory or path specified, add eg. C:
884           stem is Drive/Directory is bit up to last \ (or first :)
885           spec is bit after that                                         */
886
887     starPos = wcschr(suppliedsource, '*');
888     questPos = wcschr(suppliedsource, '?');
889     if (starPos || questPos) {
890         attribs = 0x00;  /* Ensures skips invalid or directory check below */
891     } else {
892         attribs = GetFileAttributesW(actualsource);
893     }
894
895     if (attribs == INVALID_FILE_ATTRIBUTES) {
896         XCOPY_FailMessage(GetLastError());
897         return RC_INITERROR;
898
899     /* Directory:
900          stem should be exactly as supplied plus a '\', unless it was
901           eg. C: in which case no slash required */
902     } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
903         WCHAR lastChar;
904
905         WINE_TRACE("Directory supplied\n");
906         lstrcpyW(stem, suppliedsource);
907         lastChar = stem[lstrlenW(stem)-1];
908         if (lastChar != '\\' && lastChar != ':') {
909             lstrcatW(stem, wchr_slash);
910         }
911         lstrcpyW(spec, wchr_star);
912
913     /* File or wildcard search:
914          stem should be:
915            Up to and including last slash if directory path supplied
916            If c:filename supplied, just the c:
917            Otherwise stem should be the current drive letter + ':' */
918     } else {
919         WCHAR *lastDir;
920
921         WINE_TRACE("Filename supplied\n");
922         lastDir   = wcsrchr(suppliedsource, '\\');
923
924         if (lastDir) {
925             lstrcpyW(stem, suppliedsource);
926             stem[(lastDir-suppliedsource) + 1] = 0x00;
927             lstrcpyW(spec, (lastDir+1));
928         } else if (suppliedsource[1] == ':') {
929             lstrcpyW(stem, suppliedsource);
930             stem[2] = 0x00;
931             lstrcpyW(spec, suppliedsource+2);
932         } else {
933             WCHAR curdir[MAXSTRING];
934             GetCurrentDirectoryW(sizeof(curdir)/sizeof(WCHAR), curdir);
935             stem[0] = curdir[0];
936             stem[1] = curdir[1];
937             stem[2] = 0x00;
938             lstrcpyW(spec, suppliedsource);
939         }
940     }
941
942     return RC_OK;
943 }
944
945 /* =========================================================================
946    XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
947      converts it into a stem
948    ========================================================================= */
949 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
950                                  WCHAR *srcspec, DWORD flags)
951 {
952     WCHAR  actualdestination[MAX_PATH];
953     DWORD attribs;
954     BOOL isDir = FALSE;
955
956     /*
957      * Validate the source, expanding to full path ensuring it exists
958      */
959     if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
960         WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
961         return RC_INITERROR;
962     }
963
964     /* Destination is either a directory or a file */
965     attribs = GetFileAttributesW(actualdestination);
966
967     if (attribs == INVALID_FILE_ATTRIBUTES) {
968
969         /* If /I supplied and wildcard copy, assume directory */
970         /* Also if destination ends with backslash */
971         if ((flags & OPT_ASSUMEDIR &&
972             (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) ||
973             (supplieddestination[lstrlenW(supplieddestination)-1] == '\\')) {
974
975             isDir = TRUE;
976
977         } else {
978             DWORD count;
979             char  answer[10] = "";
980             WCHAR fileChar[2];
981             WCHAR dirChar[2];
982
983             /* Read the F and D characters from the resource file */
984             wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
985             wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
986
987             while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
988                 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
989
990                 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
991                 WINE_TRACE("User answer %c\n", answer[0]);
992
993                 answer[0] = toupper(answer[0]);
994             }
995
996             if (answer[0] == dirChar[0]) {
997                 isDir = TRUE;
998             } else {
999                 isDir = FALSE;
1000             }
1001         }
1002     } else {
1003         isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
1004     }
1005
1006     if (isDir) {
1007         lstrcpyW(stem, actualdestination);
1008         *spec = 0x00;
1009
1010         /* Ensure ends with a '\' */
1011         if (stem[lstrlenW(stem)-1] != '\\') {
1012             lstrcatW(stem, wchr_slash);
1013         }
1014
1015     } else {
1016         WCHAR drive[MAX_PATH];
1017         WCHAR dir[MAX_PATH];
1018         WCHAR fname[MAX_PATH];
1019         WCHAR ext[MAX_PATH];
1020         _wsplitpath(actualdestination, drive, dir, fname, ext);
1021         lstrcpyW(stem, drive);
1022         lstrcatW(stem, dir);
1023         lstrcpyW(spec, fname);
1024         lstrcatW(spec, ext);
1025     }
1026     return RC_OK;
1027 }
1028
1029
1030 /* =========================================================================
1031    main - Main entrypoint for the xcopy command
1032
1033      Processes the args, and drives the actual copying
1034    ========================================================================= */
1035 int wmain (int argc, WCHAR *argvW[])
1036 {
1037     int     rc = 0;
1038     WCHAR   suppliedsource[MAX_PATH] = {0};   /* As supplied on the cmd line */
1039     WCHAR   supplieddestination[MAX_PATH] = {0};
1040     WCHAR   sourcestem[MAX_PATH] = {0};       /* Stem of source          */
1041     WCHAR   sourcespec[MAX_PATH] = {0};       /* Filespec of source      */
1042     WCHAR   destinationstem[MAX_PATH] = {0};  /* Stem of destination     */
1043     WCHAR   destinationspec[MAX_PATH] = {0};  /* Filespec of destination */
1044     WCHAR   copyCmd[MAXSTRING];               /* COPYCMD env var         */
1045     DWORD   flags = 0;                        /* Option flags            */
1046     const WCHAR PROMPTSTR1[]  = {'/', 'Y', 0};
1047     const WCHAR PROMPTSTR2[]  = {'/', 'y', 0};
1048     const WCHAR COPYCMD[]  = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1049
1050     /* Preinitialize flags based on COPYCMD */
1051     if (GetEnvironmentVariableW(COPYCMD, copyCmd, MAXSTRING)) {
1052         if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
1053             wcsstr(copyCmd, PROMPTSTR2) != NULL) {
1054             flags |= OPT_NOPROMPT;
1055         }
1056     }
1057
1058     /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1059        wine, but on windows these can be normal files. At least one installer
1060        uses files such as .packlist and (validly) expects them to be copied.
1061        Under wine, if we do not copy hidden files by default then they get
1062        lose                                                                   */
1063     flags |= OPT_COPYHIDSYS;
1064
1065     /*
1066      * Parse the command line
1067      */
1068     if ((rc = XCOPY_ParseCommandLine(suppliedsource, supplieddestination,
1069                                      &flags)) != RC_OK) {
1070         if (rc == RC_HELP)
1071             return RC_OK;
1072         else
1073             return rc;
1074     }
1075
1076     /* Trace out the supplied information */
1077     WINE_TRACE("Supplied parameters:\n");
1078     WINE_TRACE("Source      : '%s'\n", wine_dbgstr_w(suppliedsource));
1079     WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
1080
1081     /* Extract required information from source specification */
1082     rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
1083     if (rc != RC_OK) return rc;
1084
1085     /* Extract required information from destination specification */
1086     rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
1087                                destinationspec, sourcespec, flags);
1088     if (rc != RC_OK) return rc;
1089
1090     /* Trace out the resulting information */
1091     WINE_TRACE("Resolved parameters:\n");
1092     WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
1093     WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
1094     WINE_TRACE("Dest   Stem : '%s'\n", wine_dbgstr_w(destinationstem));
1095     WINE_TRACE("Dest   Spec : '%s'\n", wine_dbgstr_w(destinationspec));
1096
1097     /* Pause if necessary */
1098     if (flags & OPT_PAUSE) {
1099         DWORD count;
1100         char pausestr[10];
1101
1102         XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
1103         ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
1104                   &count, NULL);
1105     }
1106
1107     /* Now do the hard work... */
1108     rc = XCOPY_DoCopy(sourcestem, sourcespec,
1109                 destinationstem, destinationspec,
1110                 flags);
1111
1112     /* Clear up exclude list allocated memory */
1113     while (excludeList) {
1114         EXCLUDELIST *pos = excludeList;
1115         excludeList = excludeList -> next;
1116         HeapFree(GetProcessHeap(), 0, pos->name);
1117         HeapFree(GetProcessHeap(), 0, pos);
1118     }
1119
1120     /* Finished - print trailer and exit */
1121     if (flags & OPT_SIMULATE) {
1122         XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
1123     } else if (!(flags & OPT_NOCOPY)) {
1124         XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
1125     }
1126     if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
1127     return rc;
1128
1129 }