xcopy: Move all messages into an English resource file.
[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  * Notes:
23  * Apparently, valid return codes are:
24  *   0 - OK
25  *   1 - No files found to copy
26  *   2 - CTRL+C during copy
27  *   4 - Initialization error, or invalid source specification
28  *   5 - Disk write error
29  */
30
31
32 #include <stdio.h>
33 #include <windows.h>
34 #include <wine/debug.h>
35 #include "xcopy.h"
36
37 WINE_DEFAULT_DEBUG_CHANNEL(xcopy);
38
39 /* Prototypes */
40 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem, WCHAR *spec);
41 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem,
42                                  WCHAR *spec, WCHAR *srcspec, DWORD flags);
43 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
44                         WCHAR *deststem, WCHAR *destspec,
45                         DWORD flags);
46 static BOOL XCOPY_CreateDirectory(const WCHAR* path);
47 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms);
48 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName);
49 static WCHAR *XCOPY_LoadMessage(UINT id);
50 static void XCOPY_FailMessage(DWORD err);
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 /* Constants (Mostly for widechars) */
70
71
72 /* To minimize stack usage during recursion, some temporary variables
73    made global                                                        */
74 static WCHAR copyFrom[MAX_PATH];
75 static WCHAR copyTo[MAX_PATH];
76
77
78 /* =========================================================================
79    main - Main entrypoint for the xcopy command
80
81      Processes the args, and drives the actual copying
82    ========================================================================= */
83 int main (int argc, char *argv[])
84 {
85     int     rc = 0;
86     WCHAR   suppliedsource[MAX_PATH] = {0};   /* As supplied on the cmd line */
87     WCHAR   supplieddestination[MAX_PATH] = {0};
88     WCHAR   sourcestem[MAX_PATH] = {0};       /* Stem of source          */
89     WCHAR   sourcespec[MAX_PATH] = {0};       /* Filespec of source      */
90     WCHAR   destinationstem[MAX_PATH] = {0};  /* Stem of destination     */
91     WCHAR   destinationspec[MAX_PATH] = {0};  /* Filespec of destination */
92     WCHAR   copyCmd[MAXSTRING];               /* COPYCMD env var         */
93     DWORD   flags = 0;                        /* Option flags            */
94     LPWSTR *argvW = NULL;
95     const WCHAR PROMPTSTR1[]  = {'/', 'Y', 0};
96     const WCHAR PROMPTSTR2[]  = {'/', 'y', 0};
97     const WCHAR COPYCMD[]  = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
98     const WCHAR EXCLUDE[]  = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
99
100     /*
101      * Parse the command line
102      */
103
104     /* overwrite the command line */
105     argvW = CommandLineToArgvW( GetCommandLineW(), &argc );
106
107     /* Confirm at least one parameter */
108     if (argc < 2) {
109         wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
110         return RC_INITERROR;
111     }
112
113     /* Preinitialize flags based on COPYCMD */
114     if (GetEnvironmentVariable(COPYCMD, copyCmd, MAXSTRING)) {
115         if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
116             wcsstr(copyCmd, PROMPTSTR2) != NULL) {
117             flags |= OPT_NOPROMPT;
118         }
119     }
120
121     /* Skip first arg, which is the program name */
122     argvW++;
123
124     while (argc > 1)
125     {
126         argc--;
127         WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(*argvW));
128
129         /* First non-switch parameter is source, second is destination */
130         if (*argvW[0] != '/') {
131             if (suppliedsource[0] == 0x00) {
132                 lstrcpyW(suppliedsource, *argvW);
133             } else if (supplieddestination[0] == 0x00) {
134                 lstrcpyW(supplieddestination, *argvW);
135             } else {
136                 wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
137                 return RC_INITERROR;
138             }
139         } else {
140             /* Process all the switch options
141                  Note: Windows docs say /P prompts when dest is created
142                        but tests show it is done for each src file
143                        regardless of the destination                   */
144             switch (toupper(argvW[0][1])) {
145             case 'I': flags |= OPT_ASSUMEDIR;     break;
146             case 'S': flags |= OPT_RECURSIVE;     break;
147             case 'Q': flags |= OPT_QUIET;         break;
148             case 'F': flags |= OPT_FULL;          break;
149             case 'L': flags |= OPT_SIMULATE;      break;
150             case 'W': flags |= OPT_PAUSE;         break;
151             case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
152             case 'Y': flags |= OPT_NOPROMPT;      break;
153             case 'N': flags |= OPT_SHORTNAME;     break;
154             case 'U': flags |= OPT_MUSTEXIST;     break;
155             case 'R': flags |= OPT_REPLACEREAD;   break;
156             case 'H': flags |= OPT_COPYHIDSYS;    break;
157             case 'C': flags |= OPT_IGNOREERRORS;  break;
158             case 'P': flags |= OPT_SRCPROMPT;     break;
159             case 'A': flags |= OPT_ARCHIVEONLY;   break;
160             case 'M': flags |= OPT_ARCHIVEONLY |
161                                OPT_REMOVEARCH;    break;
162
163             /* E can be /E or /EXCLUDE */
164             case 'E': if (CompareString (LOCALE_USER_DEFAULT,
165                                          NORM_IGNORECASE | SORT_STRINGSORT,
166                                          &argvW[0][1], 8,
167                                          EXCLUDE, -1) == 2) {
168                         if (XCOPY_ProcessExcludeList(&argvW[0][9])) {
169                           XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
170                           return RC_INITERROR;
171                         } else flags |= OPT_EXCLUDELIST;
172                       } else flags |= OPT_EMPTYDIR;
173                       break;
174
175             /* D can be /D or /D: */
176             case 'D': if ((argvW[0][2])==':' && isdigit(argvW[0][3])) {
177                           SYSTEMTIME st;
178                           WCHAR     *pos = &argvW[0][3];
179                           BOOL       isError = FALSE;
180                           memset(&st, 0x00, sizeof(st));
181
182                           /* Parse the arg : Month */
183                           st.wMonth = _wtol(pos);
184                           while (*pos && isdigit(*pos)) pos++;
185                           if (*pos++ != '-') isError = TRUE;
186
187                           /* Parse the arg : Day */
188                           if (!isError) {
189                               st.wDay = _wtol(pos);
190                               while (*pos && isdigit(*pos)) pos++;
191                               if (*pos++ != '-') isError = TRUE;
192                           }
193
194                           /* Parse the arg : Day */
195                           if (!isError) {
196                               st.wYear = _wtol(pos);
197                               if (st.wYear < 100) st.wYear+=2000;
198                           }
199
200                           if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
201                               SYSTEMTIME st;
202                               WCHAR datestring[32], timestring[32];
203
204                               flags |= OPT_DATERANGE;
205
206                               /* Debug info: */
207                               FileTimeToSystemTime (&dateRange, &st);
208                               GetDateFormat (0, DATE_SHORTDATE, &st, NULL, datestring,
209                                           sizeof(datestring));
210                               GetTimeFormat (0, TIME_NOSECONDS, &st,
211                                           NULL, timestring, sizeof(timestring));
212
213                               WINE_TRACE("Date being used is: %s %s\n",
214                                          wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
215                           } else {
216                               XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
217                               return RC_INITERROR;
218                           }
219                       } else {
220                           flags |= OPT_DATENEWER;
221                       }
222                       break;
223
224             case '-': if (toupper(argvW[0][2])=='Y')
225                           flags &= ~OPT_NOPROMPT; break;
226             default:
227                 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(*argvW));
228                 wprintf(XCOPY_LoadMessage(STRING_INVPARM), *argvW);
229                 return RC_INITERROR;
230             }
231         }
232         argvW++;
233     }
234
235     /* Default the destination if not supplied */
236     if (supplieddestination[0] == 0x00)
237         lstrcpyW(supplieddestination, wchr_dot);
238
239     /* Trace out the supplied information */
240     WINE_TRACE("Supplied parameters:\n");
241     WINE_TRACE("Source      : '%s'\n", wine_dbgstr_w(suppliedsource));
242     WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
243
244     /* Extract required information from source specification */
245     rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec);
246
247     /* Extract required information from destination specification */
248     rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
249                                destinationspec, sourcespec, flags);
250
251     /* Trace out the resulting information */
252     WINE_TRACE("Resolved parameters:\n");
253     WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
254     WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
255     WINE_TRACE("Dest   Stem : '%s'\n", wine_dbgstr_w(destinationstem));
256     WINE_TRACE("Dest   Spec : '%s'\n", wine_dbgstr_w(destinationspec));
257
258     /* Pause if necessary */
259     if (flags & OPT_PAUSE) {
260         DWORD count;
261         char pausestr[10];
262
263         wprintf(XCOPY_LoadMessage(STRING_PAUSE));
264         ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
265                   &count, NULL);
266     }
267
268     /* Now do the hard work... */
269     rc = XCOPY_DoCopy(sourcestem, sourcespec,
270                 destinationstem, destinationspec,
271                 flags);
272
273     /* Clear up exclude list allocated memory */
274     while (excludeList) {
275         EXCLUDELIST *pos = excludeList;
276         excludeList = excludeList -> next;
277         HeapFree(GetProcessHeap(), 0, pos->name);
278         HeapFree(GetProcessHeap(), 0, pos);
279     }
280
281     /* Finished - print trailer and exit */
282     if (flags & OPT_SIMULATE) {
283         wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
284     } else if (!(flags & OPT_NOCOPY)) {
285         wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
286     }
287     if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
288     return rc;
289
290 }
291
292
293 /* =========================================================================
294    XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
295      converts it into a stem and a filespec
296    ========================================================================= */
297 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem, WCHAR *spec)
298 {
299     WCHAR             actualsource[MAX_PATH];
300     WCHAR            *starPos;
301     WCHAR            *questPos;
302
303     /*
304      * Validate the source, expanding to full path ensuring it exists
305      */
306     if (GetFullPathName(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
307         WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
308         return RC_INITERROR;
309     }
310
311     /*
312      * Work out the stem of the source
313      */
314
315     /* If no wildcard were supplied then the source is either a single
316        file or a directory - in which case thats the stem of the search,
317        otherwise split off the wildcards and use the higher level as the
318        stem                                                              */
319     lstrcpyW(stem, actualsource);
320     starPos = wcschr(stem, '*');
321     questPos = wcschr(stem, '?');
322     if (starPos || questPos) {
323         WCHAR *lastDir;
324
325         if (starPos) *starPos = 0x00;
326         if (questPos) *questPos = 0x00;
327
328         lastDir = wcsrchr(stem, '\\');
329         if (lastDir) *(lastDir+1) = 0x00;
330         else {
331             WINE_FIXME("Unexpected syntax error in source parameter\n");
332             return RC_INITERROR;
333         }
334         lstrcpyW(spec, actualsource + (lastDir - stem)+1);
335     } else {
336
337         DWORD attribs = GetFileAttributes(actualsource);
338
339         if (attribs == INVALID_FILE_ATTRIBUTES) {
340             XCOPY_FailMessage(GetLastError());
341             return RC_INITERROR;
342
343         /* Directory: */
344         } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
345             lstrcatW(stem, wchr_slash);
346             lstrcpyW(spec, wchr_star);
347
348         /* File: */
349         } else {
350             WCHAR drive[MAX_PATH];
351             WCHAR dir[MAX_PATH];
352             WCHAR fname[MAX_PATH];
353             WCHAR ext[MAX_PATH];
354             _wsplitpath(actualsource, drive, dir, fname, ext);
355             lstrcpyW(stem, drive);
356             lstrcatW(stem, dir);
357             lstrcpyW(spec, fname);
358             lstrcatW(spec, ext);
359         }
360     }
361     return RC_OK;
362 }
363
364 /* =========================================================================
365    XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
366      converts it into a stem
367    ========================================================================= */
368 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
369                                  WCHAR *srcspec, DWORD flags)
370 {
371     WCHAR  actualdestination[MAX_PATH];
372     DWORD attribs;
373     BOOL isDir = FALSE;
374
375     /*
376      * Validate the source, expanding to full path ensuring it exists
377      */
378     if (GetFullPathName(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
379         WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
380         return RC_INITERROR;
381     }
382
383     /* Destination is either a directory or a file */
384     attribs = GetFileAttributes(actualdestination);
385
386     if (attribs == INVALID_FILE_ATTRIBUTES) {
387
388         /* If /I supplied and wildcard copy, assume directory */
389         if (flags & OPT_ASSUMEDIR &&
390             (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) {
391
392             isDir = TRUE;
393
394         } else {
395             DWORD count;
396             char  answer[10] = "";
397             WCHAR fileChar[2];
398             WCHAR dirChar[2];
399
400             /* Read the F and D characters from the resource file */
401             wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
402             wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
403
404             while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
405                 wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
406
407                 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
408                 WINE_TRACE("User answer %c\n", answer[0]);
409
410                 answer[0] = toupper(answer[0]);
411             }
412
413             if (answer[0] == dirChar[0]) {
414                 isDir = TRUE;
415             } else {
416                 isDir = FALSE;
417             }
418         }
419     } else {
420         isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
421     }
422
423     if (isDir) {
424         lstrcpyW(stem, actualdestination);
425         *spec = 0x00;
426
427         /* Ensure ends with a '\' */
428         if (stem[lstrlenW(stem)-1] != '\\') {
429             lstrcatW(stem, wchr_slash);
430         }
431
432     } else {
433         WCHAR drive[MAX_PATH];
434         WCHAR dir[MAX_PATH];
435         WCHAR fname[MAX_PATH];
436         WCHAR ext[MAX_PATH];
437         _wsplitpath(actualdestination, drive, dir, fname, ext);
438         lstrcpyW(stem, drive);
439         lstrcatW(stem, dir);
440         lstrcpyW(spec, fname);
441         lstrcatW(spec, ext);
442     }
443     return RC_OK;
444 }
445
446 /* =========================================================================
447    XCOPY_DoCopy - Recursive function to copy files based on input parms
448      of a stem and a spec
449
450       This works by using FindFirstFile supplying the source stem and spec.
451       If results are found, any non-directory ones are processed
452       Then, if /S or /E is supplied, another search is made just for
453       directories, and this function is called again for that directory
454
455    ========================================================================= */
456 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
457                         WCHAR *deststem, WCHAR *destspec,
458                         DWORD flags)
459 {
460     WIN32_FIND_DATA *finddata;
461     HANDLE          h;
462     BOOL            findres = TRUE;
463     WCHAR           *inputpath, *outputpath;
464     BOOL            copiedFile = FALSE;
465     DWORD           destAttribs, srcAttribs;
466     BOOL            skipFile;
467
468     /* Allocate some working memory on heap to minimize footprint */
469     finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATA));
470     inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
471     outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
472
473     /* Build the search info into a single parm */
474     lstrcpyW(inputpath, srcstem);
475     lstrcatW(inputpath, srcspec);
476
477     /* Search 1 - Look for matching files */
478     h = FindFirstFile(inputpath, finddata);
479     while (h != INVALID_HANDLE_VALUE && findres) {
480
481         skipFile = FALSE;
482
483         /* Ignore . and .. */
484         if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
485             lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
486             finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
487
488             WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
489         } else {
490
491             /* Get the filename information */
492             lstrcpyW(copyFrom, srcstem);
493             if (flags & OPT_SHORTNAME) {
494               lstrcatW(copyFrom, finddata->cAlternateFileName);
495             } else {
496               lstrcatW(copyFrom, finddata->cFileName);
497             }
498
499             lstrcpyW(copyTo, deststem);
500             if (*destspec == 0x00) {
501                 if (flags & OPT_SHORTNAME) {
502                     lstrcatW(copyTo, finddata->cAlternateFileName);
503                 } else {
504                     lstrcatW(copyTo, finddata->cFileName);
505                 }
506             } else {
507                 lstrcatW(copyTo, destspec);
508             }
509
510             /* Do the copy */
511             WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
512                                                       wine_dbgstr_w(copyTo));
513             if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
514
515             /* See if allowed to copy it */
516             srcAttribs = GetFileAttributesW(copyFrom);
517             WINE_TRACE("Source attribs: %d\n", srcAttribs);
518
519             if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
520                 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
521
522                 if (!(flags & OPT_COPYHIDSYS)) {
523                     skipFile = TRUE;
524                 }
525             }
526
527             if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
528                 (flags & OPT_ARCHIVEONLY)) {
529                 skipFile = TRUE;
530             }
531
532             /* See if file exists */
533             destAttribs = GetFileAttributesW(copyTo);
534             WINE_TRACE("Dest attribs: %d\n", srcAttribs);
535
536             /* Check date ranges if a destination file already exists */
537             if (!skipFile && (flags & OPT_DATERANGE) &&
538                 (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
539                 WINE_TRACE("Skipping file as modified date too old\n");
540                 skipFile = TRUE;
541             }
542
543             /* If just /D supplied, only overwrite if src newer than dest */
544             if (!skipFile && (flags & OPT_DATENEWER) &&
545                (destAttribs != INVALID_FILE_ATTRIBUTES)) {
546                 HANDLE h = CreateFile(copyTo, GENERIC_READ, FILE_SHARE_READ,
547                                       NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
548                                       NULL);
549                 if (h != INVALID_HANDLE_VALUE) {
550                     FILETIME writeTime;
551                     GetFileTime(h, NULL, NULL, &writeTime);
552
553                     if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
554                         WINE_TRACE("Skipping file as dest newer or same date\n");
555                         skipFile = TRUE;
556                     }
557                     CloseHandle(h);
558                 }
559             }
560
561             /* See if exclude list provided. Note since filenames are case
562                insensitive, need to uppercase the filename before doing
563                strstr                                                     */
564             if (!skipFile && (flags & OPT_EXCLUDELIST)) {
565                 EXCLUDELIST *pos = excludeList;
566                 WCHAR copyFromUpper[MAX_PATH];
567
568                 /* Uppercase source filename */
569                 lstrcpyW(copyFromUpper, copyFrom);
570                 CharUpperBuff(copyFromUpper, lstrlenW(copyFromUpper));
571
572                 /* Loop through testing each exclude line */
573                 while (pos) {
574                     if (wcsstr(copyFromUpper, pos->name) != NULL) {
575                         WINE_TRACE("Skipping file as matches exclude '%s'\n",
576                                    wine_dbgstr_w(pos->name));
577                         skipFile = TRUE;
578                         pos = NULL;
579                     } else {
580                         pos = pos->next;
581                     }
582                 }
583             }
584
585             /* Prompt each file if necessary */
586             if (!skipFile && (flags & OPT_SRCPROMPT)) {
587                 DWORD count;
588                 char  answer[10];
589                 BOOL  answered = FALSE;
590                 WCHAR yesChar[2];
591                 WCHAR noChar[2];
592
593                 /* Read the Y and N characters from the resource file */
594                 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
595                 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
596
597                 while (!answered) {
598                     wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
599                     ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
600                               &count, NULL);
601
602                     answered = TRUE;
603                     if (toupper(answer[0]) == noChar[0])
604                         skipFile = TRUE;
605                     else if (toupper(answer[0]) != yesChar[0])
606                         answered = FALSE;
607                 }
608             }
609
610             if (!skipFile &&
611                 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
612                 DWORD count;
613                 char  answer[10];
614                 BOOL  answered = FALSE;
615                 WCHAR yesChar[2];
616                 WCHAR allChar[2];
617                 WCHAR noChar[2];
618
619                 /* Read the A,Y and N characters from the resource file */
620                 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
621                 wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
622                 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
623
624                 while (!answered) {
625                     wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
626                     ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
627                               &count, NULL);
628
629                     answered = TRUE;
630                     if (toupper(answer[0]) == allChar[0])
631                         flags |= OPT_NOPROMPT;
632                     else if (toupper(answer[0]) == noChar[0])
633                         skipFile = TRUE;
634                     else if (toupper(answer[0]) != yesChar[0])
635                         answered = FALSE;
636                 }
637             }
638
639             /* See if it has to exist! */
640             if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
641                 skipFile = TRUE;
642             }
643
644             /* Output a status message */
645             if (!skipFile) {
646                 if (flags & OPT_QUIET) {
647                     /* Skip message */
648                 } else if (flags & OPT_FULL) {
649                     printf("%S -> %S\n", copyFrom, copyTo);
650                 } else {
651                     printf("%S\n", copyFrom);
652                 }
653
654                 /* If allowing overwriting of read only files, remove any
655                    write protection                                       */
656                 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
657                     (flags & OPT_REPLACEREAD)) {
658                     SetFileAttributes(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
659                 }
660
661                 copiedFile = TRUE;
662                 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
663                     /* Skip copy */
664                 } else if (CopyFile(copyFrom, copyTo, FALSE) == 0) {
665
666                     DWORD error = GetLastError();
667                     wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
668                            copyFrom, copyTo, error);
669                     XCOPY_FailMessage(error);
670
671                     if (flags & OPT_IGNOREERRORS) {
672                         skipFile = TRUE;
673                     } else {
674                         return RC_WRITEERROR;
675                     }
676                 }
677
678                 /* If /M supplied, remove the archive bit after successful copy */
679                 if (!skipFile) {
680                     if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
681                         (flags & OPT_REMOVEARCH)) {
682                         SetFileAttributes(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
683                     }
684                     filesCopied++;
685                 }
686             }
687         }
688
689         /* Find next file */
690         findres = FindNextFile(h, finddata);
691     }
692     FindClose(h);
693
694     /* Search 2 - do subdirs */
695     if (flags & OPT_RECURSIVE) {
696         lstrcpyW(inputpath, srcstem);
697         lstrcatW(inputpath, wchr_star);
698         findres = TRUE;
699         WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
700
701         h = FindFirstFile(inputpath, finddata);
702         while (h != INVALID_HANDLE_VALUE && findres) {
703
704             /* Only looking for dirs */
705             if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
706                 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
707                 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
708
709                 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
710
711                 /* Make up recursive information */
712                 lstrcpyW(inputpath, srcstem);
713                 lstrcatW(inputpath, finddata->cFileName);
714                 lstrcatW(inputpath, wchr_slash);
715
716                 lstrcpyW(outputpath, deststem);
717                 if (*destspec == 0x00) {
718                     lstrcatW(outputpath, finddata->cFileName);
719
720                     /* If /E is supplied, create the directory now */
721                     if ((flags & OPT_EMPTYDIR) &&
722                         !(flags & OPT_SIMULATE))
723                         XCOPY_CreateDirectory(outputpath);
724
725                     lstrcatW(outputpath, wchr_slash);
726                 }
727
728                 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
729             }
730
731             /* Find next one */
732             findres = FindNextFile(h, finddata);
733         }
734     }
735
736     /* free up memory */
737     HeapFree(GetProcessHeap(), 0, finddata);
738     HeapFree(GetProcessHeap(), 0, inputpath);
739     HeapFree(GetProcessHeap(), 0, outputpath);
740
741     return 0;
742 }
743
744 /* =========================================================================
745  * Routine copied from cmd.exe md command -
746  * This works recursivly. so creating dir1\dir2\dir3 will create dir1 and
747  * dir2 if they do not already exist.
748  * ========================================================================= */
749 static BOOL XCOPY_CreateDirectory(const WCHAR* path)
750 {
751     int len;
752     WCHAR *new_path;
753     BOOL ret = TRUE;
754
755     new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
756     lstrcpyW(new_path,path);
757
758     while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
759         new_path[len - 1] = 0;
760
761     while (!CreateDirectory(new_path,NULL))
762     {
763         WCHAR *slash;
764         DWORD last_error = GetLastError();
765         if (last_error == ERROR_ALREADY_EXISTS)
766             break;
767
768         if (last_error != ERROR_PATH_NOT_FOUND)
769         {
770             ret = FALSE;
771             break;
772         }
773
774         if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
775         {
776             ret = FALSE;
777             break;
778         }
779
780         len = slash - new_path;
781         new_path[len] = 0;
782         if (!XCOPY_CreateDirectory(new_path))
783         {
784             ret = FALSE;
785             break;
786         }
787         new_path[len] = '\\';
788     }
789     HeapFree(GetProcessHeap(),0,new_path);
790     return ret;
791 }
792
793 /* =========================================================================
794  * Process the /EXCLUDE: file list, building up a list of substrings to
795  * avoid copying
796  * Returns TRUE on any failure
797  * ========================================================================= */
798 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {
799
800     WCHAR *filenameStart = parms;
801
802     WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
803     excludeList = NULL;
804
805     while (*parms && *parms != ' ' && *parms != '/') {
806
807         /* If found '+' then process the file found so far */
808         if (*parms == '+') {
809             if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
810                 return TRUE;
811             }
812             filenameStart = parms+1;
813         }
814         parms++;
815     }
816
817     if (filenameStart != parms) {
818         if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
819             return TRUE;
820         }
821     }
822
823     return FALSE;
824 }
825
826 /* =========================================================================
827  * Process a single file from the /EXCLUDE: file list, building up a list
828  * of substrings to avoid copying
829  * Returns TRUE on any failure
830  * ========================================================================= */
831 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName) {
832
833     WCHAR   endChar = *endOfName;
834     WCHAR   buffer[MAXSTRING];
835     FILE   *inFile  = NULL;
836     const WCHAR readTextMode[]  = {'r', 't', 0};
837
838     /* Null terminate the filename (temporarily updates the filename hence
839          parms not const)                                                 */
840     *endOfName = 0x00;
841
842     /* Open the file */
843     inFile = _wfopen(filename, readTextMode);
844     if (inFile == NULL) {
845         wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
846         *endOfName = endChar;
847         return TRUE;
848     }
849
850     /* Process line by line */
851     while (fgetws(buffer, sizeof(buffer), inFile) != NULL) {
852         EXCLUDELIST *thisEntry;
853         int length = lstrlenW(buffer);
854
855         /* Strip CRLF */
856         buffer[length-1] = 0x00;
857
858         thisEntry = HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST));
859         thisEntry->next = excludeList;
860         excludeList = thisEntry;
861         thisEntry->name = HeapAlloc(GetProcessHeap(), 0,
862                                     (length * sizeof(WCHAR))+1);
863         lstrcpyW(thisEntry->name, buffer);
864         CharUpperBuff(thisEntry->name, length);
865         WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry->name));
866     }
867
868     /* See if EOF or error occurred */
869     if (!feof(inFile)) {
870         wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
871         *endOfName = endChar;
872         return TRUE;
873     }
874
875     /* Revert the input string to original form, and cleanup + return */
876     *endOfName = endChar;
877     fclose(inFile);
878     return FALSE;
879 }
880
881 /* =========================================================================
882  * Load a string from the resource file, handling any error
883  * Returns string retrieved from resource file
884  * ========================================================================= */
885 static WCHAR *XCOPY_LoadMessage(UINT id) {
886     static WCHAR msg[2048];
887     const WCHAR failedMsg[]  = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
888
889     if (!LoadString(GetModuleHandle(NULL), id, msg, sizeof(msg))) {
890        WINE_FIXME("LoadString failed with %d\n", GetLastError());
891        lstrcpyW(msg, failedMsg);
892     }
893     return msg;
894 }
895
896 /* =========================================================================
897  * Load a string for a system error and writes it to the screen
898  * Returns string retrieved from resource file
899  * ========================================================================= */
900 static void XCOPY_FailMessage(DWORD err) {
901     LPWSTR lpMsgBuf;
902     int status;
903
904     status = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER |
905                             FORMAT_MESSAGE_FROM_SYSTEM,
906                             NULL, err, 0,
907                             (LPTSTR) &lpMsgBuf, 0, NULL);
908     if (!status) {
909       WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
910                  err, GetLastError());
911     } else {
912       printf("%S\n", lpMsgBuf);
913       LocalFree ((HLOCAL)lpMsgBuf);
914     }
915 }