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
46 #include <wine/debug.h>
47 #include <wine/unicode.h>
50 WINE_DEFAULT_DEBUG_CHANNEL(xcopy
);
54 typedef struct _EXCLUDELIST
56 struct _EXCLUDELIST
*next
;
61 /* Global variables */
62 static ULONG filesCopied
= 0; /* Number of files copied */
63 static EXCLUDELIST
*excludeList
= NULL
; /* Excluded strings list */
64 static FILETIME dateRange
; /* Date range to copy after*/
65 static const WCHAR wchr_slash
[] = {'\\', 0};
66 static const WCHAR wchr_star
[] = {'*', 0};
67 static const WCHAR wchr_dot
[] = {'.', 0};
68 static const WCHAR wchr_dotdot
[] = {'.', '.', 0};
71 /* To minimize stack usage during recursion, some temporary variables
73 static WCHAR copyFrom
[MAX_PATH
];
74 static WCHAR copyTo
[MAX_PATH
];
77 /* =========================================================================
78 * Load a string from the resource file, handling any error
79 * Returns string retrieved from resource file
80 * ========================================================================= */
81 static WCHAR
*XCOPY_LoadMessage(UINT id
) {
82 static WCHAR msg
[MAXSTRING
];
83 const WCHAR failedMsg
[] = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
85 if (!LoadStringW(GetModuleHandleW(NULL
), id
, msg
, sizeof(msg
)/sizeof(WCHAR
))) {
86 WINE_FIXME("LoadString failed with %d\n", GetLastError());
87 lstrcpyW(msg
, failedMsg
);
92 /* =========================================================================
93 * Output a formatted unicode string. Ideally this will go to the console
94 * and hence required WriteConsoleW to output it, however if file i/o is
95 * redirected, it needs to be WriteFile'd using OEM (not ANSI) format
96 * ========================================================================= */
97 static int __cdecl
XCOPY_wprintf(const WCHAR
*format
, ...) {
99 static WCHAR
*output_bufW
= NULL
;
100 static char *output_bufA
= NULL
;
101 static BOOL toConsole
= TRUE
;
102 static BOOL traceOutput
= FALSE
;
103 #define MAX_WRITECONSOLE_SIZE 65535
111 * Allocate buffer to use when writing to console
112 * Note: Not freed - memory will be allocated once and released when
116 if (!output_bufW
) output_bufW
= HeapAlloc(GetProcessHeap(), 0,
117 MAX_WRITECONSOLE_SIZE
*sizeof(WCHAR
));
119 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
123 __ms_va_start(parms
, format
);
124 SetLastError(NO_ERROR
);
125 len
= FormatMessageW(FORMAT_MESSAGE_FROM_STRING
, format
, 0, 0, output_bufW
,
126 MAX_WRITECONSOLE_SIZE
/sizeof(*output_bufW
), &parms
);
128 if (len
== 0 && GetLastError() != NO_ERROR
) {
129 WINE_FIXME("Could not format string: le=%u, fmt=%s\n", GetLastError(), wine_dbgstr_w(format
));
133 /* Try to write as unicode whenever we think it's a console */
135 res
= WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE
),
136 output_bufW
, len
, &nOut
, NULL
);
139 /* If writing to console has failed (ever) we assume it's file
140 i/o so convert to OEM codepage and output */
142 BOOL usedDefaultChar
= FALSE
;
143 DWORD convertedChars
;
148 * Allocate buffer to use when writing to file. Not freed, as above
150 if (!output_bufA
) output_bufA
= HeapAlloc(GetProcessHeap(), 0,
151 MAX_WRITECONSOLE_SIZE
);
153 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
157 /* Convert to OEM, then output */
158 convertedChars
= WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW
,
159 len
, output_bufA
, MAX_WRITECONSOLE_SIZE
,
160 "?", &usedDefaultChar
);
161 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE
), output_bufA
, convertedChars
,
165 /* Trace whether screen or console */
167 WINE_TRACE("Writing to console? (%d)\n", toConsole
);
173 /* =========================================================================
174 * Load a string for a system error and writes it to the screen
175 * Returns string retrieved from resource file
176 * ========================================================================= */
177 static void XCOPY_FailMessage(DWORD err
) {
181 status
= FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER
|
182 FORMAT_MESSAGE_FROM_SYSTEM
,
184 (LPWSTR
) &lpMsgBuf
, 0, NULL
);
186 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
187 err
, GetLastError());
189 const WCHAR infostr
[] = {'%', '1', '\n', 0};
190 XCOPY_wprintf(infostr
, lpMsgBuf
);
191 LocalFree ((HLOCAL
)lpMsgBuf
);
196 /* =========================================================================
197 * Routine copied from cmd.exe md command -
198 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
199 * dir2 if they do not already exist.
200 * ========================================================================= */
201 static BOOL
XCOPY_CreateDirectory(const WCHAR
* path
)
207 new_path
= HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR
) * (lstrlenW(path
)+1));
208 lstrcpyW(new_path
,path
);
210 while ((len
= lstrlenW(new_path
)) && new_path
[len
- 1] == '\\')
211 new_path
[len
- 1] = 0;
213 while (!CreateDirectoryW(new_path
,NULL
))
216 DWORD last_error
= GetLastError();
217 if (last_error
== ERROR_ALREADY_EXISTS
)
220 if (last_error
!= ERROR_PATH_NOT_FOUND
)
226 if (!(slash
= wcsrchr(new_path
,'\\')) && ! (slash
= wcsrchr(new_path
,'/')))
232 len
= slash
- new_path
;
234 if (!XCOPY_CreateDirectory(new_path
))
239 new_path
[len
] = '\\';
241 HeapFree(GetProcessHeap(),0,new_path
);
245 /* =========================================================================
246 * Process a single file from the /EXCLUDE: file list, building up a list
247 * of substrings to avoid copying
248 * Returns TRUE on any failure
249 * ========================================================================= */
250 static BOOL
XCOPY_ProcessExcludeFile(WCHAR
* filename
, WCHAR
* endOfName
) {
252 WCHAR endChar
= *endOfName
;
253 WCHAR buffer
[MAXSTRING
];
255 const WCHAR readTextMode
[] = {'r', 't', 0};
257 /* Null terminate the filename (temporarily updates the filename hence
262 inFile
= _wfopen(filename
, readTextMode
);
263 if (inFile
== NULL
) {
264 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL
), filename
);
265 *endOfName
= endChar
;
269 /* Process line by line */
270 while (fgetws(buffer
, sizeof(buffer
)/sizeof(WCHAR
), inFile
) != NULL
) {
271 EXCLUDELIST
*thisEntry
;
272 int length
= lstrlenW(buffer
);
274 /* If more than CRLF */
276 buffer
[length
-1] = 0; /* strip CRLF */
277 thisEntry
= HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST
));
278 thisEntry
->next
= excludeList
;
279 excludeList
= thisEntry
;
280 thisEntry
->name
= HeapAlloc(GetProcessHeap(), 0,
281 (length
* sizeof(WCHAR
))+1);
282 lstrcpyW(thisEntry
->name
, buffer
);
283 CharUpperBuffW(thisEntry
->name
, length
);
284 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry
->name
));
288 /* See if EOF or error occurred */
290 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL
), filename
);
291 *endOfName
= endChar
;
296 /* Revert the input string to original form, and cleanup + return */
297 *endOfName
= endChar
;
302 /* =========================================================================
303 * Process the /EXCLUDE: file list, building up a list of substrings to
305 * Returns TRUE on any failure
306 * ========================================================================= */
307 static BOOL
XCOPY_ProcessExcludeList(WCHAR
* parms
) {
309 WCHAR
*filenameStart
= parms
;
311 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms
));
314 while (*parms
&& *parms
!= ' ' && *parms
!= '/') {
316 /* If found '+' then process the file found so far */
318 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
321 filenameStart
= parms
+1;
326 if (filenameStart
!= parms
) {
327 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
335 /* =========================================================================
336 XCOPY_DoCopy - Recursive function to copy files based on input parms
339 This works by using FindFirstFile supplying the source stem and spec.
340 If results are found, any non-directory ones are processed
341 Then, if /S or /E is supplied, another search is made just for
342 directories, and this function is called again for that directory
344 ========================================================================= */
345 static int XCOPY_DoCopy(WCHAR
*srcstem
, WCHAR
*srcspec
,
346 WCHAR
*deststem
, WCHAR
*destspec
,
349 WIN32_FIND_DATAW
*finddata
;
352 WCHAR
*inputpath
, *outputpath
;
353 BOOL copiedFile
= FALSE
;
354 DWORD destAttribs
, srcAttribs
;
358 /* Allocate some working memory on heap to minimize footprint */
359 finddata
= HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW
));
360 inputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
361 outputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
363 /* Build the search info into a single parm */
364 lstrcpyW(inputpath
, srcstem
);
365 lstrcatW(inputpath
, srcspec
);
367 /* Search 1 - Look for matching files */
368 h
= FindFirstFileW(inputpath
, finddata
);
369 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
373 /* Ignore . and .. */
374 if (lstrcmpW(finddata
->cFileName
, wchr_dot
)==0 ||
375 lstrcmpW(finddata
->cFileName
, wchr_dotdot
)==0 ||
376 finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) {
378 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata
->cFileName
));
381 /* Get the filename information */
382 lstrcpyW(copyFrom
, srcstem
);
383 if (flags
& OPT_SHORTNAME
) {
384 lstrcatW(copyFrom
, finddata
->cAlternateFileName
);
386 lstrcatW(copyFrom
, finddata
->cFileName
);
389 lstrcpyW(copyTo
, deststem
);
390 if (*destspec
== 0x00) {
391 if (flags
& OPT_SHORTNAME
) {
392 lstrcatW(copyTo
, finddata
->cAlternateFileName
);
394 lstrcatW(copyTo
, finddata
->cFileName
);
397 lstrcatW(copyTo
, destspec
);
401 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom
),
402 wine_dbgstr_w(copyTo
));
403 if (!copiedFile
&& !(flags
& OPT_SIMULATE
)) XCOPY_CreateDirectory(deststem
);
405 /* See if allowed to copy it */
406 srcAttribs
= GetFileAttributesW(copyFrom
);
407 WINE_TRACE("Source attribs: %d\n", srcAttribs
);
409 if ((srcAttribs
& FILE_ATTRIBUTE_HIDDEN
) ||
410 (srcAttribs
& FILE_ATTRIBUTE_SYSTEM
)) {
412 if (!(flags
& OPT_COPYHIDSYS
)) {
417 if (!(srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
418 (flags
& OPT_ARCHIVEONLY
)) {
422 /* See if file exists */
423 destAttribs
= GetFileAttributesW(copyTo
);
424 WINE_TRACE("Dest attribs: %d\n", srcAttribs
);
426 /* Check date ranges if a destination file already exists */
427 if (!skipFile
&& (flags
& OPT_DATERANGE
) &&
428 (CompareFileTime(&finddata
->ftLastWriteTime
, &dateRange
) < 0)) {
429 WINE_TRACE("Skipping file as modified date too old\n");
433 /* If just /D supplied, only overwrite if src newer than dest */
434 if (!skipFile
&& (flags
& OPT_DATENEWER
) &&
435 (destAttribs
!= INVALID_FILE_ATTRIBUTES
)) {
436 HANDLE h
= CreateFileW(copyTo
, GENERIC_READ
, FILE_SHARE_READ
,
437 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
,
439 if (h
!= INVALID_HANDLE_VALUE
) {
441 GetFileTime(h
, NULL
, NULL
, &writeTime
);
443 if (CompareFileTime(&finddata
->ftLastWriteTime
, &writeTime
) <= 0) {
444 WINE_TRACE("Skipping file as dest newer or same date\n");
451 /* See if exclude list provided. Note since filenames are case
452 insensitive, need to uppercase the filename before doing
454 if (!skipFile
&& (flags
& OPT_EXCLUDELIST
)) {
455 EXCLUDELIST
*pos
= excludeList
;
456 WCHAR copyFromUpper
[MAX_PATH
];
458 /* Uppercase source filename */
459 lstrcpyW(copyFromUpper
, copyFrom
);
460 CharUpperBuffW(copyFromUpper
, lstrlenW(copyFromUpper
));
462 /* Loop through testing each exclude line */
464 if (wcsstr(copyFromUpper
, pos
->name
) != NULL
) {
465 WINE_TRACE("Skipping file as matches exclude '%s'\n",
466 wine_dbgstr_w(pos
->name
));
475 /* Prompt each file if necessary */
476 if (!skipFile
&& (flags
& OPT_SRCPROMPT
)) {
479 BOOL answered
= FALSE
;
483 /* Read the Y and N characters from the resource file */
484 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
485 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
488 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT
), copyFrom
);
489 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
493 if (toupper(answer
[0]) == noChar
[0])
495 else if (toupper(answer
[0]) != yesChar
[0])
501 destAttribs
!= INVALID_FILE_ATTRIBUTES
&& !(flags
& OPT_NOPROMPT
)) {
504 BOOL answered
= FALSE
;
509 /* Read the A,Y and N characters from the resource file */
510 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
511 wcscpy(allChar
, XCOPY_LoadMessage(STRING_ALL_CHAR
));
512 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
515 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE
), copyTo
);
516 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
520 if (toupper(answer
[0]) == allChar
[0])
521 flags
|= OPT_NOPROMPT
;
522 else if (toupper(answer
[0]) == noChar
[0])
524 else if (toupper(answer
[0]) != yesChar
[0])
529 /* See if it has to exist! */
530 if (destAttribs
== INVALID_FILE_ATTRIBUTES
&& (flags
& OPT_MUSTEXIST
)) {
534 /* Output a status message */
536 if (flags
& OPT_QUIET
) {
538 } else if (flags
& OPT_FULL
) {
539 const WCHAR infostr
[] = {'%', '1', ' ', '-', '>', ' ',
542 XCOPY_wprintf(infostr
, copyFrom
, copyTo
);
544 const WCHAR infostr
[] = {'%', '1', '\n', 0};
545 XCOPY_wprintf(infostr
, copyFrom
);
548 /* If allowing overwriting of read only files, remove any
550 if ((destAttribs
& FILE_ATTRIBUTE_READONLY
) &&
551 (flags
& OPT_REPLACEREAD
)) {
552 SetFileAttributesW(copyTo
, destAttribs
& ~FILE_ATTRIBUTE_READONLY
);
556 if (flags
& OPT_SIMULATE
|| flags
& OPT_NOCOPY
) {
558 } else if (CopyFileW(copyFrom
, copyTo
, FALSE
) == 0) {
560 DWORD error
= GetLastError();
561 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL
),
562 copyFrom
, copyTo
, error
);
563 XCOPY_FailMessage(error
);
565 if (flags
& OPT_IGNOREERRORS
) {
573 /* If /M supplied, remove the archive bit after successful copy */
575 if ((srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
576 (flags
& OPT_REMOVEARCH
)) {
577 SetFileAttributesW(copyFrom
, (srcAttribs
& ~FILE_ATTRIBUTE_ARCHIVE
));
585 findres
= FindNextFileW(h
, finddata
);
589 /* Search 2 - do subdirs */
590 if (flags
& OPT_RECURSIVE
) {
591 lstrcpyW(inputpath
, srcstem
);
592 lstrcatW(inputpath
, wchr_star
);
594 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath
));
596 h
= FindFirstFileW(inputpath
, finddata
);
597 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
599 /* Only looking for dirs */
600 if ((finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) &&
601 (lstrcmpW(finddata
->cFileName
, wchr_dot
) != 0) &&
602 (lstrcmpW(finddata
->cFileName
, wchr_dotdot
) != 0)) {
604 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata
->cFileName
));
606 /* Make up recursive information */
607 lstrcpyW(inputpath
, srcstem
);
608 lstrcatW(inputpath
, finddata
->cFileName
);
609 lstrcatW(inputpath
, wchr_slash
);
611 lstrcpyW(outputpath
, deststem
);
612 if (*destspec
== 0x00) {
613 lstrcatW(outputpath
, finddata
->cFileName
);
615 /* If /E is supplied, create the directory now */
616 if ((flags
& OPT_EMPTYDIR
) &&
617 !(flags
& OPT_SIMULATE
))
618 XCOPY_CreateDirectory(outputpath
);
620 lstrcatW(outputpath
, wchr_slash
);
623 XCOPY_DoCopy(inputpath
, srcspec
, outputpath
, destspec
, flags
);
627 findres
= FindNextFileW(h
, finddata
);
635 HeapFree(GetProcessHeap(), 0, finddata
);
636 HeapFree(GetProcessHeap(), 0, inputpath
);
637 HeapFree(GetProcessHeap(), 0, outputpath
);
643 /* =========================================================================
644 XCOPY_ParseCommandLine - Parses the command line
645 ========================================================================= */
646 static BOOL
is_whitespace(WCHAR c
)
648 return c
== ' ' || c
== '\t';
651 static WCHAR
*skip_whitespace(WCHAR
*p
)
653 for (; *p
&& is_whitespace(*p
); p
++);
657 /* Windows XCOPY uses a simplified command line parsing algorithm
658 that lacks the escaped-quote logic of build_argv(), because
659 literal double quotes are illegal in any of its arguments.
660 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
661 static int find_end_of_word(const WCHAR
*word
, WCHAR
**end
)
663 BOOL in_quotes
= FALSE
;
664 const WCHAR
*ptr
= word
;
666 for (; *ptr
!= '\0' && *ptr
!= '"' &&
667 (in_quotes
|| !is_whitespace(*ptr
)); ptr
++);
669 in_quotes
= !in_quotes
;
672 /* Odd number of double quotes is illegal for XCOPY */
673 if (in_quotes
&& *ptr
== '\0')
675 if (*ptr
== '\0' || (!in_quotes
&& is_whitespace(*ptr
)))
682 /* Remove all double quotes from a word */
683 static void strip_quotes(WCHAR
*word
, WCHAR
**end
)
686 for (rp
= word
, wp
= word
; *rp
!= '\0'; rp
++) {
697 static int XCOPY_ParseCommandLine(WCHAR
*suppliedsource
,
698 WCHAR
*supplieddestination
, DWORD
*pflags
)
700 const WCHAR EXCLUDE
[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
701 DWORD flags
= *pflags
;
702 WCHAR
*cmdline
, *word
, *end
, *next
;
703 int rc
= RC_INITERROR
;
705 cmdline
= _wcsdup(GetCommandLineW());
709 /* Skip first arg, which is the program name */
710 if ((rc
= find_end_of_word(cmdline
, &word
)) != RC_OK
)
712 word
= skip_whitespace(word
);
717 if ((rc
= find_end_of_word(word
, &end
)) != RC_OK
)
720 next
= skip_whitespace(end
);
723 strip_quotes(word
, &end
);
724 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word
));
726 /* First non-switch parameter is source, second is destination */
728 if (suppliedsource
[0] == 0x00) {
729 lstrcpyW(suppliedsource
, word
);
730 } else if (supplieddestination
[0] == 0x00) {
731 lstrcpyW(supplieddestination
, word
);
733 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS
));
737 /* Process all the switch options
738 Note: Windows docs say /P prompts when dest is created
739 but tests show it is done for each src file
740 regardless of the destination */
741 switch (toupper(word
[1])) {
742 case 'I': flags
|= OPT_ASSUMEDIR
; break;
743 case 'S': flags
|= OPT_RECURSIVE
; break;
744 case 'Q': flags
|= OPT_QUIET
; break;
745 case 'F': flags
|= OPT_FULL
; break;
746 case 'L': flags
|= OPT_SIMULATE
; break;
747 case 'W': flags
|= OPT_PAUSE
; break;
748 case 'T': flags
|= OPT_NOCOPY
| OPT_RECURSIVE
; break;
749 case 'Y': flags
|= OPT_NOPROMPT
; break;
750 case 'N': flags
|= OPT_SHORTNAME
; break;
751 case 'U': flags
|= OPT_MUSTEXIST
; break;
752 case 'R': flags
|= OPT_REPLACEREAD
; break;
753 case 'H': flags
|= OPT_COPYHIDSYS
; break;
754 case 'C': flags
|= OPT_IGNOREERRORS
; break;
755 case 'P': flags
|= OPT_SRCPROMPT
; break;
756 case 'A': flags
|= OPT_ARCHIVEONLY
; break;
757 case 'M': flags
|= OPT_ARCHIVEONLY
|
758 OPT_REMOVEARCH
; break;
760 /* E can be /E or /EXCLUDE */
761 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT
,
762 NORM_IGNORECASE
| SORT_STRINGSORT
,
764 EXCLUDE
, -1) == CSTR_EQUAL
) {
765 if (XCOPY_ProcessExcludeList(&word
[9])) {
766 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
768 } else flags
|= OPT_EXCLUDELIST
;
769 } else flags
|= OPT_EMPTYDIR
| OPT_RECURSIVE
;
772 /* D can be /D or /D: */
773 case 'D': if (word
[2]==':' && isdigit(word
[3])) {
775 WCHAR
*pos
= &word
[3];
776 BOOL isError
= FALSE
;
777 memset(&st
, 0x00, sizeof(st
));
779 /* Microsoft xcopy's usage message implies that the date
780 * format depends on the locale, but that is false.
781 * It is hardcoded to month-day-year.
783 st
.wMonth
= _wtol(pos
);
784 while (*pos
&& isdigit(*pos
)) pos
++;
785 if (*pos
++ != '-') isError
= TRUE
;
788 st
.wDay
= _wtol(pos
);
789 while (*pos
&& isdigit(*pos
)) pos
++;
790 if (*pos
++ != '-') isError
= TRUE
;
794 st
.wYear
= _wtol(pos
);
795 while (*pos
&& isdigit(*pos
)) pos
++;
796 if (st
.wYear
< 100) st
.wYear
+=2000;
799 if (!isError
&& SystemTimeToFileTime(&st
, &dateRange
)) {
801 WCHAR datestring
[32], timestring
[32];
803 flags
|= OPT_DATERANGE
;
806 FileTimeToSystemTime (&dateRange
, &st
);
807 GetDateFormatW(0, DATE_SHORTDATE
, &st
, NULL
, datestring
,
808 sizeof(datestring
)/sizeof(WCHAR
));
809 GetTimeFormatW(0, TIME_NOSECONDS
, &st
,
810 NULL
, timestring
, sizeof(timestring
)/sizeof(WCHAR
));
812 WINE_TRACE("Date being used is: %s %s\n",
813 wine_dbgstr_w(datestring
), wine_dbgstr_w(timestring
));
815 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
819 flags
|= OPT_DATENEWER
;
823 case '-': if (toupper(word
[2])=='Y')
824 flags
&= ~OPT_NOPROMPT
;
826 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP
));
830 WINE_FIXME("ignoring /V\n");
833 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word
));
834 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM
), word
);
841 /* Default the destination if not supplied */
842 if (supplieddestination
[0] == 0x00)
843 lstrcpyW(supplieddestination
, wchr_dot
);
854 /* =========================================================================
855 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
856 converts it into a stem and a filespec
857 ========================================================================= */
858 static int XCOPY_ProcessSourceParm(WCHAR
*suppliedsource
, WCHAR
*stem
,
859 WCHAR
*spec
, DWORD flags
)
861 WCHAR actualsource
[MAX_PATH
];
867 * Validate the source, expanding to full path ensuring it exists
869 if (GetFullPathNameW(suppliedsource
, MAX_PATH
, actualsource
, NULL
) == 0) {
870 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
874 /* If full names required, convert to using the full path */
875 if (flags
& OPT_FULL
) {
876 lstrcpyW(suppliedsource
, actualsource
);
880 * Work out the stem of the source
883 /* If a directory is supplied, use that as-is (either fully or
885 If a filename is supplied + a directory or drive path, use that
888 If no directory or path specified, add eg. C:
889 stem is Drive/Directory is bit up to last \ (or first :)
890 spec is bit after that */
892 starPos
= wcschr(suppliedsource
, '*');
893 questPos
= wcschr(suppliedsource
, '?');
894 if (starPos
|| questPos
) {
895 attribs
= 0x00; /* Ensures skips invalid or directory check below */
897 attribs
= GetFileAttributesW(actualsource
);
900 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
901 XCOPY_FailMessage(GetLastError());
905 stem should be exactly as supplied plus a '\', unless it was
906 eg. C: in which case no slash required */
907 } else if (attribs
& FILE_ATTRIBUTE_DIRECTORY
) {
910 WINE_TRACE("Directory supplied\n");
911 lstrcpyW(stem
, suppliedsource
);
912 lastChar
= stem
[lstrlenW(stem
)-1];
913 if (lastChar
!= '\\' && lastChar
!= ':') {
914 lstrcatW(stem
, wchr_slash
);
916 lstrcpyW(spec
, wchr_star
);
918 /* File or wildcard search:
920 Up to and including last slash if directory path supplied
921 If c:filename supplied, just the c:
922 Otherwise stem should be the current drive letter + ':' */
926 WINE_TRACE("Filename supplied\n");
927 lastDir
= wcsrchr(suppliedsource
, '\\');
930 lstrcpyW(stem
, suppliedsource
);
931 stem
[(lastDir
-suppliedsource
) + 1] = 0x00;
932 lstrcpyW(spec
, (lastDir
+1));
933 } else if (suppliedsource
[1] == ':') {
934 lstrcpyW(stem
, suppliedsource
);
936 lstrcpyW(spec
, suppliedsource
+2);
938 WCHAR curdir
[MAXSTRING
];
939 GetCurrentDirectoryW(sizeof(curdir
)/sizeof(WCHAR
), curdir
);
943 lstrcpyW(spec
, suppliedsource
);
950 /* =========================================================================
951 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
952 converts it into a stem
953 ========================================================================= */
954 static int XCOPY_ProcessDestParm(WCHAR
*supplieddestination
, WCHAR
*stem
, WCHAR
*spec
,
955 WCHAR
*srcspec
, DWORD flags
)
957 WCHAR actualdestination
[MAX_PATH
];
962 * Validate the source, expanding to full path ensuring it exists
964 if (GetFullPathNameW(supplieddestination
, MAX_PATH
, actualdestination
, NULL
) == 0) {
965 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
969 /* Destination is either a directory or a file */
970 attribs
= GetFileAttributesW(actualdestination
);
972 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
974 /* If /I supplied and wildcard copy, assume directory */
975 /* Also if destination ends with backslash */
976 if ((flags
& OPT_ASSUMEDIR
&&
977 (wcschr(srcspec
, '?') || wcschr(srcspec
, '*'))) ||
978 (supplieddestination
[lstrlenW(supplieddestination
)-1] == '\\')) {
984 char answer
[10] = "";
988 /* Read the F and D characters from the resource file */
989 wcscpy(fileChar
, XCOPY_LoadMessage(STRING_FILE_CHAR
));
990 wcscpy(dirChar
, XCOPY_LoadMessage(STRING_DIR_CHAR
));
992 while (answer
[0] != fileChar
[0] && answer
[0] != dirChar
[0]) {
993 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR
), supplieddestination
);
995 ReadFile(GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
), &count
, NULL
);
996 WINE_TRACE("User answer %c\n", answer
[0]);
998 answer
[0] = toupper(answer
[0]);
1001 if (answer
[0] == dirChar
[0]) {
1008 isDir
= (attribs
& FILE_ATTRIBUTE_DIRECTORY
);
1012 lstrcpyW(stem
, actualdestination
);
1015 /* Ensure ends with a '\' */
1016 if (stem
[lstrlenW(stem
)-1] != '\\') {
1017 lstrcatW(stem
, wchr_slash
);
1021 WCHAR drive
[MAX_PATH
];
1022 WCHAR dir
[MAX_PATH
];
1023 WCHAR fname
[MAX_PATH
];
1024 WCHAR ext
[MAX_PATH
];
1025 _wsplitpath(actualdestination
, drive
, dir
, fname
, ext
);
1026 lstrcpyW(stem
, drive
);
1027 lstrcatW(stem
, dir
);
1028 lstrcpyW(spec
, fname
);
1029 lstrcatW(spec
, ext
);
1035 /* =========================================================================
1036 main - Main entrypoint for the xcopy command
1038 Processes the args, and drives the actual copying
1039 ========================================================================= */
1040 int wmain (int argc
, WCHAR
*argvW
[])
1043 WCHAR suppliedsource
[MAX_PATH
] = {0}; /* As supplied on the cmd line */
1044 WCHAR supplieddestination
[MAX_PATH
] = {0};
1045 WCHAR sourcestem
[MAX_PATH
] = {0}; /* Stem of source */
1046 WCHAR sourcespec
[MAX_PATH
] = {0}; /* Filespec of source */
1047 WCHAR destinationstem
[MAX_PATH
] = {0}; /* Stem of destination */
1048 WCHAR destinationspec
[MAX_PATH
] = {0}; /* Filespec of destination */
1049 WCHAR copyCmd
[MAXSTRING
]; /* COPYCMD env var */
1050 DWORD flags
= 0; /* Option flags */
1051 const WCHAR PROMPTSTR1
[] = {'/', 'Y', 0};
1052 const WCHAR PROMPTSTR2
[] = {'/', 'y', 0};
1053 const WCHAR COPYCMD
[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1055 /* Preinitialize flags based on COPYCMD */
1056 if (GetEnvironmentVariableW(COPYCMD
, copyCmd
, MAXSTRING
)) {
1057 if (wcsstr(copyCmd
, PROMPTSTR1
) != NULL
||
1058 wcsstr(copyCmd
, PROMPTSTR2
) != NULL
) {
1059 flags
|= OPT_NOPROMPT
;
1063 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1064 wine, but on windows these can be normal files. At least one installer
1065 uses files such as .packlist and (validly) expects them to be copied.
1066 Under wine, if we do not copy hidden files by default then they get
1068 flags
|= OPT_COPYHIDSYS
;
1071 * Parse the command line
1073 if ((rc
= XCOPY_ParseCommandLine(suppliedsource
, supplieddestination
,
1074 &flags
)) != RC_OK
) {
1081 /* Trace out the supplied information */
1082 WINE_TRACE("Supplied parameters:\n");
1083 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource
));
1084 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination
));
1086 /* Extract required information from source specification */
1087 rc
= XCOPY_ProcessSourceParm(suppliedsource
, sourcestem
, sourcespec
, flags
);
1088 if (rc
!= RC_OK
) return rc
;
1090 /* Extract required information from destination specification */
1091 rc
= XCOPY_ProcessDestParm(supplieddestination
, destinationstem
,
1092 destinationspec
, sourcespec
, flags
);
1093 if (rc
!= RC_OK
) return rc
;
1095 /* Trace out the resulting information */
1096 WINE_TRACE("Resolved parameters:\n");
1097 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem
));
1098 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec
));
1099 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem
));
1100 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec
));
1102 /* Pause if necessary */
1103 if (flags
& OPT_PAUSE
) {
1107 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE
));
1108 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), pausestr
, sizeof(pausestr
),
1112 /* Now do the hard work... */
1113 rc
= XCOPY_DoCopy(sourcestem
, sourcespec
,
1114 destinationstem
, destinationspec
,
1117 /* Clear up exclude list allocated memory */
1118 while (excludeList
) {
1119 EXCLUDELIST
*pos
= excludeList
;
1120 excludeList
= excludeList
-> next
;
1121 HeapFree(GetProcessHeap(), 0, pos
->name
);
1122 HeapFree(GetProcessHeap(), 0, pos
);
1125 /* Finished - print trailer and exit */
1126 if (flags
& OPT_SIMULATE
) {
1127 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY
), filesCopied
);
1128 } else if (!(flags
& OPT_NOCOPY
)) {
1129 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY
), filesCopied
);
1131 if (rc
== RC_OK
&& filesCopied
== 0) rc
= RC_NOFILES
;