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 * Documented valid return codes are:
36 * 1 - No files found to copy (*1)
37 * 2 - CTRL+C during copy
38 * 4 - Initialization error, or invalid source specification
39 * 5 - Disk write error
41 * (*1) Testing shows return code 1 is never returned
48 #include <wine/debug.h>
49 #include <wine/unicode.h>
52 WINE_DEFAULT_DEBUG_CHANNEL(xcopy
);
56 typedef struct _EXCLUDELIST
58 struct _EXCLUDELIST
*next
;
63 /* Global variables */
64 static ULONG filesCopied
= 0; /* Number of files copied */
65 static EXCLUDELIST
*excludeList
= NULL
; /* Excluded strings list */
66 static FILETIME dateRange
; /* Date range to copy after*/
67 static const WCHAR wchr_slash
[] = {'\\', 0};
68 static const WCHAR wchr_star
[] = {'*', 0};
69 static const WCHAR wchr_dot
[] = {'.', 0};
70 static const WCHAR wchr_dotdot
[] = {'.', '.', 0};
73 /* To minimize stack usage during recursion, some temporary variables
75 static WCHAR copyFrom
[MAX_PATH
];
76 static WCHAR copyTo
[MAX_PATH
];
79 /* =========================================================================
80 * Load a string from the resource file, handling any error
81 * Returns string retrieved from resource file
82 * ========================================================================= */
83 static WCHAR
*XCOPY_LoadMessage(UINT id
) {
84 static WCHAR msg
[MAXSTRING
];
85 const WCHAR failedMsg
[] = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
87 if (!LoadStringW(GetModuleHandleW(NULL
), id
, msg
, ARRAY_SIZE(msg
))) {
88 WINE_FIXME("LoadString failed with %d\n", GetLastError());
89 lstrcpyW(msg
, failedMsg
);
94 /* =========================================================================
95 * Output a formatted unicode string. Ideally this will go to the console
96 * and hence required WriteConsoleW to output it, however if file i/o is
97 * redirected, it needs to be WriteFile'd using OEM (not ANSI) format
98 * ========================================================================= */
99 static int WINAPIV
XCOPY_wprintf(const WCHAR
*format
, ...) {
101 static WCHAR
*output_bufW
= NULL
;
102 static char *output_bufA
= NULL
;
103 static BOOL toConsole
= TRUE
;
104 static BOOL traceOutput
= FALSE
;
105 #define MAX_WRITECONSOLE_SIZE 65535
113 * Allocate buffer to use when writing to console
114 * Note: Not freed - memory will be allocated once and released when
118 if (!output_bufW
) output_bufW
= HeapAlloc(GetProcessHeap(), 0,
119 MAX_WRITECONSOLE_SIZE
*sizeof(WCHAR
));
121 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
125 __ms_va_start(parms
, format
);
126 SetLastError(NO_ERROR
);
127 len
= FormatMessageW(FORMAT_MESSAGE_FROM_STRING
, format
, 0, 0, output_bufW
,
128 MAX_WRITECONSOLE_SIZE
/sizeof(*output_bufW
), &parms
);
130 if (len
== 0 && GetLastError() != NO_ERROR
) {
131 WINE_FIXME("Could not format string: le=%u, fmt=%s\n", GetLastError(), wine_dbgstr_w(format
));
135 /* Try to write as unicode whenever we think it's a console */
137 res
= WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE
),
138 output_bufW
, len
, &nOut
, NULL
);
141 /* If writing to console has failed (ever) we assume it's file
142 i/o so convert to OEM codepage and output */
144 BOOL usedDefaultChar
= FALSE
;
145 DWORD convertedChars
;
150 * Allocate buffer to use when writing to file. Not freed, as above
152 if (!output_bufA
) output_bufA
= HeapAlloc(GetProcessHeap(), 0,
153 MAX_WRITECONSOLE_SIZE
);
155 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
159 /* Convert to OEM, then output */
160 convertedChars
= WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW
,
161 len
, output_bufA
, MAX_WRITECONSOLE_SIZE
,
162 "?", &usedDefaultChar
);
163 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE
), output_bufA
, convertedChars
,
167 /* Trace whether screen or console */
169 WINE_TRACE("Writing to console? (%d)\n", toConsole
);
175 /* =========================================================================
176 * Load a string for a system error and writes it to the screen
177 * Returns string retrieved from resource file
178 * ========================================================================= */
179 static void XCOPY_FailMessage(DWORD err
) {
183 status
= FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER
|
184 FORMAT_MESSAGE_FROM_SYSTEM
,
186 (LPWSTR
) &lpMsgBuf
, 0, NULL
);
188 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
189 err
, GetLastError());
191 const WCHAR infostr
[] = {'%', '1', '\n', 0};
192 XCOPY_wprintf(infostr
, lpMsgBuf
);
193 LocalFree ((HLOCAL
)lpMsgBuf
);
198 /* =========================================================================
199 * Routine copied from cmd.exe md command -
200 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
201 * dir2 if they do not already exist.
202 * ========================================================================= */
203 static BOOL
XCOPY_CreateDirectory(const WCHAR
* path
)
209 new_path
= HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR
) * (lstrlenW(path
)+1));
210 lstrcpyW(new_path
,path
);
212 while ((len
= lstrlenW(new_path
)) && new_path
[len
- 1] == '\\')
213 new_path
[len
- 1] = 0;
215 while (!CreateDirectoryW(new_path
,NULL
))
218 DWORD last_error
= GetLastError();
219 if (last_error
== ERROR_ALREADY_EXISTS
)
222 if (last_error
!= ERROR_PATH_NOT_FOUND
)
228 if (!(slash
= wcsrchr(new_path
,'\\')) && ! (slash
= wcsrchr(new_path
,'/')))
234 len
= slash
- new_path
;
236 if (!XCOPY_CreateDirectory(new_path
))
241 new_path
[len
] = '\\';
243 HeapFree(GetProcessHeap(),0,new_path
);
247 /* =========================================================================
248 * Process a single file from the /EXCLUDE: file list, building up a list
249 * of substrings to avoid copying
250 * Returns TRUE on any failure
251 * ========================================================================= */
252 static BOOL
XCOPY_ProcessExcludeFile(WCHAR
* filename
, WCHAR
* endOfName
) {
254 WCHAR endChar
= *endOfName
;
255 WCHAR buffer
[MAXSTRING
];
257 const WCHAR readTextMode
[] = {'r', 't', 0};
259 /* Null terminate the filename (temporarily updates the filename hence
264 inFile
= _wfopen(filename
, readTextMode
);
265 if (inFile
== NULL
) {
266 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL
), filename
);
267 *endOfName
= endChar
;
271 /* Process line by line */
272 while (fgetws(buffer
, ARRAY_SIZE(buffer
), inFile
) != NULL
) {
273 EXCLUDELIST
*thisEntry
;
274 int length
= lstrlenW(buffer
);
276 /* If more than CRLF */
278 buffer
[length
-1] = 0; /* strip CRLF */
279 thisEntry
= HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST
));
280 thisEntry
->next
= excludeList
;
281 excludeList
= thisEntry
;
282 thisEntry
->name
= HeapAlloc(GetProcessHeap(), 0,
283 (length
* sizeof(WCHAR
))+1);
284 lstrcpyW(thisEntry
->name
, buffer
);
285 CharUpperBuffW(thisEntry
->name
, length
);
286 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry
->name
));
290 /* See if EOF or error occurred */
292 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL
), filename
);
293 *endOfName
= endChar
;
298 /* Revert the input string to original form, and cleanup + return */
299 *endOfName
= endChar
;
304 /* =========================================================================
305 * Process the /EXCLUDE: file list, building up a list of substrings to
307 * Returns TRUE on any failure
308 * ========================================================================= */
309 static BOOL
XCOPY_ProcessExcludeList(WCHAR
* parms
) {
311 WCHAR
*filenameStart
= parms
;
313 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms
));
316 while (*parms
&& *parms
!= ' ' && *parms
!= '/') {
318 /* If found '+' then process the file found so far */
320 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
323 filenameStart
= parms
+1;
328 if (filenameStart
!= parms
) {
329 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
337 /* =========================================================================
338 XCOPY_DoCopy - Recursive function to copy files based on input parms
341 This works by using FindFirstFile supplying the source stem and spec.
342 If results are found, any non-directory ones are processed
343 Then, if /S or /E is supplied, another search is made just for
344 directories, and this function is called again for that directory
346 ========================================================================= */
347 static int XCOPY_DoCopy(WCHAR
*srcstem
, WCHAR
*srcspec
,
348 WCHAR
*deststem
, WCHAR
*destspec
,
351 WIN32_FIND_DATAW
*finddata
;
354 WCHAR
*inputpath
, *outputpath
;
355 BOOL copiedFile
= FALSE
;
356 DWORD destAttribs
, srcAttribs
;
360 /* Allocate some working memory on heap to minimize footprint */
361 finddata
= HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW
));
362 inputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
363 outputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
365 /* Build the search info into a single parm */
366 lstrcpyW(inputpath
, srcstem
);
367 lstrcatW(inputpath
, srcspec
);
369 /* Search 1 - Look for matching files */
370 h
= FindFirstFileW(inputpath
, finddata
);
371 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
375 /* Ignore . and .. */
376 if (lstrcmpW(finddata
->cFileName
, wchr_dot
)==0 ||
377 lstrcmpW(finddata
->cFileName
, wchr_dotdot
)==0 ||
378 finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) {
380 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata
->cFileName
));
383 /* Get the filename information */
384 lstrcpyW(copyFrom
, srcstem
);
385 if (flags
& OPT_SHORTNAME
) {
386 lstrcatW(copyFrom
, finddata
->cAlternateFileName
);
388 lstrcatW(copyFrom
, finddata
->cFileName
);
391 lstrcpyW(copyTo
, deststem
);
392 if (*destspec
== 0x00) {
393 if (flags
& OPT_SHORTNAME
) {
394 lstrcatW(copyTo
, finddata
->cAlternateFileName
);
396 lstrcatW(copyTo
, finddata
->cFileName
);
399 lstrcatW(copyTo
, destspec
);
403 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom
),
404 wine_dbgstr_w(copyTo
));
405 if (!copiedFile
&& !(flags
& OPT_SIMULATE
)) XCOPY_CreateDirectory(deststem
);
407 /* See if allowed to copy it */
408 srcAttribs
= GetFileAttributesW(copyFrom
);
409 WINE_TRACE("Source attribs: %d\n", srcAttribs
);
411 if ((srcAttribs
& FILE_ATTRIBUTE_HIDDEN
) ||
412 (srcAttribs
& FILE_ATTRIBUTE_SYSTEM
)) {
414 if (!(flags
& OPT_COPYHIDSYS
)) {
419 if (!(srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
420 (flags
& OPT_ARCHIVEONLY
)) {
424 /* See if file exists */
425 destAttribs
= GetFileAttributesW(copyTo
);
426 WINE_TRACE("Dest attribs: %d\n", srcAttribs
);
428 /* Check date ranges if a destination file already exists */
429 if (!skipFile
&& (flags
& OPT_DATERANGE
) &&
430 (CompareFileTime(&finddata
->ftLastWriteTime
, &dateRange
) < 0)) {
431 WINE_TRACE("Skipping file as modified date too old\n");
435 /* If just /D supplied, only overwrite if src newer than dest */
436 if (!skipFile
&& (flags
& OPT_DATENEWER
) &&
437 (destAttribs
!= INVALID_FILE_ATTRIBUTES
)) {
438 HANDLE h
= CreateFileW(copyTo
, GENERIC_READ
, FILE_SHARE_READ
,
439 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
,
441 if (h
!= INVALID_HANDLE_VALUE
) {
443 GetFileTime(h
, NULL
, NULL
, &writeTime
);
445 if (CompareFileTime(&finddata
->ftLastWriteTime
, &writeTime
) <= 0) {
446 WINE_TRACE("Skipping file as dest newer or same date\n");
453 /* See if exclude list provided. Note since filenames are case
454 insensitive, need to uppercase the filename before doing
456 if (!skipFile
&& (flags
& OPT_EXCLUDELIST
)) {
457 EXCLUDELIST
*pos
= excludeList
;
458 WCHAR copyFromUpper
[MAX_PATH
];
460 /* Uppercase source filename */
461 lstrcpyW(copyFromUpper
, copyFrom
);
462 CharUpperBuffW(copyFromUpper
, lstrlenW(copyFromUpper
));
464 /* Loop through testing each exclude line */
466 if (wcsstr(copyFromUpper
, pos
->name
) != NULL
) {
467 WINE_TRACE("Skipping file as matches exclude '%s'\n",
468 wine_dbgstr_w(pos
->name
));
477 /* Prompt each file if necessary */
478 if (!skipFile
&& (flags
& OPT_SRCPROMPT
)) {
481 BOOL answered
= FALSE
;
485 /* Read the Y and N characters from the resource file */
486 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
487 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
490 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT
), copyFrom
);
491 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
495 if (toupper(answer
[0]) == noChar
[0])
497 else if (toupper(answer
[0]) != yesChar
[0])
503 destAttribs
!= INVALID_FILE_ATTRIBUTES
&& !(flags
& OPT_NOPROMPT
)) {
506 BOOL answered
= FALSE
;
511 /* Read the A,Y and N characters from the resource file */
512 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
513 wcscpy(allChar
, XCOPY_LoadMessage(STRING_ALL_CHAR
));
514 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
517 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE
), copyTo
);
518 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
522 if (toupper(answer
[0]) == allChar
[0])
523 flags
|= OPT_NOPROMPT
;
524 else if (toupper(answer
[0]) == noChar
[0])
526 else if (toupper(answer
[0]) != yesChar
[0])
531 /* See if it has to exist! */
532 if (destAttribs
== INVALID_FILE_ATTRIBUTES
&& (flags
& OPT_MUSTEXIST
)) {
536 /* Output a status message */
538 if (flags
& OPT_QUIET
) {
540 } else if (flags
& OPT_FULL
) {
541 const WCHAR infostr
[] = {'%', '1', ' ', '-', '>', ' ',
544 XCOPY_wprintf(infostr
, copyFrom
, copyTo
);
546 const WCHAR infostr
[] = {'%', '1', '\n', 0};
547 XCOPY_wprintf(infostr
, copyFrom
);
550 /* If allowing overwriting of read only files, remove any
552 if ((destAttribs
& FILE_ATTRIBUTE_READONLY
) &&
553 (flags
& OPT_REPLACEREAD
)) {
554 SetFileAttributesW(copyTo
, destAttribs
& ~FILE_ATTRIBUTE_READONLY
);
558 if (flags
& OPT_SIMULATE
|| flags
& OPT_NOCOPY
) {
560 } else if (CopyFileW(copyFrom
, copyTo
, FALSE
) == 0) {
562 DWORD error
= GetLastError();
563 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL
),
564 copyFrom
, copyTo
, error
);
565 XCOPY_FailMessage(error
);
567 if (flags
& OPT_IGNOREERRORS
) {
576 /* If keeping attributes, update the destination attributes
577 otherwise remove the read only attribute */
578 if (flags
& OPT_KEEPATTRS
) {
579 SetFileAttributesW(copyTo
, srcAttribs
| FILE_ATTRIBUTE_ARCHIVE
);
581 SetFileAttributesW(copyTo
,
582 (GetFileAttributesW(copyTo
) & ~FILE_ATTRIBUTE_READONLY
));
585 /* If /M supplied, remove the archive bit after successful copy */
586 if ((srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
587 (flags
& OPT_REMOVEARCH
)) {
588 SetFileAttributesW(copyFrom
, (srcAttribs
& ~FILE_ATTRIBUTE_ARCHIVE
));
597 findres
= FindNextFileW(h
, finddata
);
601 /* Search 2 - do subdirs */
602 if (flags
& OPT_RECURSIVE
) {
604 /* If /E is supplied, create the directory now */
605 if ((flags
& OPT_EMPTYDIR
) &&
606 !(flags
& OPT_SIMULATE
)) {
607 XCOPY_CreateDirectory(deststem
);
610 lstrcpyW(inputpath
, srcstem
);
611 lstrcatW(inputpath
, wchr_star
);
613 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath
));
615 h
= FindFirstFileW(inputpath
, finddata
);
616 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
618 /* Only looking for dirs */
619 if ((finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) &&
620 (lstrcmpW(finddata
->cFileName
, wchr_dot
) != 0) &&
621 (lstrcmpW(finddata
->cFileName
, wchr_dotdot
) != 0)) {
623 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata
->cFileName
));
625 /* Make up recursive information */
626 lstrcpyW(inputpath
, srcstem
);
627 lstrcatW(inputpath
, finddata
->cFileName
);
628 lstrcatW(inputpath
, wchr_slash
);
630 lstrcpyW(outputpath
, deststem
);
631 if (*destspec
== 0x00) {
632 lstrcatW(outputpath
, finddata
->cFileName
);
633 lstrcatW(outputpath
, wchr_slash
);
636 XCOPY_DoCopy(inputpath
, srcspec
, outputpath
, destspec
, flags
);
640 findres
= FindNextFileW(h
, finddata
);
648 HeapFree(GetProcessHeap(), 0, finddata
);
649 HeapFree(GetProcessHeap(), 0, inputpath
);
650 HeapFree(GetProcessHeap(), 0, outputpath
);
656 /* =========================================================================
657 XCOPY_ParseCommandLine - Parses the command line
658 ========================================================================= */
659 static inline BOOL
is_whitespace(WCHAR c
)
661 return c
== ' ' || c
== '\t';
664 static WCHAR
*skip_whitespace(WCHAR
*p
)
666 for (; *p
&& is_whitespace(*p
); p
++);
670 static inline BOOL
is_digit(WCHAR c
)
672 return c
>= '0' && c
<= '9';
675 /* Windows XCOPY uses a simplified command line parsing algorithm
676 that lacks the escaped-quote logic of build_argv(), because
677 literal double quotes are illegal in any of its arguments.
678 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
679 static int find_end_of_word(const WCHAR
*word
, WCHAR
**end
)
681 BOOL in_quotes
= FALSE
;
682 const WCHAR
*ptr
= word
;
684 for (; *ptr
!= '\0' && *ptr
!= '"' &&
685 (in_quotes
|| !is_whitespace(*ptr
)); ptr
++);
687 in_quotes
= !in_quotes
;
690 /* Odd number of double quotes is illegal for XCOPY */
691 if (in_quotes
&& *ptr
== '\0')
693 if (*ptr
== '\0' || (!in_quotes
&& is_whitespace(*ptr
)))
700 /* Remove all double quotes from a word */
701 static void strip_quotes(WCHAR
*word
, WCHAR
**end
)
704 for (rp
= word
, wp
= word
; *rp
!= '\0'; rp
++) {
715 static int XCOPY_ParseCommandLine(WCHAR
*suppliedsource
,
716 WCHAR
*supplieddestination
, DWORD
*pflags
)
718 const WCHAR EXCLUDE
[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
719 DWORD flags
= *pflags
;
720 WCHAR
*cmdline
, *word
, *end
, *next
;
721 int rc
= RC_INITERROR
;
723 cmdline
= _wcsdup(GetCommandLineW());
727 /* Skip first arg, which is the program name */
728 if ((rc
= find_end_of_word(cmdline
, &word
)) != RC_OK
)
730 word
= skip_whitespace(word
);
735 if ((rc
= find_end_of_word(word
, &end
)) != RC_OK
)
738 next
= skip_whitespace(end
);
741 strip_quotes(word
, &end
);
742 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word
));
744 /* First non-switch parameter is source, second is destination */
746 if (suppliedsource
[0] == 0x00) {
747 lstrcpyW(suppliedsource
, word
);
748 } else if (supplieddestination
[0] == 0x00) {
749 lstrcpyW(supplieddestination
, word
);
751 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS
));
755 /* Process all the switch options
756 Note: Windows docs say /P prompts when dest is created
757 but tests show it is done for each src file
758 regardless of the destination */
765 switch (toupper(word
[1])) {
766 case 'I': flags
|= OPT_ASSUMEDIR
; break;
767 case 'S': flags
|= OPT_RECURSIVE
; break;
768 case 'Q': flags
|= OPT_QUIET
; break;
769 case 'F': flags
|= OPT_FULL
; break;
770 case 'L': flags
|= OPT_SIMULATE
; break;
771 case 'W': flags
|= OPT_PAUSE
; break;
772 case 'T': flags
|= OPT_NOCOPY
| OPT_RECURSIVE
; break;
773 case 'Y': flags
|= OPT_NOPROMPT
; break;
774 case 'N': flags
|= OPT_SHORTNAME
; break;
775 case 'U': flags
|= OPT_MUSTEXIST
; break;
776 case 'R': flags
|= OPT_REPLACEREAD
; break;
777 case 'K': flags
|= OPT_KEEPATTRS
; break;
778 case 'H': flags
|= OPT_COPYHIDSYS
; break;
779 case 'C': flags
|= OPT_IGNOREERRORS
; break;
780 case 'P': flags
|= OPT_SRCPROMPT
; break;
781 case 'A': flags
|= OPT_ARCHIVEONLY
; break;
782 case 'M': flags
|= OPT_ARCHIVEONLY
|
783 OPT_REMOVEARCH
; break;
785 /* E can be /E or /EXCLUDE */
786 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT
,
787 NORM_IGNORECASE
| SORT_STRINGSORT
,
789 EXCLUDE
, -1) == CSTR_EQUAL
) {
790 if (XCOPY_ProcessExcludeList(&word
[9])) {
791 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
794 flags
|= OPT_EXCLUDELIST
;
796 /* Do not support concatenated switches onto exclude lists yet */
800 flags
|= OPT_EMPTYDIR
| OPT_RECURSIVE
;
804 /* D can be /D or /D: */
805 case 'D': if (word
[2]==':' && is_digit(word
[3])) {
807 WCHAR
*pos
= &word
[3];
808 BOOL isError
= FALSE
;
809 memset(&st
, 0x00, sizeof(st
));
811 /* Microsoft xcopy's usage message implies that the date
812 * format depends on the locale, but that is false.
813 * It is hardcoded to month-day-year.
815 st
.wMonth
= _wtol(pos
);
816 while (*pos
&& is_digit(*pos
)) pos
++;
817 if (*pos
++ != '-') isError
= TRUE
;
820 st
.wDay
= _wtol(pos
);
821 while (*pos
&& is_digit(*pos
)) pos
++;
822 if (*pos
++ != '-') isError
= TRUE
;
826 st
.wYear
= _wtol(pos
);
827 while (*pos
&& is_digit(*pos
)) pos
++;
828 if (st
.wYear
< 100) st
.wYear
+=2000;
831 /* Handle switches straight after the supplied date */
834 if (!isError
&& SystemTimeToFileTime(&st
, &dateRange
)) {
836 WCHAR datestring
[32], timestring
[32];
838 flags
|= OPT_DATERANGE
;
841 FileTimeToSystemTime (&dateRange
, &st
);
842 GetDateFormatW(0, DATE_SHORTDATE
, &st
, NULL
, datestring
,
843 ARRAY_SIZE(datestring
));
844 GetTimeFormatW(0, TIME_NOSECONDS
, &st
,
845 NULL
, timestring
, ARRAY_SIZE(timestring
));
847 WINE_TRACE("Date being used is: %s %s\n",
848 wine_dbgstr_w(datestring
), wine_dbgstr_w(timestring
));
850 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
854 flags
|= OPT_DATENEWER
;
858 case '-': if (toupper(word
[2])=='Y') {
859 flags
&= ~OPT_NOPROMPT
;
860 rest
= &word
[3]; /* Skip over 3 characters */
863 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP
));
867 WINE_FIXME("ignoring /V\n");
870 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word
));
871 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM
), word
);
875 /* Unless overridden above, skip over the '/' and the first character */
876 if (rest
== NULL
) rest
= &word
[2];
878 /* By now, rest should point either to the null after the
879 switch, or the beginning of the next switch if there
880 was no whitespace between them */
881 if (!skip
&& *rest
&& *rest
!= '/') {
882 WINE_FIXME("Unexpected characters found and ignored '%s'\n", wine_dbgstr_w(rest
));
892 /* Default the destination if not supplied */
893 if (supplieddestination
[0] == 0x00)
894 lstrcpyW(supplieddestination
, wchr_dot
);
905 /* =========================================================================
906 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
907 converts it into a stem and a filespec
908 ========================================================================= */
909 static int XCOPY_ProcessSourceParm(WCHAR
*suppliedsource
, WCHAR
*stem
,
910 WCHAR
*spec
, DWORD flags
)
912 WCHAR actualsource
[MAX_PATH
];
918 * Validate the source, expanding to full path ensuring it exists
920 if (GetFullPathNameW(suppliedsource
, MAX_PATH
, actualsource
, NULL
) == 0) {
921 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
925 /* If full names required, convert to using the full path */
926 if (flags
& OPT_FULL
) {
927 lstrcpyW(suppliedsource
, actualsource
);
931 * Work out the stem of the source
934 /* If a directory is supplied, use that as-is (either fully or
936 If a filename is supplied + a directory or drive path, use that
939 If no directory or path specified, add eg. C:
940 stem is Drive/Directory is bit up to last \ (or first :)
941 spec is bit after that */
943 starPos
= wcschr(suppliedsource
, '*');
944 questPos
= wcschr(suppliedsource
, '?');
945 if (starPos
|| questPos
) {
946 attribs
= 0x00; /* Ensures skips invalid or directory check below */
948 attribs
= GetFileAttributesW(actualsource
);
951 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
952 XCOPY_FailMessage(GetLastError());
956 stem should be exactly as supplied plus a '\', unless it was
957 eg. C: in which case no slash required */
958 } else if (attribs
& FILE_ATTRIBUTE_DIRECTORY
) {
961 WINE_TRACE("Directory supplied\n");
962 lstrcpyW(stem
, suppliedsource
);
963 lastChar
= stem
[lstrlenW(stem
)-1];
964 if (lastChar
!= '\\' && lastChar
!= ':') {
965 lstrcatW(stem
, wchr_slash
);
967 lstrcpyW(spec
, wchr_star
);
969 /* File or wildcard search:
971 Up to and including last slash if directory path supplied
972 If c:filename supplied, just the c:
973 Otherwise stem should be the current drive letter + ':' */
977 WINE_TRACE("Filename supplied\n");
978 lastDir
= wcsrchr(suppliedsource
, '\\');
981 lstrcpyW(stem
, suppliedsource
);
982 stem
[(lastDir
-suppliedsource
) + 1] = 0x00;
983 lstrcpyW(spec
, (lastDir
+1));
984 } else if (suppliedsource
[1] == ':') {
985 lstrcpyW(stem
, suppliedsource
);
987 lstrcpyW(spec
, suppliedsource
+2);
989 WCHAR curdir
[MAXSTRING
];
990 GetCurrentDirectoryW(ARRAY_SIZE(curdir
), curdir
);
994 lstrcpyW(spec
, suppliedsource
);
1001 /* =========================================================================
1002 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
1003 converts it into a stem
1004 ========================================================================= */
1005 static int XCOPY_ProcessDestParm(WCHAR
*supplieddestination
, WCHAR
*stem
, WCHAR
*spec
,
1006 WCHAR
*srcspec
, DWORD flags
)
1008 WCHAR actualdestination
[MAX_PATH
];
1013 * Validate the source, expanding to full path ensuring it exists
1015 if (GetFullPathNameW(supplieddestination
, MAX_PATH
, actualdestination
, NULL
) == 0) {
1016 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
1017 return RC_INITERROR
;
1020 /* Destination is either a directory or a file */
1021 attribs
= GetFileAttributesW(actualdestination
);
1023 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
1025 /* If /I supplied and wildcard copy, assume directory */
1026 /* Also if destination ends with backslash */
1027 if ((flags
& OPT_ASSUMEDIR
&&
1028 (wcschr(srcspec
, '?') || wcschr(srcspec
, '*'))) ||
1029 (supplieddestination
[lstrlenW(supplieddestination
)-1] == '\\')) {
1035 char answer
[10] = "";
1039 /* Read the F and D characters from the resource file */
1040 wcscpy(fileChar
, XCOPY_LoadMessage(STRING_FILE_CHAR
));
1041 wcscpy(dirChar
, XCOPY_LoadMessage(STRING_DIR_CHAR
));
1043 while (answer
[0] != fileChar
[0] && answer
[0] != dirChar
[0]) {
1044 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR
), supplieddestination
);
1046 ReadFile(GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
), &count
, NULL
);
1047 WINE_TRACE("User answer %c\n", answer
[0]);
1049 answer
[0] = toupper(answer
[0]);
1052 if (answer
[0] == dirChar
[0]) {
1059 isDir
= (attribs
& FILE_ATTRIBUTE_DIRECTORY
);
1063 lstrcpyW(stem
, actualdestination
);
1066 /* Ensure ends with a '\' */
1067 if (stem
[lstrlenW(stem
)-1] != '\\') {
1068 lstrcatW(stem
, wchr_slash
);
1072 WCHAR drive
[MAX_PATH
];
1073 WCHAR dir
[MAX_PATH
];
1074 WCHAR fname
[MAX_PATH
];
1075 WCHAR ext
[MAX_PATH
];
1076 _wsplitpath(actualdestination
, drive
, dir
, fname
, ext
);
1077 lstrcpyW(stem
, drive
);
1078 lstrcatW(stem
, dir
);
1079 lstrcpyW(spec
, fname
);
1080 lstrcatW(spec
, ext
);
1086 /* =========================================================================
1087 main - Main entrypoint for the xcopy command
1089 Processes the args, and drives the actual copying
1090 ========================================================================= */
1091 int wmain (int argc
, WCHAR
*argvW
[])
1094 WCHAR suppliedsource
[MAX_PATH
] = {0}; /* As supplied on the cmd line */
1095 WCHAR supplieddestination
[MAX_PATH
] = {0};
1096 WCHAR sourcestem
[MAX_PATH
] = {0}; /* Stem of source */
1097 WCHAR sourcespec
[MAX_PATH
] = {0}; /* Filespec of source */
1098 WCHAR destinationstem
[MAX_PATH
] = {0}; /* Stem of destination */
1099 WCHAR destinationspec
[MAX_PATH
] = {0}; /* Filespec of destination */
1100 WCHAR copyCmd
[MAXSTRING
]; /* COPYCMD env var */
1101 DWORD flags
= 0; /* Option flags */
1102 const WCHAR PROMPTSTR1
[] = {'/', 'Y', 0};
1103 const WCHAR PROMPTSTR2
[] = {'/', 'y', 0};
1104 const WCHAR COPYCMD
[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1106 /* Preinitialize flags based on COPYCMD */
1107 if (GetEnvironmentVariableW(COPYCMD
, copyCmd
, MAXSTRING
)) {
1108 if (wcsstr(copyCmd
, PROMPTSTR1
) != NULL
||
1109 wcsstr(copyCmd
, PROMPTSTR2
) != NULL
) {
1110 flags
|= OPT_NOPROMPT
;
1114 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1115 wine, but on windows these can be normal files. At least one installer
1116 uses files such as .packlist and (validly) expects them to be copied.
1117 Under wine, if we do not copy hidden files by default then they get
1119 flags
|= OPT_COPYHIDSYS
;
1122 * Parse the command line
1124 if ((rc
= XCOPY_ParseCommandLine(suppliedsource
, supplieddestination
,
1125 &flags
)) != RC_OK
) {
1132 /* Trace out the supplied information */
1133 WINE_TRACE("Supplied parameters:\n");
1134 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource
));
1135 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination
));
1137 /* Extract required information from source specification */
1138 rc
= XCOPY_ProcessSourceParm(suppliedsource
, sourcestem
, sourcespec
, flags
);
1139 if (rc
!= RC_OK
) return rc
;
1141 /* Extract required information from destination specification */
1142 rc
= XCOPY_ProcessDestParm(supplieddestination
, destinationstem
,
1143 destinationspec
, sourcespec
, flags
);
1144 if (rc
!= RC_OK
) return rc
;
1146 /* Trace out the resulting information */
1147 WINE_TRACE("Resolved parameters:\n");
1148 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem
));
1149 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec
));
1150 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem
));
1151 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec
));
1153 /* Pause if necessary */
1154 if (flags
& OPT_PAUSE
) {
1158 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE
));
1159 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), pausestr
, sizeof(pausestr
),
1163 /* Now do the hard work... */
1164 rc
= XCOPY_DoCopy(sourcestem
, sourcespec
,
1165 destinationstem
, destinationspec
,
1168 /* Clear up exclude list allocated memory */
1169 while (excludeList
) {
1170 EXCLUDELIST
*pos
= excludeList
;
1171 excludeList
= excludeList
-> next
;
1172 HeapFree(GetProcessHeap(), 0, pos
->name
);
1173 HeapFree(GetProcessHeap(), 0, pos
);
1176 /* Finished - print trailer and exit */
1177 if (flags
& OPT_SIMULATE
) {
1178 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY
), filesCopied
);
1179 } else if (!(flags
& OPT_NOCOPY
)) {
1180 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY
), filesCopied
);