2 * XCOPY - Wine-compatible xcopy program
4 * Copyright (C) 2007 J. Edmeades
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.
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.
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
23 * This should now support all options listed in the xcopy help from
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
34 * Apparently, valid return codes are:
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
45 #include <wine/debug.h>
48 WINE_DEFAULT_DEBUG_CHANNEL(xcopy);
51 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
52 WCHAR *spec, DWORD flags);
53 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem,
54 WCHAR *spec, WCHAR *srcspec, DWORD flags);
55 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
56 WCHAR *deststem, WCHAR *destspec,
58 static BOOL XCOPY_CreateDirectory(const WCHAR* path);
59 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms);
60 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName);
61 static WCHAR *XCOPY_LoadMessage(UINT id);
62 static void XCOPY_FailMessage(DWORD err);
63 static int XCOPY_wprintf(const WCHAR *format, ...);
66 typedef struct _EXCLUDELIST
68 struct _EXCLUDELIST *next;
73 /* Global variables */
74 static ULONG filesCopied = 0; /* Number of files copied */
75 static EXCLUDELIST *excludeList = NULL; /* Excluded strings list */
76 static FILETIME dateRange; /* Date range to copy after*/
77 static const WCHAR wchr_slash[] = {'\\', 0};
78 static const WCHAR wchr_star[] = {'*', 0};
79 static const WCHAR wchr_dot[] = {'.', 0};
80 static const WCHAR wchr_dotdot[] = {'.', '.', 0};
82 /* Constants (Mostly for widechars) */
85 /* To minimize stack usage during recursion, some temporary variables
87 static WCHAR copyFrom[MAX_PATH];
88 static WCHAR copyTo[MAX_PATH];
91 /* =========================================================================
92 main - Main entrypoint for the xcopy command
94 Processes the args, and drives the actual copying
95 ========================================================================= */
96 int wmain (int argc, WCHAR *argvW[])
99 WCHAR suppliedsource[MAX_PATH] = {0}; /* As supplied on the cmd line */
100 WCHAR supplieddestination[MAX_PATH] = {0};
101 WCHAR sourcestem[MAX_PATH] = {0}; /* Stem of source */
102 WCHAR sourcespec[MAX_PATH] = {0}; /* Filespec of source */
103 WCHAR destinationstem[MAX_PATH] = {0}; /* Stem of destination */
104 WCHAR destinationspec[MAX_PATH] = {0}; /* Filespec of destination */
105 WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */
106 DWORD flags = 0; /* Option flags */
107 const WCHAR PROMPTSTR1[] = {'/', 'Y', 0};
108 const WCHAR PROMPTSTR2[] = {'/', 'y', 0};
109 const WCHAR COPYCMD[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
110 const WCHAR EXCLUDE[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
113 * Parse the command line
116 /* Confirm at least one parameter */
118 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
122 /* Preinitialize flags based on COPYCMD */
123 if (GetEnvironmentVariable(COPYCMD, copyCmd, MAXSTRING)) {
124 if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
125 wcsstr(copyCmd, PROMPTSTR2) != NULL) {
126 flags |= OPT_NOPROMPT;
130 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
131 wine, but on windows these can be normal files. At least one installer
132 uses files such as .packlist and (validly) expects them to be copied.
133 Under wine, if we do not copy hidden files by default then they get
135 flags |= OPT_COPYHIDSYS;
137 /* Skip first arg, which is the program name */
143 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(*argvW));
145 /* First non-switch parameter is source, second is destination */
146 if (*argvW[0] != '/') {
147 if (suppliedsource[0] == 0x00) {
148 lstrcpyW(suppliedsource, *argvW);
149 } else if (supplieddestination[0] == 0x00) {
150 lstrcpyW(supplieddestination, *argvW);
152 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
156 /* Process all the switch options
157 Note: Windows docs say /P prompts when dest is created
158 but tests show it is done for each src file
159 regardless of the destination */
160 switch (toupper(argvW[0][1])) {
161 case 'I': flags |= OPT_ASSUMEDIR; break;
162 case 'S': flags |= OPT_RECURSIVE; break;
163 case 'Q': flags |= OPT_QUIET; break;
164 case 'F': flags |= OPT_FULL; break;
165 case 'L': flags |= OPT_SIMULATE; break;
166 case 'W': flags |= OPT_PAUSE; break;
167 case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
168 case 'Y': flags |= OPT_NOPROMPT; break;
169 case 'N': flags |= OPT_SHORTNAME; break;
170 case 'U': flags |= OPT_MUSTEXIST; break;
171 case 'R': flags |= OPT_REPLACEREAD; break;
172 case 'H': flags |= OPT_COPYHIDSYS; break;
173 case 'C': flags |= OPT_IGNOREERRORS; break;
174 case 'P': flags |= OPT_SRCPROMPT; break;
175 case 'A': flags |= OPT_ARCHIVEONLY; break;
176 case 'M': flags |= OPT_ARCHIVEONLY |
177 OPT_REMOVEARCH; break;
179 /* E can be /E or /EXCLUDE */
180 case 'E': if (CompareString (LOCALE_USER_DEFAULT,
181 NORM_IGNORECASE | SORT_STRINGSORT,
184 if (XCOPY_ProcessExcludeList(&argvW[0][9])) {
185 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
187 } else flags |= OPT_EXCLUDELIST;
188 } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
191 /* D can be /D or /D: */
192 case 'D': if ((argvW[0][2])==':' && isdigit(argvW[0][3])) {
194 WCHAR *pos = &argvW[0][3];
195 BOOL isError = FALSE;
196 memset(&st, 0x00, sizeof(st));
198 /* Parse the arg : Month */
199 st.wMonth = _wtol(pos);
200 while (*pos && isdigit(*pos)) pos++;
201 if (*pos++ != '-') isError = TRUE;
203 /* Parse the arg : Day */
205 st.wDay = _wtol(pos);
206 while (*pos && isdigit(*pos)) pos++;
207 if (*pos++ != '-') isError = TRUE;
210 /* Parse the arg : Day */
212 st.wYear = _wtol(pos);
213 if (st.wYear < 100) st.wYear+=2000;
216 if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
218 WCHAR datestring[32], timestring[32];
220 flags |= OPT_DATERANGE;
223 FileTimeToSystemTime (&dateRange, &st);
224 GetDateFormat (0, DATE_SHORTDATE, &st, NULL, datestring,
226 GetTimeFormat (0, TIME_NOSECONDS, &st,
227 NULL, timestring, sizeof(timestring));
229 WINE_TRACE("Date being used is: %s %s\n",
230 wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
232 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
236 flags |= OPT_DATENEWER;
240 case '-': if (toupper(argvW[0][2])=='Y')
241 flags &= ~OPT_NOPROMPT; break;
242 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
245 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(*argvW));
246 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), *argvW);
253 /* Default the destination if not supplied */
254 if (supplieddestination[0] == 0x00)
255 lstrcpyW(supplieddestination, wchr_dot);
257 /* Trace out the supplied information */
258 WINE_TRACE("Supplied parameters:\n");
259 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource));
260 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
262 /* Extract required information from source specification */
263 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
265 /* Extract required information from destination specification */
266 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
267 destinationspec, sourcespec, flags);
269 /* Trace out the resulting information */
270 WINE_TRACE("Resolved parameters:\n");
271 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
272 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
273 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem));
274 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec));
276 /* Pause if necessary */
277 if (flags & OPT_PAUSE) {
281 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
282 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
286 /* Now do the hard work... */
287 rc = XCOPY_DoCopy(sourcestem, sourcespec,
288 destinationstem, destinationspec,
291 /* Clear up exclude list allocated memory */
292 while (excludeList) {
293 EXCLUDELIST *pos = excludeList;
294 excludeList = excludeList -> next;
295 HeapFree(GetProcessHeap(), 0, pos->name);
296 HeapFree(GetProcessHeap(), 0, pos);
299 /* Finished - print trailer and exit */
300 if (flags & OPT_SIMULATE) {
301 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
302 } else if (!(flags & OPT_NOCOPY)) {
303 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
305 if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
311 /* =========================================================================
312 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
313 converts it into a stem and a filespec
314 ========================================================================= */
315 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
316 WCHAR *spec, DWORD flags)
318 WCHAR actualsource[MAX_PATH];
324 * Validate the source, expanding to full path ensuring it exists
326 if (GetFullPathName(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
327 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
331 /* If full names required, convert to using the full path */
332 if (flags & OPT_FULL) {
333 lstrcpyW(suppliedsource, actualsource);
337 * Work out the stem of the source
340 /* If a directory is supplied, use that as-is (either fully or
342 If a filename is supplied + a directory or drive path, use that
345 If no directory or path specified, add eg. C:
346 stem is Drive/Directory is bit up to last \ (or first :)
347 spec is bit after that */
349 starPos = wcschr(suppliedsource, '*');
350 questPos = wcschr(suppliedsource, '?');
351 if (starPos || questPos) {
352 attribs = 0x00; /* Ensures skips invalid or directory check below */
354 attribs = GetFileAttributes(actualsource);
357 if (attribs == INVALID_FILE_ATTRIBUTES) {
358 XCOPY_FailMessage(GetLastError());
362 stem should be exactly as supplied plus a '\', unless it was
363 eg. C: in which case no slash required */
364 } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
367 WINE_TRACE("Directory supplied\n");
368 lstrcpyW(stem, suppliedsource);
369 lastChar = stem[lstrlenW(stem)-1];
370 if (lastChar != '\\' && lastChar != ':') {
371 lstrcatW(stem, wchr_slash);
373 lstrcpyW(spec, wchr_star);
375 /* File or wildcard search:
377 Up to and including last slash if directory path supplied
378 If c:filename supplied, just the c:
379 Otherwise stem should be the current drive letter + ':' */
383 WINE_TRACE("Filename supplied\n");
384 lastDir = wcsrchr(suppliedsource, '\\');
387 lstrcpyW(stem, suppliedsource);
388 stem[(lastDir-suppliedsource) + 1] = 0x00;
389 lstrcpyW(spec, (lastDir+1));
390 } else if (suppliedsource[1] == ':') {
391 lstrcpyW(stem, suppliedsource);
393 lstrcpyW(spec, suppliedsource+2);
395 WCHAR curdir[MAXSTRING];
396 GetCurrentDirectory (sizeof(curdir), curdir);
400 lstrcpyW(spec, suppliedsource);
407 /* =========================================================================
408 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
409 converts it into a stem
410 ========================================================================= */
411 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
412 WCHAR *srcspec, DWORD flags)
414 WCHAR actualdestination[MAX_PATH];
419 * Validate the source, expanding to full path ensuring it exists
421 if (GetFullPathName(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
422 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
426 /* Destination is either a directory or a file */
427 attribs = GetFileAttributes(actualdestination);
429 if (attribs == INVALID_FILE_ATTRIBUTES) {
431 /* If /I supplied and wildcard copy, assume directory */
432 if (flags & OPT_ASSUMEDIR &&
433 (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) {
439 char answer[10] = "";
443 /* Read the F and D characters from the resource file */
444 wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
445 wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
447 while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
448 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
450 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
451 WINE_TRACE("User answer %c\n", answer[0]);
453 answer[0] = toupper(answer[0]);
456 if (answer[0] == dirChar[0]) {
463 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
467 lstrcpyW(stem, actualdestination);
470 /* Ensure ends with a '\' */
471 if (stem[lstrlenW(stem)-1] != '\\') {
472 lstrcatW(stem, wchr_slash);
476 WCHAR drive[MAX_PATH];
478 WCHAR fname[MAX_PATH];
480 _wsplitpath(actualdestination, drive, dir, fname, ext);
481 lstrcpyW(stem, drive);
483 lstrcpyW(spec, fname);
489 /* =========================================================================
490 XCOPY_DoCopy - Recursive function to copy files based on input parms
493 This works by using FindFirstFile supplying the source stem and spec.
494 If results are found, any non-directory ones are processed
495 Then, if /S or /E is supplied, another search is made just for
496 directories, and this function is called again for that directory
498 ========================================================================= */
499 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
500 WCHAR *deststem, WCHAR *destspec,
503 WIN32_FIND_DATA *finddata;
506 WCHAR *inputpath, *outputpath;
507 BOOL copiedFile = FALSE;
508 DWORD destAttribs, srcAttribs;
512 /* Allocate some working memory on heap to minimize footprint */
513 finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATA));
514 inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
515 outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
517 /* Build the search info into a single parm */
518 lstrcpyW(inputpath, srcstem);
519 lstrcatW(inputpath, srcspec);
521 /* Search 1 - Look for matching files */
522 h = FindFirstFile(inputpath, finddata);
523 while (h != INVALID_HANDLE_VALUE && findres) {
527 /* Ignore . and .. */
528 if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
529 lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
530 finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
532 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
535 /* Get the filename information */
536 lstrcpyW(copyFrom, srcstem);
537 if (flags & OPT_SHORTNAME) {
538 lstrcatW(copyFrom, finddata->cAlternateFileName);
540 lstrcatW(copyFrom, finddata->cFileName);
543 lstrcpyW(copyTo, deststem);
544 if (*destspec == 0x00) {
545 if (flags & OPT_SHORTNAME) {
546 lstrcatW(copyTo, finddata->cAlternateFileName);
548 lstrcatW(copyTo, finddata->cFileName);
551 lstrcatW(copyTo, destspec);
555 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
556 wine_dbgstr_w(copyTo));
557 if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
559 /* See if allowed to copy it */
560 srcAttribs = GetFileAttributesW(copyFrom);
561 WINE_TRACE("Source attribs: %d\n", srcAttribs);
563 if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
564 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
566 if (!(flags & OPT_COPYHIDSYS)) {
571 if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
572 (flags & OPT_ARCHIVEONLY)) {
576 /* See if file exists */
577 destAttribs = GetFileAttributesW(copyTo);
578 WINE_TRACE("Dest attribs: %d\n", srcAttribs);
580 /* Check date ranges if a destination file already exists */
581 if (!skipFile && (flags & OPT_DATERANGE) &&
582 (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
583 WINE_TRACE("Skipping file as modified date too old\n");
587 /* If just /D supplied, only overwrite if src newer than dest */
588 if (!skipFile && (flags & OPT_DATENEWER) &&
589 (destAttribs != INVALID_FILE_ATTRIBUTES)) {
590 HANDLE h = CreateFile(copyTo, GENERIC_READ, FILE_SHARE_READ,
591 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
593 if (h != INVALID_HANDLE_VALUE) {
595 GetFileTime(h, NULL, NULL, &writeTime);
597 if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
598 WINE_TRACE("Skipping file as dest newer or same date\n");
605 /* See if exclude list provided. Note since filenames are case
606 insensitive, need to uppercase the filename before doing
608 if (!skipFile && (flags & OPT_EXCLUDELIST)) {
609 EXCLUDELIST *pos = excludeList;
610 WCHAR copyFromUpper[MAX_PATH];
612 /* Uppercase source filename */
613 lstrcpyW(copyFromUpper, copyFrom);
614 CharUpperBuff(copyFromUpper, lstrlenW(copyFromUpper));
616 /* Loop through testing each exclude line */
618 if (wcsstr(copyFromUpper, pos->name) != NULL) {
619 WINE_TRACE("Skipping file as matches exclude '%s'\n",
620 wine_dbgstr_w(pos->name));
629 /* Prompt each file if necessary */
630 if (!skipFile && (flags & OPT_SRCPROMPT)) {
633 BOOL answered = FALSE;
637 /* Read the Y and N characters from the resource file */
638 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
639 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
642 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
643 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
647 if (toupper(answer[0]) == noChar[0])
649 else if (toupper(answer[0]) != yesChar[0])
655 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
658 BOOL answered = FALSE;
663 /* Read the A,Y and N characters from the resource file */
664 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
665 wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
666 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
669 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
670 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
674 if (toupper(answer[0]) == allChar[0])
675 flags |= OPT_NOPROMPT;
676 else if (toupper(answer[0]) == noChar[0])
678 else if (toupper(answer[0]) != yesChar[0])
683 /* See if it has to exist! */
684 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
688 /* Output a status message */
690 if (flags & OPT_QUIET) {
692 } else if (flags & OPT_FULL) {
693 const WCHAR infostr[] = {'%', 's', ' ', '-', '>', ' ',
696 XCOPY_wprintf(infostr, copyFrom, copyTo);
698 const WCHAR infostr[] = {'%', 's', '\n', 0};
699 XCOPY_wprintf(infostr, copyFrom);
702 /* If allowing overwriting of read only files, remove any
704 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
705 (flags & OPT_REPLACEREAD)) {
706 SetFileAttributes(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
710 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
712 } else if (CopyFile(copyFrom, copyTo, FALSE) == 0) {
714 DWORD error = GetLastError();
715 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
716 copyFrom, copyTo, error);
717 XCOPY_FailMessage(error);
719 if (flags & OPT_IGNOREERRORS) {
727 /* If /M supplied, remove the archive bit after successful copy */
729 if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
730 (flags & OPT_REMOVEARCH)) {
731 SetFileAttributes(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
739 findres = FindNextFile(h, finddata);
743 /* Search 2 - do subdirs */
744 if (flags & OPT_RECURSIVE) {
745 lstrcpyW(inputpath, srcstem);
746 lstrcatW(inputpath, wchr_star);
748 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
750 h = FindFirstFile(inputpath, finddata);
751 while (h != INVALID_HANDLE_VALUE && findres) {
753 /* Only looking for dirs */
754 if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
755 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
756 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
758 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
760 /* Make up recursive information */
761 lstrcpyW(inputpath, srcstem);
762 lstrcatW(inputpath, finddata->cFileName);
763 lstrcatW(inputpath, wchr_slash);
765 lstrcpyW(outputpath, deststem);
766 if (*destspec == 0x00) {
767 lstrcatW(outputpath, finddata->cFileName);
769 /* If /E is supplied, create the directory now */
770 if ((flags & OPT_EMPTYDIR) &&
771 !(flags & OPT_SIMULATE))
772 XCOPY_CreateDirectory(outputpath);
774 lstrcatW(outputpath, wchr_slash);
777 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
781 findres = FindNextFile(h, finddata);
788 HeapFree(GetProcessHeap(), 0, finddata);
789 HeapFree(GetProcessHeap(), 0, inputpath);
790 HeapFree(GetProcessHeap(), 0, outputpath);
795 /* =========================================================================
796 * Routine copied from cmd.exe md command -
797 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
798 * dir2 if they do not already exist.
799 * ========================================================================= */
800 static BOOL XCOPY_CreateDirectory(const WCHAR* path)
806 new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
807 lstrcpyW(new_path,path);
809 while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
810 new_path[len - 1] = 0;
812 while (!CreateDirectory(new_path,NULL))
815 DWORD last_error = GetLastError();
816 if (last_error == ERROR_ALREADY_EXISTS)
819 if (last_error != ERROR_PATH_NOT_FOUND)
825 if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
831 len = slash - new_path;
833 if (!XCOPY_CreateDirectory(new_path))
838 new_path[len] = '\\';
840 HeapFree(GetProcessHeap(),0,new_path);
844 /* =========================================================================
845 * Process the /EXCLUDE: file list, building up a list of substrings to
847 * Returns TRUE on any failure
848 * ========================================================================= */
849 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {
851 WCHAR *filenameStart = parms;
853 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
856 while (*parms && *parms != ' ' && *parms != '/') {
858 /* If found '+' then process the file found so far */
860 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
863 filenameStart = parms+1;
868 if (filenameStart != parms) {
869 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
877 /* =========================================================================
878 * Process a single file from the /EXCLUDE: file list, building up a list
879 * of substrings to avoid copying
880 * Returns TRUE on any failure
881 * ========================================================================= */
882 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName) {
884 WCHAR endChar = *endOfName;
885 WCHAR buffer[MAXSTRING];
887 const WCHAR readTextMode[] = {'r', 't', 0};
889 /* Null terminate the filename (temporarily updates the filename hence
894 inFile = _wfopen(filename, readTextMode);
895 if (inFile == NULL) {
896 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
897 *endOfName = endChar;
901 /* Process line by line */
902 while (fgetws(buffer, sizeof(buffer), inFile) != NULL) {
903 EXCLUDELIST *thisEntry;
904 int length = lstrlenW(buffer);
907 buffer[length-1] = 0x00;
909 /* If more than CRLF */
911 thisEntry = HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST));
912 thisEntry->next = excludeList;
913 excludeList = thisEntry;
914 thisEntry->name = HeapAlloc(GetProcessHeap(), 0,
915 (length * sizeof(WCHAR))+1);
916 lstrcpyW(thisEntry->name, buffer);
917 CharUpperBuff(thisEntry->name, length);
918 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry->name));
922 /* See if EOF or error occurred */
924 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
925 *endOfName = endChar;
929 /* Revert the input string to original form, and cleanup + return */
930 *endOfName = endChar;
935 /* =========================================================================
936 * Load a string from the resource file, handling any error
937 * Returns string retrieved from resource file
938 * ========================================================================= */
939 static WCHAR *XCOPY_LoadMessage(UINT id) {
940 static WCHAR msg[MAXSTRING];
941 const WCHAR failedMsg[] = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
943 if (!LoadString(GetModuleHandle(NULL), id, msg, sizeof(msg))) {
944 WINE_FIXME("LoadString failed with %d\n", GetLastError());
945 lstrcpyW(msg, failedMsg);
950 /* =========================================================================
951 * Load a string for a system error and writes it to the screen
952 * Returns string retrieved from resource file
953 * ========================================================================= */
954 static void XCOPY_FailMessage(DWORD err) {
958 status = FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER |
959 FORMAT_MESSAGE_FROM_SYSTEM,
961 (LPTSTR) &lpMsgBuf, 0, NULL);
963 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
964 err, GetLastError());
966 const WCHAR infostr[] = {'%', 's', '\n', 0};
967 XCOPY_wprintf(infostr, lpMsgBuf);
968 LocalFree ((HLOCAL)lpMsgBuf);
972 /* =========================================================================
973 * Output a formatted unicode string. Ideally this will go to the console
974 * and hence required WriteConsoleW to output it, however if file i/o is
975 * redirected, it needs to be WriteFile'd using OEM (not ANSI) format
976 * ========================================================================= */
977 int XCOPY_wprintf(const WCHAR *format, ...) {
979 static WCHAR *output_bufW = NULL;
980 static char *output_bufA = NULL;
981 static BOOL toConsole = TRUE;
982 static BOOL traceOutput = FALSE;
983 #define MAX_WRITECONSOLE_SIZE 65535
990 * Allocate buffer to use when writing to console
991 * Note: Not freed - memory will be allocated once and released when
995 if (!output_bufW) output_bufW = HeapAlloc(GetProcessHeap(), 0,
996 MAX_WRITECONSOLE_SIZE);
998 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
1002 /* Use wvsprintf to store output into unicode buffer */
1003 va_start(parms, format);
1004 len = vswprintf(output_bufW, format, parms);
1007 /* Try to write as unicode all the time we think its a console */
1009 res = WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),
1010 output_bufW, len, &nOut, NULL);
1013 /* If writing to console has failed (ever) we assume its file
1014 i/o so convert to OEM codepage and output */
1016 BOOL usedDefaultChar = FALSE;
1017 DWORD convertedChars;
1022 * Allocate buffer to use when writing to file. Not freed, as above
1024 if (!output_bufA) output_bufA = HeapAlloc(GetProcessHeap(), 0,
1025 MAX_WRITECONSOLE_SIZE);
1027 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
1031 /* Convert to OEM, then output */
1032 convertedChars = WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW,
1033 len, output_bufA, MAX_WRITECONSOLE_SIZE,
1034 "?", &usedDefaultChar);
1035 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), output_bufA, convertedChars,
1039 /* Trace whether screen or console */
1041 WINE_TRACE("Writing to console? (%d)\n", toConsole);