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
50 #include <wine/debug.h>
51 //#include <wine/unicode.h>
55 WINE_DEFAULT_DEBUG_CHANNEL(xcopy
);
59 typedef struct _EXCLUDELIST
61 struct _EXCLUDELIST
*next
;
66 /* Global variables */
67 static ULONG filesCopied
= 0; /* Number of files copied */
68 static EXCLUDELIST
*excludeList
= NULL
; /* Excluded strings list */
69 static FILETIME dateRange
; /* Date range to copy after*/
70 static const WCHAR wchr_slash
[] = {'\\', 0};
71 static const WCHAR wchr_star
[] = {'*', 0};
72 static const WCHAR wchr_dot
[] = {'.', 0};
73 static const WCHAR wchr_dotdot
[] = {'.', '.', 0};
76 /* To minimize stack usage during recursion, some temporary variables
78 static WCHAR copyFrom
[MAX_PATH
];
79 static WCHAR copyTo
[MAX_PATH
];
82 /* =========================================================================
83 * Load a string from the resource file, handling any error
84 * Returns string retrieved from resource file
85 * ========================================================================= */
86 static WCHAR
*XCOPY_LoadMessage(UINT id
) {
87 static WCHAR msg
[MAXSTRING
];
88 const WCHAR failedMsg
[] = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
90 if (!LoadStringW(GetModuleHandleW(NULL
), id
, msg
, sizeof(msg
)/sizeof(WCHAR
))) {
91 WINE_FIXME("LoadString failed with %d\n", GetLastError());
92 lstrcpyW(msg
, failedMsg
);
97 /* =========================================================================
98 * Output a formatted unicode string. Ideally this will go to the console
99 * and hence required WriteConsoleW to output it, however if file i/o is
100 * redirected, it needs to be WriteFile'd using OEM (not ANSI) format
101 * ========================================================================= */
102 static int __cdecl
XCOPY_wprintf(const WCHAR
*format
, ...) {
104 static WCHAR
*output_bufW
= NULL
;
105 static char *output_bufA
= NULL
;
106 static BOOL toConsole
= TRUE
;
107 static BOOL traceOutput
= FALSE
;
108 #define MAX_WRITECONSOLE_SIZE 65535
116 * Allocate buffer to use when writing to console
117 * Note: Not freed - memory will be allocated once and released when
121 if (!output_bufW
) output_bufW
= HeapAlloc(GetProcessHeap(), 0,
122 MAX_WRITECONSOLE_SIZE
*sizeof(WCHAR
));
124 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
128 __ms_va_start(parms
, format
);
129 SetLastError(NO_ERROR
);
130 len
= FormatMessageW(FORMAT_MESSAGE_FROM_STRING
, format
, 0, 0, output_bufW
,
131 MAX_WRITECONSOLE_SIZE
/sizeof(*output_bufW
), &parms
);
133 if (len
== 0 && GetLastError() != NO_ERROR
) {
134 WINE_FIXME("Could not format string: le=%u, fmt=%s\n", GetLastError(), wine_dbgstr_w(format
));
138 /* Try to write as unicode whenever we think it's a console */
140 res
= WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE
),
141 output_bufW
, len
, &nOut
, NULL
);
144 /* If writing to console has failed (ever) we assume it's file
145 i/o so convert to OEM codepage and output */
147 BOOL usedDefaultChar
= FALSE
;
148 DWORD convertedChars
;
153 * Allocate buffer to use when writing to file. Not freed, as above
155 if (!output_bufA
) output_bufA
= HeapAlloc(GetProcessHeap(), 0,
156 MAX_WRITECONSOLE_SIZE
);
158 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
162 /* Convert to OEM, then output */
163 convertedChars
= WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW
,
164 len
, output_bufA
, MAX_WRITECONSOLE_SIZE
,
165 "?", &usedDefaultChar
);
166 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE
), output_bufA
, convertedChars
,
170 /* Trace whether screen or console */
172 WINE_TRACE("Writing to console? (%d)\n", toConsole
);
178 /* =========================================================================
179 * Load a string for a system error and writes it to the screen
180 * Returns string retrieved from resource file
181 * ========================================================================= */
182 static void XCOPY_FailMessage(DWORD err
) {
186 status
= FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER
|
187 FORMAT_MESSAGE_FROM_SYSTEM
,
189 (LPWSTR
) &lpMsgBuf
, 0, NULL
);
191 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
192 err
, GetLastError());
194 const WCHAR infostr
[] = {'%', '1', '\n', 0};
195 XCOPY_wprintf(infostr
, lpMsgBuf
);
196 LocalFree ((HLOCAL
)lpMsgBuf
);
201 /* =========================================================================
202 * Routine copied from cmd.exe md command -
203 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
204 * dir2 if they do not already exist.
205 * ========================================================================= */
206 static BOOL
XCOPY_CreateDirectory(const WCHAR
* path
)
212 new_path
= HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR
) * (lstrlenW(path
)+1));
213 lstrcpyW(new_path
,path
);
215 while ((len
= lstrlenW(new_path
)) && new_path
[len
- 1] == '\\')
216 new_path
[len
- 1] = 0;
218 while (!CreateDirectoryW(new_path
,NULL
))
221 DWORD last_error
= GetLastError();
222 if (last_error
== ERROR_ALREADY_EXISTS
)
225 if (last_error
!= ERROR_PATH_NOT_FOUND
)
231 if (!(slash
= wcsrchr(new_path
,'\\')) && ! (slash
= wcsrchr(new_path
,'/')))
237 len
= slash
- new_path
;
239 if (!XCOPY_CreateDirectory(new_path
))
244 new_path
[len
] = '\\';
246 HeapFree(GetProcessHeap(),0,new_path
);
250 /* =========================================================================
251 * Process a single file from the /EXCLUDE: file list, building up a list
252 * of substrings to avoid copying
253 * Returns TRUE on any failure
254 * ========================================================================= */
255 static BOOL
XCOPY_ProcessExcludeFile(WCHAR
* filename
, WCHAR
* endOfName
) {
257 WCHAR endChar
= *endOfName
;
258 WCHAR buffer
[MAXSTRING
];
260 const WCHAR readTextMode
[] = {'r', 't', 0};
262 /* Null terminate the filename (temporarily updates the filename hence
267 inFile
= _wfopen(filename
, readTextMode
);
268 if (inFile
== NULL
) {
269 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL
), filename
);
270 *endOfName
= endChar
;
274 /* Process line by line */
275 while (fgetws(buffer
, sizeof(buffer
)/sizeof(WCHAR
), inFile
) != NULL
) {
276 EXCLUDELIST
*thisEntry
;
277 int length
= lstrlenW(buffer
);
279 /* If more than CRLF */
281 buffer
[length
-1] = 0; /* strip CRLF */
282 thisEntry
= HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST
));
283 thisEntry
->next
= excludeList
;
284 excludeList
= thisEntry
;
285 thisEntry
->name
= HeapAlloc(GetProcessHeap(), 0,
286 (length
* sizeof(WCHAR
))+1);
287 lstrcpyW(thisEntry
->name
, buffer
);
288 CharUpperBuffW(thisEntry
->name
, length
);
289 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry
->name
));
293 /* See if EOF or error occurred */
295 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL
), filename
);
296 *endOfName
= endChar
;
301 /* Revert the input string to original form, and cleanup + return */
302 *endOfName
= endChar
;
307 /* =========================================================================
308 * Process the /EXCLUDE: file list, building up a list of substrings to
310 * Returns TRUE on any failure
311 * ========================================================================= */
312 static BOOL
XCOPY_ProcessExcludeList(WCHAR
* parms
) {
314 WCHAR
*filenameStart
= parms
;
316 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms
));
319 while (*parms
&& *parms
!= ' ' && *parms
!= '/') {
321 /* If found '+' then process the file found so far */
323 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
326 filenameStart
= parms
+1;
331 if (filenameStart
!= parms
) {
332 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
340 /* =========================================================================
341 XCOPY_DoCopy - Recursive function to copy files based on input parms
344 This works by using FindFirstFile supplying the source stem and spec.
345 If results are found, any non-directory ones are processed
346 Then, if /S or /E is supplied, another search is made just for
347 directories, and this function is called again for that directory
349 ========================================================================= */
350 static int XCOPY_DoCopy(WCHAR
*srcstem
, WCHAR
*srcspec
,
351 WCHAR
*deststem
, WCHAR
*destspec
,
354 WIN32_FIND_DATAW
*finddata
;
357 WCHAR
*inputpath
, *outputpath
;
358 BOOL copiedFile
= FALSE
;
359 DWORD destAttribs
, srcAttribs
;
363 /* Allocate some working memory on heap to minimize footprint */
364 finddata
= HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW
));
365 inputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
366 outputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
368 /* Build the search info into a single parm */
369 lstrcpyW(inputpath
, srcstem
);
370 lstrcatW(inputpath
, srcspec
);
372 /* Search 1 - Look for matching files */
373 h
= FindFirstFileW(inputpath
, finddata
);
374 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
378 /* Ignore . and .. */
379 if (lstrcmpW(finddata
->cFileName
, wchr_dot
)==0 ||
380 lstrcmpW(finddata
->cFileName
, wchr_dotdot
)==0 ||
381 finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) {
383 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata
->cFileName
));
386 /* Get the filename information */
387 lstrcpyW(copyFrom
, srcstem
);
388 if (flags
& OPT_SHORTNAME
) {
389 lstrcatW(copyFrom
, finddata
->cAlternateFileName
);
391 lstrcatW(copyFrom
, finddata
->cFileName
);
394 lstrcpyW(copyTo
, deststem
);
395 if (*destspec
== 0x00) {
396 if (flags
& OPT_SHORTNAME
) {
397 lstrcatW(copyTo
, finddata
->cAlternateFileName
);
399 lstrcatW(copyTo
, finddata
->cFileName
);
402 lstrcatW(copyTo
, destspec
);
406 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom
),
407 wine_dbgstr_w(copyTo
));
408 if (!copiedFile
&& !(flags
& OPT_SIMULATE
)) XCOPY_CreateDirectory(deststem
);
410 /* See if allowed to copy it */
411 srcAttribs
= GetFileAttributesW(copyFrom
);
412 WINE_TRACE("Source attribs: %d\n", srcAttribs
);
414 if ((srcAttribs
& FILE_ATTRIBUTE_HIDDEN
) ||
415 (srcAttribs
& FILE_ATTRIBUTE_SYSTEM
)) {
417 if (!(flags
& OPT_COPYHIDSYS
)) {
422 if (!(srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
423 (flags
& OPT_ARCHIVEONLY
)) {
427 /* See if file exists */
428 destAttribs
= GetFileAttributesW(copyTo
);
429 WINE_TRACE("Dest attribs: %d\n", srcAttribs
);
431 /* Check date ranges if a destination file already exists */
432 if (!skipFile
&& (flags
& OPT_DATERANGE
) &&
433 (CompareFileTime(&finddata
->ftLastWriteTime
, &dateRange
) < 0)) {
434 WINE_TRACE("Skipping file as modified date too old\n");
438 /* If just /D supplied, only overwrite if src newer than dest */
439 if (!skipFile
&& (flags
& OPT_DATENEWER
) &&
440 (destAttribs
!= INVALID_FILE_ATTRIBUTES
)) {
441 HANDLE h
= CreateFileW(copyTo
, GENERIC_READ
, FILE_SHARE_READ
,
442 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
,
444 if (h
!= INVALID_HANDLE_VALUE
) {
446 GetFileTime(h
, NULL
, NULL
, &writeTime
);
448 if (CompareFileTime(&finddata
->ftLastWriteTime
, &writeTime
) <= 0) {
449 WINE_TRACE("Skipping file as dest newer or same date\n");
456 /* See if exclude list provided. Note since filenames are case
457 insensitive, need to uppercase the filename before doing
459 if (!skipFile
&& (flags
& OPT_EXCLUDELIST
)) {
460 EXCLUDELIST
*pos
= excludeList
;
461 WCHAR copyFromUpper
[MAX_PATH
];
463 /* Uppercase source filename */
464 lstrcpyW(copyFromUpper
, copyFrom
);
465 CharUpperBuffW(copyFromUpper
, lstrlenW(copyFromUpper
));
467 /* Loop through testing each exclude line */
469 if (wcsstr(copyFromUpper
, pos
->name
) != NULL
) {
470 WINE_TRACE("Skipping file as matches exclude '%s'\n",
471 wine_dbgstr_w(pos
->name
));
480 /* Prompt each file if necessary */
481 if (!skipFile
&& (flags
& OPT_SRCPROMPT
)) {
484 BOOL answered
= FALSE
;
488 /* Read the Y and N characters from the resource file */
489 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
490 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
493 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT
), copyFrom
);
494 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
498 if (toupper(answer
[0]) == noChar
[0])
500 else if (toupper(answer
[0]) != yesChar
[0])
506 destAttribs
!= INVALID_FILE_ATTRIBUTES
&& !(flags
& OPT_NOPROMPT
)) {
509 BOOL answered
= FALSE
;
514 /* Read the A,Y and N characters from the resource file */
515 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
516 wcscpy(allChar
, XCOPY_LoadMessage(STRING_ALL_CHAR
));
517 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
520 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE
), copyTo
);
521 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
525 if (toupper(answer
[0]) == allChar
[0])
526 flags
|= OPT_NOPROMPT
;
527 else if (toupper(answer
[0]) == noChar
[0])
529 else if (toupper(answer
[0]) != yesChar
[0])
534 /* See if it has to exist! */
535 if (destAttribs
== INVALID_FILE_ATTRIBUTES
&& (flags
& OPT_MUSTEXIST
)) {
539 /* Output a status message */
541 if (flags
& OPT_QUIET
) {
543 } else if (flags
& OPT_FULL
) {
544 const WCHAR infostr
[] = {'%', '1', ' ', '-', '>', ' ',
547 XCOPY_wprintf(infostr
, copyFrom
, copyTo
);
549 const WCHAR infostr
[] = {'%', '1', '\n', 0};
550 XCOPY_wprintf(infostr
, copyFrom
);
553 /* If allowing overwriting of read only files, remove any
555 if ((destAttribs
& FILE_ATTRIBUTE_READONLY
) &&
556 (flags
& OPT_REPLACEREAD
)) {
557 SetFileAttributesW(copyTo
, destAttribs
& ~FILE_ATTRIBUTE_READONLY
);
561 if (flags
& OPT_SIMULATE
|| flags
& OPT_NOCOPY
) {
563 } else if (CopyFileW(copyFrom
, copyTo
, FALSE
) == 0) {
565 DWORD error
= GetLastError();
566 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL
),
567 copyFrom
, copyTo
, error
);
568 XCOPY_FailMessage(error
);
570 if (flags
& OPT_IGNOREERRORS
) {
578 /* If /M supplied, remove the archive bit after successful copy */
580 if ((srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
581 (flags
& OPT_REMOVEARCH
)) {
582 SetFileAttributesW(copyFrom
, (srcAttribs
& ~FILE_ATTRIBUTE_ARCHIVE
));
590 findres
= FindNextFileW(h
, finddata
);
594 /* Search 2 - do subdirs */
595 if (flags
& OPT_RECURSIVE
) {
596 lstrcpyW(inputpath
, srcstem
);
597 lstrcatW(inputpath
, wchr_star
);
599 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath
));
601 h
= FindFirstFileW(inputpath
, finddata
);
602 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
604 /* Only looking for dirs */
605 if ((finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) &&
606 (lstrcmpW(finddata
->cFileName
, wchr_dot
) != 0) &&
607 (lstrcmpW(finddata
->cFileName
, wchr_dotdot
) != 0)) {
609 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata
->cFileName
));
611 /* Make up recursive information */
612 lstrcpyW(inputpath
, srcstem
);
613 lstrcatW(inputpath
, finddata
->cFileName
);
614 lstrcatW(inputpath
, wchr_slash
);
616 lstrcpyW(outputpath
, deststem
);
617 if (*destspec
== 0x00) {
618 lstrcatW(outputpath
, finddata
->cFileName
);
620 /* If /E is supplied, create the directory now */
621 if ((flags
& OPT_EMPTYDIR
) &&
622 !(flags
& OPT_SIMULATE
))
623 XCOPY_CreateDirectory(outputpath
);
625 lstrcatW(outputpath
, wchr_slash
);
628 XCOPY_DoCopy(inputpath
, srcspec
, outputpath
, destspec
, flags
);
632 findres
= FindNextFileW(h
, finddata
);
640 HeapFree(GetProcessHeap(), 0, finddata
);
641 HeapFree(GetProcessHeap(), 0, inputpath
);
642 HeapFree(GetProcessHeap(), 0, outputpath
);
648 /* =========================================================================
649 XCOPY_ParseCommandLine - Parses the command line
650 ========================================================================= */
651 static BOOL
is_whitespace(WCHAR c
)
653 return c
== ' ' || c
== '\t';
656 static WCHAR
*skip_whitespace(WCHAR
*p
)
658 for (; *p
&& is_whitespace(*p
); p
++);
662 /* Windows XCOPY uses a simplified command line parsing algorithm
663 that lacks the escaped-quote logic of build_argv(), because
664 literal double quotes are illegal in any of its arguments.
665 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
666 static int find_end_of_word(const WCHAR
*word
, WCHAR
**end
)
668 BOOL in_quotes
= FALSE
;
669 const WCHAR
*ptr
= word
;
671 for (; *ptr
!= '\0' && *ptr
!= '"' &&
672 (in_quotes
|| !is_whitespace(*ptr
)); ptr
++);
674 in_quotes
= !in_quotes
;
677 /* Odd number of double quotes is illegal for XCOPY */
678 if (in_quotes
&& *ptr
== '\0')
680 if (*ptr
== '\0' || (!in_quotes
&& is_whitespace(*ptr
)))
687 /* Remove all double quotes from a word */
688 static void strip_quotes(WCHAR
*word
, WCHAR
**end
)
691 for (rp
= word
, wp
= word
; *rp
!= '\0'; rp
++) {
702 static int XCOPY_ParseCommandLine(WCHAR
*suppliedsource
,
703 WCHAR
*supplieddestination
, DWORD
*pflags
)
705 const WCHAR EXCLUDE
[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
706 DWORD flags
= *pflags
;
707 WCHAR
*cmdline
, *word
, *end
, *next
;
708 int rc
= RC_INITERROR
;
710 cmdline
= _wcsdup(GetCommandLineW());
714 /* Skip first arg, which is the program name */
715 if ((rc
= find_end_of_word(cmdline
, &word
)) != RC_OK
)
717 word
= skip_whitespace(word
);
722 if ((rc
= find_end_of_word(word
, &end
)) != RC_OK
)
725 next
= skip_whitespace(end
);
728 strip_quotes(word
, &end
);
729 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word
));
731 /* First non-switch parameter is source, second is destination */
733 if (suppliedsource
[0] == 0x00) {
734 lstrcpyW(suppliedsource
, word
);
735 } else if (supplieddestination
[0] == 0x00) {
736 lstrcpyW(supplieddestination
, word
);
738 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS
));
742 /* Process all the switch options
743 Note: Windows docs say /P prompts when dest is created
744 but tests show it is done for each src file
745 regardless of the destination */
746 switch (toupper(word
[1])) {
747 case 'I': flags
|= OPT_ASSUMEDIR
; break;
748 case 'S': flags
|= OPT_RECURSIVE
; break;
749 case 'Q': flags
|= OPT_QUIET
; break;
750 case 'F': flags
|= OPT_FULL
; break;
751 case 'L': flags
|= OPT_SIMULATE
; break;
752 case 'W': flags
|= OPT_PAUSE
; break;
753 case 'T': flags
|= OPT_NOCOPY
| OPT_RECURSIVE
; break;
754 case 'Y': flags
|= OPT_NOPROMPT
; break;
755 case 'N': flags
|= OPT_SHORTNAME
; break;
756 case 'U': flags
|= OPT_MUSTEXIST
; break;
757 case 'R': flags
|= OPT_REPLACEREAD
; break;
758 case 'H': flags
|= OPT_COPYHIDSYS
; break;
759 case 'C': flags
|= OPT_IGNOREERRORS
; break;
760 case 'P': flags
|= OPT_SRCPROMPT
; break;
761 case 'A': flags
|= OPT_ARCHIVEONLY
; break;
762 case 'M': flags
|= OPT_ARCHIVEONLY
|
763 OPT_REMOVEARCH
; break;
765 /* E can be /E or /EXCLUDE */
766 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT
,
767 NORM_IGNORECASE
| SORT_STRINGSORT
,
769 EXCLUDE
, -1) == CSTR_EQUAL
) {
770 if (XCOPY_ProcessExcludeList(&word
[9])) {
771 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
773 } else flags
|= OPT_EXCLUDELIST
;
774 } else flags
|= OPT_EMPTYDIR
| OPT_RECURSIVE
;
777 /* D can be /D or /D: */
778 case 'D': if (word
[2]==':' && isdigit(word
[3])) {
780 WCHAR
*pos
= &word
[3];
781 BOOL isError
= FALSE
;
782 memset(&st
, 0x00, sizeof(st
));
784 /* Microsoft xcopy's usage message implies that the date
785 * format depends on the locale, but that is false.
786 * It is hardcoded to month-day-year.
788 st
.wMonth
= _wtol(pos
);
789 while (*pos
&& isdigit(*pos
)) pos
++;
790 if (*pos
++ != '-') isError
= TRUE
;
793 st
.wDay
= _wtol(pos
);
794 while (*pos
&& isdigit(*pos
)) pos
++;
795 if (*pos
++ != '-') isError
= TRUE
;
799 st
.wYear
= _wtol(pos
);
800 while (*pos
&& isdigit(*pos
)) pos
++;
801 if (st
.wYear
< 100) st
.wYear
+=2000;
804 if (!isError
&& SystemTimeToFileTime(&st
, &dateRange
)) {
806 WCHAR datestring
[32], timestring
[32];
808 flags
|= OPT_DATERANGE
;
811 FileTimeToSystemTime (&dateRange
, &st
);
812 GetDateFormatW(0, DATE_SHORTDATE
, &st
, NULL
, datestring
,
813 sizeof(datestring
)/sizeof(WCHAR
));
814 GetTimeFormatW(0, TIME_NOSECONDS
, &st
,
815 NULL
, timestring
, sizeof(timestring
)/sizeof(WCHAR
));
817 WINE_TRACE("Date being used is: %s %s\n",
818 wine_dbgstr_w(datestring
), wine_dbgstr_w(timestring
));
820 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
824 flags
|= OPT_DATENEWER
;
828 case '-': if (toupper(word
[2])=='Y')
829 flags
&= ~OPT_NOPROMPT
;
831 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP
));
835 WINE_FIXME("ignoring /V\n");
838 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word
));
839 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM
), word
);
846 /* Default the destination if not supplied */
847 if (supplieddestination
[0] == 0x00)
848 lstrcpyW(supplieddestination
, wchr_dot
);
859 /* =========================================================================
860 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
861 converts it into a stem and a filespec
862 ========================================================================= */
863 static int XCOPY_ProcessSourceParm(WCHAR
*suppliedsource
, WCHAR
*stem
,
864 WCHAR
*spec
, DWORD flags
)
866 WCHAR actualsource
[MAX_PATH
];
872 * Validate the source, expanding to full path ensuring it exists
874 if (GetFullPathNameW(suppliedsource
, MAX_PATH
, actualsource
, NULL
) == 0) {
875 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
879 /* If full names required, convert to using the full path */
880 if (flags
& OPT_FULL
) {
881 lstrcpyW(suppliedsource
, actualsource
);
885 * Work out the stem of the source
888 /* If a directory is supplied, use that as-is (either fully or
890 If a filename is supplied + a directory or drive path, use that
893 If no directory or path specified, add eg. C:
894 stem is Drive/Directory is bit up to last \ (or first :)
895 spec is bit after that */
897 starPos
= wcschr(suppliedsource
, '*');
898 questPos
= wcschr(suppliedsource
, '?');
899 if (starPos
|| questPos
) {
900 attribs
= 0x00; /* Ensures skips invalid or directory check below */
902 attribs
= GetFileAttributesW(actualsource
);
905 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
906 XCOPY_FailMessage(GetLastError());
910 stem should be exactly as supplied plus a '\', unless it was
911 eg. C: in which case no slash required */
912 } else if (attribs
& FILE_ATTRIBUTE_DIRECTORY
) {
915 WINE_TRACE("Directory supplied\n");
916 lstrcpyW(stem
, suppliedsource
);
917 lastChar
= stem
[lstrlenW(stem
)-1];
918 if (lastChar
!= '\\' && lastChar
!= ':') {
919 lstrcatW(stem
, wchr_slash
);
921 lstrcpyW(spec
, wchr_star
);
923 /* File or wildcard search:
925 Up to and including last slash if directory path supplied
926 If c:filename supplied, just the c:
927 Otherwise stem should be the current drive letter + ':' */
931 WINE_TRACE("Filename supplied\n");
932 lastDir
= wcsrchr(suppliedsource
, '\\');
935 lstrcpyW(stem
, suppliedsource
);
936 stem
[(lastDir
-suppliedsource
) + 1] = 0x00;
937 lstrcpyW(spec
, (lastDir
+1));
938 } else if (suppliedsource
[1] == ':') {
939 lstrcpyW(stem
, suppliedsource
);
941 lstrcpyW(spec
, suppliedsource
+2);
943 WCHAR curdir
[MAXSTRING
];
944 GetCurrentDirectoryW(sizeof(curdir
)/sizeof(WCHAR
), curdir
);
948 lstrcpyW(spec
, suppliedsource
);
955 /* =========================================================================
956 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
957 converts it into a stem
958 ========================================================================= */
959 static int XCOPY_ProcessDestParm(WCHAR
*supplieddestination
, WCHAR
*stem
, WCHAR
*spec
,
960 WCHAR
*srcspec
, DWORD flags
)
962 WCHAR actualdestination
[MAX_PATH
];
967 * Validate the source, expanding to full path ensuring it exists
969 if (GetFullPathNameW(supplieddestination
, MAX_PATH
, actualdestination
, NULL
) == 0) {
970 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
974 /* Destination is either a directory or a file */
975 attribs
= GetFileAttributesW(actualdestination
);
977 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
979 /* If /I supplied and wildcard copy, assume directory */
980 /* Also if destination ends with backslash */
981 if ((flags
& OPT_ASSUMEDIR
&&
982 (wcschr(srcspec
, '?') || wcschr(srcspec
, '*'))) ||
983 (supplieddestination
[lstrlenW(supplieddestination
)-1] == '\\')) {
989 char answer
[10] = "";
993 /* Read the F and D characters from the resource file */
994 wcscpy(fileChar
, XCOPY_LoadMessage(STRING_FILE_CHAR
));
995 wcscpy(dirChar
, XCOPY_LoadMessage(STRING_DIR_CHAR
));
997 while (answer
[0] != fileChar
[0] && answer
[0] != dirChar
[0]) {
998 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR
), supplieddestination
);
1000 ReadFile(GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
), &count
, NULL
);
1001 WINE_TRACE("User answer %c\n", answer
[0]);
1003 answer
[0] = toupper(answer
[0]);
1006 if (answer
[0] == dirChar
[0]) {
1013 isDir
= (attribs
& FILE_ATTRIBUTE_DIRECTORY
);
1017 lstrcpyW(stem
, actualdestination
);
1020 /* Ensure ends with a '\' */
1021 if (stem
[lstrlenW(stem
)-1] != '\\') {
1022 lstrcatW(stem
, wchr_slash
);
1026 WCHAR drive
[MAX_PATH
];
1027 WCHAR dir
[MAX_PATH
];
1028 WCHAR fname
[MAX_PATH
];
1029 WCHAR ext
[MAX_PATH
];
1030 _wsplitpath(actualdestination
, drive
, dir
, fname
, ext
);
1031 lstrcpyW(stem
, drive
);
1032 lstrcatW(stem
, dir
);
1033 lstrcpyW(spec
, fname
);
1034 lstrcatW(spec
, ext
);
1040 /* =========================================================================
1041 main - Main entrypoint for the xcopy command
1043 Processes the args, and drives the actual copying
1044 ========================================================================= */
1045 int wmain (int argc
, WCHAR
*argvW
[])
1048 WCHAR suppliedsource
[MAX_PATH
] = {0}; /* As supplied on the cmd line */
1049 WCHAR supplieddestination
[MAX_PATH
] = {0};
1050 WCHAR sourcestem
[MAX_PATH
] = {0}; /* Stem of source */
1051 WCHAR sourcespec
[MAX_PATH
] = {0}; /* Filespec of source */
1052 WCHAR destinationstem
[MAX_PATH
] = {0}; /* Stem of destination */
1053 WCHAR destinationspec
[MAX_PATH
] = {0}; /* Filespec of destination */
1054 WCHAR copyCmd
[MAXSTRING
]; /* COPYCMD env var */
1055 DWORD flags
= 0; /* Option flags */
1056 const WCHAR PROMPTSTR1
[] = {'/', 'Y', 0};
1057 const WCHAR PROMPTSTR2
[] = {'/', 'y', 0};
1058 const WCHAR COPYCMD
[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1060 /* Preinitialize flags based on COPYCMD */
1061 if (GetEnvironmentVariableW(COPYCMD
, copyCmd
, MAXSTRING
)) {
1062 if (wcsstr(copyCmd
, PROMPTSTR1
) != NULL
||
1063 wcsstr(copyCmd
, PROMPTSTR2
) != NULL
) {
1064 flags
|= OPT_NOPROMPT
;
1068 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1069 wine, but on windows these can be normal files. At least one installer
1070 uses files such as .packlist and (validly) expects them to be copied.
1071 Under wine, if we do not copy hidden files by default then they get
1073 flags
|= OPT_COPYHIDSYS
;
1076 * Parse the command line
1078 if ((rc
= XCOPY_ParseCommandLine(suppliedsource
, supplieddestination
,
1079 &flags
)) != RC_OK
) {
1086 /* Trace out the supplied information */
1087 WINE_TRACE("Supplied parameters:\n");
1088 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource
));
1089 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination
));
1091 /* Extract required information from source specification */
1092 rc
= XCOPY_ProcessSourceParm(suppliedsource
, sourcestem
, sourcespec
, flags
);
1093 if (rc
!= RC_OK
) return rc
;
1095 /* Extract required information from destination specification */
1096 rc
= XCOPY_ProcessDestParm(supplieddestination
, destinationstem
,
1097 destinationspec
, sourcespec
, flags
);
1098 if (rc
!= RC_OK
) return rc
;
1100 /* Trace out the resulting information */
1101 WINE_TRACE("Resolved parameters:\n");
1102 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem
));
1103 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec
));
1104 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem
));
1105 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec
));
1107 /* Pause if necessary */
1108 if (flags
& OPT_PAUSE
) {
1112 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE
));
1113 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), pausestr
, sizeof(pausestr
),
1117 /* Now do the hard work... */
1118 rc
= XCOPY_DoCopy(sourcestem
, sourcespec
,
1119 destinationstem
, destinationspec
,
1122 /* Clear up exclude list allocated memory */
1123 while (excludeList
) {
1124 EXCLUDELIST
*pos
= excludeList
;
1125 excludeList
= excludeList
-> next
;
1126 HeapFree(GetProcessHeap(), 0, pos
->name
);
1127 HeapFree(GetProcessHeap(), 0, pos
);
1130 /* Finished - print trailer and exit */
1131 if (flags
& OPT_SIMULATE
) {
1132 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY
), filesCopied
);
1133 } else if (!(flags
& OPT_NOCOPY
)) {
1134 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY
), filesCopied
);
1136 if (rc
== RC_OK
&& filesCopied
== 0) rc
= RC_NOFILES
;