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
);
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
);
280 buffer
[length
-1] = 0x00;
282 /* If more than CRLF */
284 thisEntry
= HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST
));
285 thisEntry
->next
= excludeList
;
286 excludeList
= thisEntry
;
287 thisEntry
->name
= HeapAlloc(GetProcessHeap(), 0,
288 (length
* sizeof(WCHAR
))+1);
289 lstrcpyW(thisEntry
->name
, buffer
);
290 CharUpperBuffW(thisEntry
->name
, length
);
291 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry
->name
));
295 /* See if EOF or error occurred */
297 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL
), filename
);
298 *endOfName
= endChar
;
303 /* Revert the input string to original form, and cleanup + return */
304 *endOfName
= endChar
;
309 /* =========================================================================
310 * Process the /EXCLUDE: file list, building up a list of substrings to
312 * Returns TRUE on any failure
313 * ========================================================================= */
314 static BOOL
XCOPY_ProcessExcludeList(WCHAR
* parms
) {
316 WCHAR
*filenameStart
= parms
;
318 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms
));
321 while (*parms
&& *parms
!= ' ' && *parms
!= '/') {
323 /* If found '+' then process the file found so far */
325 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
328 filenameStart
= parms
+1;
333 if (filenameStart
!= parms
) {
334 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
342 /* =========================================================================
343 XCOPY_DoCopy - Recursive function to copy files based on input parms
346 This works by using FindFirstFile supplying the source stem and spec.
347 If results are found, any non-directory ones are processed
348 Then, if /S or /E is supplied, another search is made just for
349 directories, and this function is called again for that directory
351 ========================================================================= */
352 static int XCOPY_DoCopy(WCHAR
*srcstem
, WCHAR
*srcspec
,
353 WCHAR
*deststem
, WCHAR
*destspec
,
356 WIN32_FIND_DATAW
*finddata
;
359 WCHAR
*inputpath
, *outputpath
;
360 BOOL copiedFile
= FALSE
;
361 DWORD destAttribs
, srcAttribs
;
365 /* Allocate some working memory on heap to minimize footprint */
366 finddata
= HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW
));
367 inputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
368 outputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
370 /* Build the search info into a single parm */
371 lstrcpyW(inputpath
, srcstem
);
372 lstrcatW(inputpath
, srcspec
);
374 /* Search 1 - Look for matching files */
375 h
= FindFirstFileW(inputpath
, finddata
);
376 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
380 /* Ignore . and .. */
381 if (lstrcmpW(finddata
->cFileName
, wchr_dot
)==0 ||
382 lstrcmpW(finddata
->cFileName
, wchr_dotdot
)==0 ||
383 finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) {
385 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata
->cFileName
));
388 /* Get the filename information */
389 lstrcpyW(copyFrom
, srcstem
);
390 if (flags
& OPT_SHORTNAME
) {
391 lstrcatW(copyFrom
, finddata
->cAlternateFileName
);
393 lstrcatW(copyFrom
, finddata
->cFileName
);
396 lstrcpyW(copyTo
, deststem
);
397 if (*destspec
== 0x00) {
398 if (flags
& OPT_SHORTNAME
) {
399 lstrcatW(copyTo
, finddata
->cAlternateFileName
);
401 lstrcatW(copyTo
, finddata
->cFileName
);
404 lstrcatW(copyTo
, destspec
);
408 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom
),
409 wine_dbgstr_w(copyTo
));
410 if (!copiedFile
&& !(flags
& OPT_SIMULATE
)) XCOPY_CreateDirectory(deststem
);
412 /* See if allowed to copy it */
413 srcAttribs
= GetFileAttributesW(copyFrom
);
414 WINE_TRACE("Source attribs: %d\n", srcAttribs
);
416 if ((srcAttribs
& FILE_ATTRIBUTE_HIDDEN
) ||
417 (srcAttribs
& FILE_ATTRIBUTE_SYSTEM
)) {
419 if (!(flags
& OPT_COPYHIDSYS
)) {
424 if (!(srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
425 (flags
& OPT_ARCHIVEONLY
)) {
429 /* See if file exists */
430 destAttribs
= GetFileAttributesW(copyTo
);
431 WINE_TRACE("Dest attribs: %d\n", srcAttribs
);
433 /* Check date ranges if a destination file already exists */
434 if (!skipFile
&& (flags
& OPT_DATERANGE
) &&
435 (CompareFileTime(&finddata
->ftLastWriteTime
, &dateRange
) < 0)) {
436 WINE_TRACE("Skipping file as modified date too old\n");
440 /* If just /D supplied, only overwrite if src newer than dest */
441 if (!skipFile
&& (flags
& OPT_DATENEWER
) &&
442 (destAttribs
!= INVALID_FILE_ATTRIBUTES
)) {
443 HANDLE h
= CreateFileW(copyTo
, GENERIC_READ
, FILE_SHARE_READ
,
444 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
,
446 if (h
!= INVALID_HANDLE_VALUE
) {
448 GetFileTime(h
, NULL
, NULL
, &writeTime
);
450 if (CompareFileTime(&finddata
->ftLastWriteTime
, &writeTime
) <= 0) {
451 WINE_TRACE("Skipping file as dest newer or same date\n");
458 /* See if exclude list provided. Note since filenames are case
459 insensitive, need to uppercase the filename before doing
461 if (!skipFile
&& (flags
& OPT_EXCLUDELIST
)) {
462 EXCLUDELIST
*pos
= excludeList
;
463 WCHAR copyFromUpper
[MAX_PATH
];
465 /* Uppercase source filename */
466 lstrcpyW(copyFromUpper
, copyFrom
);
467 CharUpperBuffW(copyFromUpper
, lstrlenW(copyFromUpper
));
469 /* Loop through testing each exclude line */
471 if (wcsstr(copyFromUpper
, pos
->name
) != NULL
) {
472 WINE_TRACE("Skipping file as matches exclude '%s'\n",
473 wine_dbgstr_w(pos
->name
));
482 /* Prompt each file if necessary */
483 if (!skipFile
&& (flags
& OPT_SRCPROMPT
)) {
486 BOOL answered
= FALSE
;
490 /* Read the Y and N characters from the resource file */
491 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
492 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
495 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT
), copyFrom
);
496 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
500 if (toupper(answer
[0]) == noChar
[0])
502 else if (toupper(answer
[0]) != yesChar
[0])
508 destAttribs
!= INVALID_FILE_ATTRIBUTES
&& !(flags
& OPT_NOPROMPT
)) {
511 BOOL answered
= FALSE
;
516 /* Read the A,Y and N characters from the resource file */
517 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
518 wcscpy(allChar
, XCOPY_LoadMessage(STRING_ALL_CHAR
));
519 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
522 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE
), copyTo
);
523 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
527 if (toupper(answer
[0]) == allChar
[0])
528 flags
|= OPT_NOPROMPT
;
529 else if (toupper(answer
[0]) == noChar
[0])
531 else if (toupper(answer
[0]) != yesChar
[0])
536 /* See if it has to exist! */
537 if (destAttribs
== INVALID_FILE_ATTRIBUTES
&& (flags
& OPT_MUSTEXIST
)) {
541 /* Output a status message */
543 if (flags
& OPT_QUIET
) {
545 } else if (flags
& OPT_FULL
) {
546 const WCHAR infostr
[] = {'%', '1', ' ', '-', '>', ' ',
549 XCOPY_wprintf(infostr
, copyFrom
, copyTo
);
551 const WCHAR infostr
[] = {'%', '1', '\n', 0};
552 XCOPY_wprintf(infostr
, copyFrom
);
555 /* If allowing overwriting of read only files, remove any
557 if ((destAttribs
& FILE_ATTRIBUTE_READONLY
) &&
558 (flags
& OPT_REPLACEREAD
)) {
559 SetFileAttributesW(copyTo
, destAttribs
& ~FILE_ATTRIBUTE_READONLY
);
563 if (flags
& OPT_SIMULATE
|| flags
& OPT_NOCOPY
) {
565 } else if (CopyFileW(copyFrom
, copyTo
, FALSE
) == 0) {
567 DWORD error
= GetLastError();
568 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL
),
569 copyFrom
, copyTo
, error
);
570 XCOPY_FailMessage(error
);
572 if (flags
& OPT_IGNOREERRORS
) {
580 /* If /M supplied, remove the archive bit after successful copy */
582 if ((srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
583 (flags
& OPT_REMOVEARCH
)) {
584 SetFileAttributesW(copyFrom
, (srcAttribs
& ~FILE_ATTRIBUTE_ARCHIVE
));
592 findres
= FindNextFileW(h
, finddata
);
596 /* Search 2 - do subdirs */
597 if (flags
& OPT_RECURSIVE
) {
598 lstrcpyW(inputpath
, srcstem
);
599 lstrcatW(inputpath
, wchr_star
);
601 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath
));
603 h
= FindFirstFileW(inputpath
, finddata
);
604 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
606 /* Only looking for dirs */
607 if ((finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) &&
608 (lstrcmpW(finddata
->cFileName
, wchr_dot
) != 0) &&
609 (lstrcmpW(finddata
->cFileName
, wchr_dotdot
) != 0)) {
611 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata
->cFileName
));
613 /* Make up recursive information */
614 lstrcpyW(inputpath
, srcstem
);
615 lstrcatW(inputpath
, finddata
->cFileName
);
616 lstrcatW(inputpath
, wchr_slash
);
618 lstrcpyW(outputpath
, deststem
);
619 if (*destspec
== 0x00) {
620 lstrcatW(outputpath
, finddata
->cFileName
);
622 /* If /E is supplied, create the directory now */
623 if ((flags
& OPT_EMPTYDIR
) &&
624 !(flags
& OPT_SIMULATE
))
625 XCOPY_CreateDirectory(outputpath
);
627 lstrcatW(outputpath
, wchr_slash
);
630 XCOPY_DoCopy(inputpath
, srcspec
, outputpath
, destspec
, flags
);
634 findres
= FindNextFileW(h
, finddata
);
642 HeapFree(GetProcessHeap(), 0, finddata
);
643 HeapFree(GetProcessHeap(), 0, inputpath
);
644 HeapFree(GetProcessHeap(), 0, outputpath
);
650 /* =========================================================================
651 XCOPY_ParseCommandLine - Parses the command line
652 ========================================================================= */
653 static BOOL
is_whitespace(WCHAR c
)
655 return c
== ' ' || c
== '\t';
658 static WCHAR
*skip_whitespace(WCHAR
*p
)
660 for (; *p
&& is_whitespace(*p
); p
++);
664 /* Windows XCOPY uses a simplified command line parsing algorithm
665 that lacks the escaped-quote logic of build_argv(), because
666 literal double quotes are illegal in any of its arguments.
667 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
668 static int find_end_of_word(const WCHAR
*word
, WCHAR
**end
)
671 const WCHAR
*ptr
= word
;
673 for (; *ptr
!= '\0' && *ptr
!= '"' &&
674 (in_quotes
|| !is_whitespace(*ptr
)); ptr
++);
676 in_quotes
= !in_quotes
;
679 /* Odd number of double quotes is illegal for XCOPY */
680 if (in_quotes
&& *ptr
== '\0')
682 if (*ptr
== '\0' || (!in_quotes
&& is_whitespace(*ptr
)))
689 /* Remove all double quotes from a word */
690 static void strip_quotes(WCHAR
*word
, WCHAR
**end
)
693 for (rp
= word
, wp
= word
; *rp
!= '\0'; rp
++) {
704 static int XCOPY_ParseCommandLine(WCHAR
*suppliedsource
,
705 WCHAR
*supplieddestination
, DWORD
*pflags
)
707 const WCHAR EXCLUDE
[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
708 DWORD flags
= *pflags
;
709 WCHAR
*cmdline
, *word
, *end
, *next
;
710 int rc
= RC_INITERROR
;
712 cmdline
= _wcsdup(GetCommandLineW());
716 /* Skip first arg, which is the program name */
717 if ((rc
= find_end_of_word(cmdline
, &word
)) != RC_OK
)
719 word
= skip_whitespace(word
);
724 if ((rc
= find_end_of_word(word
, &end
)) != RC_OK
)
727 next
= skip_whitespace(end
);
730 strip_quotes(word
, &end
);
731 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word
));
733 /* First non-switch parameter is source, second is destination */
735 if (suppliedsource
[0] == 0x00) {
736 lstrcpyW(suppliedsource
, word
);
737 } else if (supplieddestination
[0] == 0x00) {
738 lstrcpyW(supplieddestination
, word
);
740 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS
));
744 /* Process all the switch options
745 Note: Windows docs say /P prompts when dest is created
746 but tests show it is done for each src file
747 regardless of the destination */
748 switch (toupper(word
[1])) {
749 case 'I': flags
|= OPT_ASSUMEDIR
; break;
750 case 'S': flags
|= OPT_RECURSIVE
; break;
751 case 'Q': flags
|= OPT_QUIET
; break;
752 case 'F': flags
|= OPT_FULL
; break;
753 case 'L': flags
|= OPT_SIMULATE
; break;
754 case 'W': flags
|= OPT_PAUSE
; break;
755 case 'T': flags
|= OPT_NOCOPY
| OPT_RECURSIVE
; break;
756 case 'Y': flags
|= OPT_NOPROMPT
; break;
757 case 'N': flags
|= OPT_SHORTNAME
; break;
758 case 'U': flags
|= OPT_MUSTEXIST
; break;
759 case 'R': flags
|= OPT_REPLACEREAD
; break;
760 case 'H': flags
|= OPT_COPYHIDSYS
; break;
761 case 'C': flags
|= OPT_IGNOREERRORS
; break;
762 case 'P': flags
|= OPT_SRCPROMPT
; break;
763 case 'A': flags
|= OPT_ARCHIVEONLY
; break;
764 case 'M': flags
|= OPT_ARCHIVEONLY
|
765 OPT_REMOVEARCH
; break;
767 /* E can be /E or /EXCLUDE */
768 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT
,
769 NORM_IGNORECASE
| SORT_STRINGSORT
,
771 EXCLUDE
, -1) == CSTR_EQUAL
) {
772 if (XCOPY_ProcessExcludeList(&word
[9])) {
773 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
775 } else flags
|= OPT_EXCLUDELIST
;
776 } else flags
|= OPT_EMPTYDIR
| OPT_RECURSIVE
;
779 /* D can be /D or /D: */
780 case 'D': if (word
[2]==':' && isdigit(word
[3])) {
782 WCHAR
*pos
= &word
[3];
783 BOOL isError
= FALSE
;
784 memset(&st
, 0x00, sizeof(st
));
786 /* Parse the arg : Month */
787 st
.wMonth
= _wtol(pos
);
788 while (*pos
&& isdigit(*pos
)) pos
++;
789 if (*pos
++ != '-') isError
= TRUE
;
791 /* Parse the arg : Day */
793 st
.wDay
= _wtol(pos
);
794 while (*pos
&& isdigit(*pos
)) pos
++;
795 if (*pos
++ != '-') isError
= TRUE
;
798 /* Parse the arg : Year */
800 st
.wYear
= _wtol(pos
);
801 while (*pos
&& isdigit(*pos
)) pos
++;
802 if (st
.wYear
< 100) st
.wYear
+=2000;
805 if (!isError
&& SystemTimeToFileTime(&st
, &dateRange
)) {
807 WCHAR datestring
[32], timestring
[32];
809 flags
|= OPT_DATERANGE
;
812 FileTimeToSystemTime (&dateRange
, &st
);
813 GetDateFormatW(0, DATE_SHORTDATE
, &st
, NULL
, datestring
,
814 sizeof(datestring
)/sizeof(WCHAR
));
815 GetTimeFormatW(0, TIME_NOSECONDS
, &st
,
816 NULL
, timestring
, sizeof(timestring
)/sizeof(WCHAR
));
818 WINE_TRACE("Date being used is: %s %s\n",
819 wine_dbgstr_w(datestring
), wine_dbgstr_w(timestring
));
821 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
825 flags
|= OPT_DATENEWER
;
829 case '-': if (toupper(word
[2])=='Y')
830 flags
&= ~OPT_NOPROMPT
;
832 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP
));
836 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word
));
837 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM
), word
);
844 /* Default the destination if not supplied */
845 if (supplieddestination
[0] == 0x00)
846 lstrcpyW(supplieddestination
, wchr_dot
);
857 /* =========================================================================
858 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
859 converts it into a stem and a filespec
860 ========================================================================= */
861 static int XCOPY_ProcessSourceParm(WCHAR
*suppliedsource
, WCHAR
*stem
,
862 WCHAR
*spec
, DWORD flags
)
864 WCHAR actualsource
[MAX_PATH
];
870 * Validate the source, expanding to full path ensuring it exists
872 if (GetFullPathNameW(suppliedsource
, MAX_PATH
, actualsource
, NULL
) == 0) {
873 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
877 /* If full names required, convert to using the full path */
878 if (flags
& OPT_FULL
) {
879 lstrcpyW(suppliedsource
, actualsource
);
883 * Work out the stem of the source
886 /* If a directory is supplied, use that as-is (either fully or
888 If a filename is supplied + a directory or drive path, use that
891 If no directory or path specified, add eg. C:
892 stem is Drive/Directory is bit up to last \ (or first :)
893 spec is bit after that */
895 starPos
= wcschr(suppliedsource
, '*');
896 questPos
= wcschr(suppliedsource
, '?');
897 if (starPos
|| questPos
) {
898 attribs
= 0x00; /* Ensures skips invalid or directory check below */
900 attribs
= GetFileAttributesW(actualsource
);
903 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
904 XCOPY_FailMessage(GetLastError());
908 stem should be exactly as supplied plus a '\', unless it was
909 eg. C: in which case no slash required */
910 } else if (attribs
& FILE_ATTRIBUTE_DIRECTORY
) {
913 WINE_TRACE("Directory supplied\n");
914 lstrcpyW(stem
, suppliedsource
);
915 lastChar
= stem
[lstrlenW(stem
)-1];
916 if (lastChar
!= '\\' && lastChar
!= ':') {
917 lstrcatW(stem
, wchr_slash
);
919 lstrcpyW(spec
, wchr_star
);
921 /* File or wildcard search:
923 Up to and including last slash if directory path supplied
924 If c:filename supplied, just the c:
925 Otherwise stem should be the current drive letter + ':' */
929 WINE_TRACE("Filename supplied\n");
930 lastDir
= wcsrchr(suppliedsource
, '\\');
933 lstrcpyW(stem
, suppliedsource
);
934 stem
[(lastDir
-suppliedsource
) + 1] = 0x00;
935 lstrcpyW(spec
, (lastDir
+1));
936 } else if (suppliedsource
[1] == ':') {
937 lstrcpyW(stem
, suppliedsource
);
939 lstrcpyW(spec
, suppliedsource
+2);
941 WCHAR curdir
[MAXSTRING
];
942 GetCurrentDirectoryW(sizeof(curdir
)/sizeof(WCHAR
), curdir
);
946 lstrcpyW(spec
, suppliedsource
);
953 /* =========================================================================
954 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
955 converts it into a stem
956 ========================================================================= */
957 static int XCOPY_ProcessDestParm(WCHAR
*supplieddestination
, WCHAR
*stem
, WCHAR
*spec
,
958 WCHAR
*srcspec
, DWORD flags
)
960 WCHAR actualdestination
[MAX_PATH
];
965 * Validate the source, expanding to full path ensuring it exists
967 if (GetFullPathNameW(supplieddestination
, MAX_PATH
, actualdestination
, NULL
) == 0) {
968 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
972 /* Destination is either a directory or a file */
973 attribs
= GetFileAttributesW(actualdestination
);
975 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
977 /* If /I supplied and wildcard copy, assume directory */
978 /* Also if destination ends with backslash */
979 if ((flags
& OPT_ASSUMEDIR
&&
980 (wcschr(srcspec
, '?') || wcschr(srcspec
, '*'))) ||
981 (supplieddestination
[lstrlenW(supplieddestination
)-1] == '\\')) {
987 char answer
[10] = "";
991 /* Read the F and D characters from the resource file */
992 wcscpy(fileChar
, XCOPY_LoadMessage(STRING_FILE_CHAR
));
993 wcscpy(dirChar
, XCOPY_LoadMessage(STRING_DIR_CHAR
));
995 while (answer
[0] != fileChar
[0] && answer
[0] != dirChar
[0]) {
996 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR
), supplieddestination
);
998 ReadFile(GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
), &count
, NULL
);
999 WINE_TRACE("User answer %c\n", answer
[0]);
1001 answer
[0] = toupper(answer
[0]);
1004 if (answer
[0] == dirChar
[0]) {
1011 isDir
= (attribs
& FILE_ATTRIBUTE_DIRECTORY
);
1015 lstrcpyW(stem
, actualdestination
);
1018 /* Ensure ends with a '\' */
1019 if (stem
[lstrlenW(stem
)-1] != '\\') {
1020 lstrcatW(stem
, wchr_slash
);
1024 WCHAR drive
[MAX_PATH
];
1025 WCHAR dir
[MAX_PATH
];
1026 WCHAR fname
[MAX_PATH
];
1027 WCHAR ext
[MAX_PATH
];
1028 _wsplitpath(actualdestination
, drive
, dir
, fname
, ext
);
1029 lstrcpyW(stem
, drive
);
1030 lstrcatW(stem
, dir
);
1031 lstrcpyW(spec
, fname
);
1032 lstrcatW(spec
, ext
);
1038 /* =========================================================================
1039 main - Main entrypoint for the xcopy command
1041 Processes the args, and drives the actual copying
1042 ========================================================================= */
1043 int wmain (int argc
, WCHAR
*argvW
[])
1046 WCHAR suppliedsource
[MAX_PATH
] = {0}; /* As supplied on the cmd line */
1047 WCHAR supplieddestination
[MAX_PATH
] = {0};
1048 WCHAR sourcestem
[MAX_PATH
] = {0}; /* Stem of source */
1049 WCHAR sourcespec
[MAX_PATH
] = {0}; /* Filespec of source */
1050 WCHAR destinationstem
[MAX_PATH
] = {0}; /* Stem of destination */
1051 WCHAR destinationspec
[MAX_PATH
] = {0}; /* Filespec of destination */
1052 WCHAR copyCmd
[MAXSTRING
]; /* COPYCMD env var */
1053 DWORD flags
= 0; /* Option flags */
1054 const WCHAR PROMPTSTR1
[] = {'/', 'Y', 0};
1055 const WCHAR PROMPTSTR2
[] = {'/', 'y', 0};
1056 const WCHAR COPYCMD
[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1058 /* Preinitialize flags based on COPYCMD */
1059 if (GetEnvironmentVariableW(COPYCMD
, copyCmd
, MAXSTRING
)) {
1060 if (wcsstr(copyCmd
, PROMPTSTR1
) != NULL
||
1061 wcsstr(copyCmd
, PROMPTSTR2
) != NULL
) {
1062 flags
|= OPT_NOPROMPT
;
1066 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1067 wine, but on windows these can be normal files. At least one installer
1068 uses files such as .packlist and (validly) expects them to be copied.
1069 Under wine, if we do not copy hidden files by default then they get
1071 flags
|= OPT_COPYHIDSYS
;
1074 * Parse the command line
1076 if ((rc
= XCOPY_ParseCommandLine(suppliedsource
, supplieddestination
,
1077 &flags
)) != RC_OK
) {
1084 /* Trace out the supplied information */
1085 WINE_TRACE("Supplied parameters:\n");
1086 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource
));
1087 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination
));
1089 /* Extract required information from source specification */
1090 rc
= XCOPY_ProcessSourceParm(suppliedsource
, sourcestem
, sourcespec
, flags
);
1091 if (rc
!= RC_OK
) return rc
;
1093 /* Extract required information from destination specification */
1094 rc
= XCOPY_ProcessDestParm(supplieddestination
, destinationstem
,
1095 destinationspec
, sourcespec
, flags
);
1096 if (rc
!= RC_OK
) return rc
;
1098 /* Trace out the resulting information */
1099 WINE_TRACE("Resolved parameters:\n");
1100 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem
));
1101 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec
));
1102 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem
));
1103 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec
));
1105 /* Pause if necessary */
1106 if (flags
& OPT_PAUSE
) {
1110 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE
));
1111 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), pausestr
, sizeof(pausestr
),
1115 /* Now do the hard work... */
1116 rc
= XCOPY_DoCopy(sourcestem
, sourcespec
,
1117 destinationstem
, destinationspec
,
1120 /* Clear up exclude list allocated memory */
1121 while (excludeList
) {
1122 EXCLUDELIST
*pos
= excludeList
;
1123 excludeList
= excludeList
-> next
;
1124 HeapFree(GetProcessHeap(), 0, pos
->name
);
1125 HeapFree(GetProcessHeap(), 0, pos
);
1128 /* Finished - print trailer and exit */
1129 if (flags
& OPT_SIMULATE
) {
1130 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY
), filesCopied
);
1131 } else if (!(flags
& OPT_NOCOPY
)) {
1132 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY
), filesCopied
);
1134 if (rc
== RC_OK
&& filesCopied
== 0) rc
= RC_NOFILES
;