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>
51 WINE_DEFAULT_DEBUG_CHANNEL(xcopy
);
55 typedef struct _EXCLUDELIST
57 struct _EXCLUDELIST
*next
;
62 /* Global variables */
63 static ULONG filesCopied
= 0; /* Number of files copied */
64 static EXCLUDELIST
*excludeList
= NULL
; /* Excluded strings list */
65 static FILETIME dateRange
; /* Date range to copy after*/
68 /* To minimize stack usage during recursion, some temporary variables
70 static WCHAR copyFrom
[MAX_PATH
];
71 static WCHAR copyTo
[MAX_PATH
];
74 /* =========================================================================
75 * Load a string from the resource file, handling any error
76 * Returns string retrieved from resource file
77 * ========================================================================= */
78 static WCHAR
*XCOPY_LoadMessage(UINT id
) {
79 static WCHAR msg
[MAXSTRING
];
81 if (!LoadStringW(GetModuleHandleW(NULL
), id
, msg
, ARRAY_SIZE(msg
))) {
82 WINE_FIXME("LoadString failed with %d\n", GetLastError());
83 lstrcpyW(msg
, L
"Failed!");
88 /* =========================================================================
89 * Output a formatted unicode string. Ideally this will go to the console
90 * and hence required WriteConsoleW to output it, however if file i/o is
91 * redirected, it needs to be WriteFile'd using OEM (not ANSI) format
92 * ========================================================================= */
93 static int WINAPIV
XCOPY_wprintf(const WCHAR
*format
, ...) {
95 static WCHAR
*output_bufW
= NULL
;
96 static char *output_bufA
= NULL
;
97 static BOOL toConsole
= TRUE
;
98 static BOOL traceOutput
= FALSE
;
99 #define MAX_WRITECONSOLE_SIZE 65535
107 * Allocate buffer to use when writing to console
108 * Note: Not freed - memory will be allocated once and released when
112 if (!output_bufW
) output_bufW
= HeapAlloc(GetProcessHeap(), 0,
113 MAX_WRITECONSOLE_SIZE
*sizeof(WCHAR
));
115 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
119 __ms_va_start(parms
, format
);
120 len
= FormatMessageW(FORMAT_MESSAGE_FROM_STRING
, format
, 0, 0, output_bufW
,
121 MAX_WRITECONSOLE_SIZE
/sizeof(*output_bufW
), &parms
);
123 if (len
== 0 && GetLastError() != ERROR_NO_WORK_DONE
) {
124 WINE_FIXME("Could not format string: le=%u, fmt=%s\n", GetLastError(), wine_dbgstr_w(format
));
128 /* Try to write as unicode whenever we think it's a console */
130 res
= WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE
),
131 output_bufW
, len
, &nOut
, NULL
);
134 /* If writing to console has failed (ever) we assume it's file
135 i/o so convert to OEM codepage and output */
137 BOOL usedDefaultChar
= FALSE
;
138 DWORD convertedChars
;
143 * Allocate buffer to use when writing to file. Not freed, as above
145 if (!output_bufA
) output_bufA
= HeapAlloc(GetProcessHeap(), 0,
146 MAX_WRITECONSOLE_SIZE
);
148 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
152 /* Convert to OEM, then output */
153 convertedChars
= WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW
,
154 len
, output_bufA
, MAX_WRITECONSOLE_SIZE
,
155 "?", &usedDefaultChar
);
156 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE
), output_bufA
, convertedChars
,
160 /* Trace whether screen or console */
162 WINE_TRACE("Writing to console? (%d)\n", toConsole
);
168 /* =========================================================================
169 * Load a string for a system error and writes it to the screen
170 * Returns string retrieved from resource file
171 * ========================================================================= */
172 static void XCOPY_FailMessage(DWORD err
) {
176 status
= FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER
|
177 FORMAT_MESSAGE_FROM_SYSTEM
,
179 (LPWSTR
) &lpMsgBuf
, 0, NULL
);
181 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
182 err
, GetLastError());
184 XCOPY_wprintf(L
"%1\n", lpMsgBuf
);
185 LocalFree ((HLOCAL
)lpMsgBuf
);
190 /* =========================================================================
191 * Routine copied from cmd.exe md command -
192 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
193 * dir2 if they do not already exist.
194 * ========================================================================= */
195 static BOOL
XCOPY_CreateDirectory(const WCHAR
* path
)
201 new_path
= HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR
) * (lstrlenW(path
)+1));
202 lstrcpyW(new_path
,path
);
204 while ((len
= lstrlenW(new_path
)) && new_path
[len
- 1] == '\\')
205 new_path
[len
- 1] = 0;
207 while (!CreateDirectoryW(new_path
,NULL
))
210 DWORD last_error
= GetLastError();
211 if (last_error
== ERROR_ALREADY_EXISTS
)
214 if (last_error
!= ERROR_PATH_NOT_FOUND
)
220 if (!(slash
= wcsrchr(new_path
,'\\')) && ! (slash
= wcsrchr(new_path
,'/')))
226 len
= slash
- new_path
;
228 if (!XCOPY_CreateDirectory(new_path
))
233 new_path
[len
] = '\\';
235 HeapFree(GetProcessHeap(),0,new_path
);
239 /* =========================================================================
240 * Process a single file from the /EXCLUDE: file list, building up a list
241 * of substrings to avoid copying
242 * Returns TRUE on any failure
243 * ========================================================================= */
244 static BOOL
XCOPY_ProcessExcludeFile(WCHAR
* filename
, WCHAR
* endOfName
) {
246 WCHAR endChar
= *endOfName
;
247 WCHAR buffer
[MAXSTRING
];
250 /* Null terminate the filename (temporarily updates the filename hence
255 inFile
= _wfopen(filename
, L
"rt");
256 if (inFile
== NULL
) {
257 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL
), filename
);
258 *endOfName
= endChar
;
262 /* Process line by line */
263 while (fgetws(buffer
, ARRAY_SIZE(buffer
), inFile
) != NULL
) {
264 EXCLUDELIST
*thisEntry
;
265 int length
= lstrlenW(buffer
);
267 /* If more than CRLF */
269 buffer
[length
-1] = 0; /* strip CRLF */
270 thisEntry
= HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST
));
271 thisEntry
->next
= excludeList
;
272 excludeList
= thisEntry
;
273 thisEntry
->name
= HeapAlloc(GetProcessHeap(), 0,
274 (length
* sizeof(WCHAR
))+1);
275 lstrcpyW(thisEntry
->name
, buffer
);
276 CharUpperBuffW(thisEntry
->name
, length
);
277 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry
->name
));
281 /* See if EOF or error occurred */
283 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL
), filename
);
284 *endOfName
= endChar
;
289 /* Revert the input string to original form, and cleanup + return */
290 *endOfName
= endChar
;
295 /* =========================================================================
296 * Process the /EXCLUDE: file list, building up a list of substrings to
298 * Returns TRUE on any failure
299 * ========================================================================= */
300 static BOOL
XCOPY_ProcessExcludeList(WCHAR
* parms
) {
302 WCHAR
*filenameStart
= parms
;
304 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms
));
307 while (*parms
&& *parms
!= ' ' && *parms
!= '/') {
309 /* If found '+' then process the file found so far */
311 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
314 filenameStart
= parms
+1;
319 if (filenameStart
!= parms
) {
320 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
328 /* =========================================================================
329 XCOPY_DoCopy - Recursive function to copy files based on input parms
332 This works by using FindFirstFile supplying the source stem and spec.
333 If results are found, any non-directory ones are processed
334 Then, if /S or /E is supplied, another search is made just for
335 directories, and this function is called again for that directory
337 ========================================================================= */
338 static int XCOPY_DoCopy(WCHAR
*srcstem
, WCHAR
*srcspec
,
339 WCHAR
*deststem
, WCHAR
*destspec
,
342 WIN32_FIND_DATAW
*finddata
;
345 WCHAR
*inputpath
, *outputpath
;
346 BOOL copiedFile
= FALSE
;
347 DWORD destAttribs
, srcAttribs
;
351 /* Allocate some working memory on heap to minimize footprint */
352 finddata
= HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW
));
353 inputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
354 outputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
356 /* Build the search info into a single parm */
357 lstrcpyW(inputpath
, srcstem
);
358 lstrcatW(inputpath
, srcspec
);
360 /* Search 1 - Look for matching files */
361 h
= FindFirstFileW(inputpath
, finddata
);
362 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
366 /* Ignore . and .. */
367 if (lstrcmpW(finddata
->cFileName
, L
".")==0 ||
368 lstrcmpW(finddata
->cFileName
, L
"..")==0 ||
369 finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) {
371 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata
->cFileName
));
374 /* Get the filename information */
375 lstrcpyW(copyFrom
, srcstem
);
376 if (flags
& OPT_SHORTNAME
) {
377 lstrcatW(copyFrom
, finddata
->cAlternateFileName
);
379 lstrcatW(copyFrom
, finddata
->cFileName
);
382 lstrcpyW(copyTo
, deststem
);
383 if (*destspec
== 0x00) {
384 if (flags
& OPT_SHORTNAME
) {
385 lstrcatW(copyTo
, finddata
->cAlternateFileName
);
387 lstrcatW(copyTo
, finddata
->cFileName
);
390 lstrcatW(copyTo
, destspec
);
394 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom
),
395 wine_dbgstr_w(copyTo
));
396 if (!copiedFile
&& !(flags
& OPT_SIMULATE
)) XCOPY_CreateDirectory(deststem
);
398 /* See if allowed to copy it */
399 srcAttribs
= GetFileAttributesW(copyFrom
);
400 WINE_TRACE("Source attribs: %d\n", srcAttribs
);
402 if ((srcAttribs
& FILE_ATTRIBUTE_HIDDEN
) ||
403 (srcAttribs
& FILE_ATTRIBUTE_SYSTEM
)) {
405 if (!(flags
& OPT_COPYHIDSYS
)) {
410 if (!(srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
411 (flags
& OPT_ARCHIVEONLY
)) {
415 /* See if file exists */
416 destAttribs
= GetFileAttributesW(copyTo
);
417 WINE_TRACE("Dest attribs: %d\n", srcAttribs
);
419 /* Check date ranges if a destination file already exists */
420 if (!skipFile
&& (flags
& OPT_DATERANGE
) &&
421 (CompareFileTime(&finddata
->ftLastWriteTime
, &dateRange
) < 0)) {
422 WINE_TRACE("Skipping file as modified date too old\n");
426 /* If just /D supplied, only overwrite if src newer than dest */
427 if (!skipFile
&& (flags
& OPT_DATENEWER
) &&
428 (destAttribs
!= INVALID_FILE_ATTRIBUTES
)) {
429 HANDLE h
= CreateFileW(copyTo
, GENERIC_READ
, FILE_SHARE_READ
,
430 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
,
432 if (h
!= INVALID_HANDLE_VALUE
) {
434 GetFileTime(h
, NULL
, NULL
, &writeTime
);
436 if (CompareFileTime(&finddata
->ftLastWriteTime
, &writeTime
) <= 0) {
437 WINE_TRACE("Skipping file as dest newer or same date\n");
444 /* See if exclude list provided. Note since filenames are case
445 insensitive, need to uppercase the filename before doing
447 if (!skipFile
&& (flags
& OPT_EXCLUDELIST
)) {
448 EXCLUDELIST
*pos
= excludeList
;
449 WCHAR copyFromUpper
[MAX_PATH
];
451 /* Uppercase source filename */
452 lstrcpyW(copyFromUpper
, copyFrom
);
453 CharUpperBuffW(copyFromUpper
, lstrlenW(copyFromUpper
));
455 /* Loop through testing each exclude line */
457 if (wcsstr(copyFromUpper
, pos
->name
) != NULL
) {
458 WINE_TRACE("Skipping file as matches exclude '%s'\n",
459 wine_dbgstr_w(pos
->name
));
468 /* Prompt each file if necessary */
469 if (!skipFile
&& (flags
& OPT_SRCPROMPT
)) {
472 BOOL answered
= FALSE
;
476 /* Read the Y and N characters from the resource file */
477 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
478 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
481 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT
), copyFrom
);
482 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
486 if (toupper(answer
[0]) == noChar
[0])
488 else if (toupper(answer
[0]) != yesChar
[0])
494 destAttribs
!= INVALID_FILE_ATTRIBUTES
&& !(flags
& OPT_NOPROMPT
)) {
497 BOOL answered
= FALSE
;
502 /* Read the A,Y and N characters from the resource file */
503 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
504 wcscpy(allChar
, XCOPY_LoadMessage(STRING_ALL_CHAR
));
505 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
508 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE
), copyTo
);
509 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
513 if (toupper(answer
[0]) == allChar
[0])
514 flags
|= OPT_NOPROMPT
;
515 else if (toupper(answer
[0]) == noChar
[0])
517 else if (toupper(answer
[0]) != yesChar
[0])
522 /* See if it has to exist! */
523 if (destAttribs
== INVALID_FILE_ATTRIBUTES
&& (flags
& OPT_MUSTEXIST
)) {
527 /* Output a status message */
529 if (!(flags
& OPT_QUIET
)) {
530 if (flags
& OPT_FULL
)
531 XCOPY_wprintf(L
"%1 -> %2\n", copyFrom
, copyTo
);
533 XCOPY_wprintf(L
"%1\n", copyFrom
);
536 /* If allowing overwriting of read only files, remove any
538 if ((destAttribs
& FILE_ATTRIBUTE_READONLY
) &&
539 (flags
& OPT_REPLACEREAD
)) {
540 SetFileAttributesW(copyTo
, destAttribs
& ~FILE_ATTRIBUTE_READONLY
);
544 if (flags
& OPT_SIMULATE
|| flags
& OPT_NOCOPY
) {
546 } else if (CopyFileW(copyFrom
, copyTo
, FALSE
) == 0) {
548 DWORD error
= GetLastError();
549 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL
),
550 copyFrom
, copyTo
, error
);
551 XCOPY_FailMessage(error
);
553 if (flags
& OPT_IGNOREERRORS
) {
562 /* If keeping attributes, update the destination attributes
563 otherwise remove the read only attribute */
564 if (flags
& OPT_KEEPATTRS
) {
565 SetFileAttributesW(copyTo
, srcAttribs
| FILE_ATTRIBUTE_ARCHIVE
);
567 SetFileAttributesW(copyTo
,
568 (GetFileAttributesW(copyTo
) & ~FILE_ATTRIBUTE_READONLY
));
571 /* If /M supplied, remove the archive bit after successful copy */
572 if ((srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
573 (flags
& OPT_REMOVEARCH
)) {
574 SetFileAttributesW(copyFrom
, (srcAttribs
& ~FILE_ATTRIBUTE_ARCHIVE
));
583 findres
= FindNextFileW(h
, finddata
);
587 /* Search 2 - do subdirs */
588 if (flags
& OPT_RECURSIVE
) {
590 /* If /E is supplied, create the directory now */
591 if ((flags
& OPT_EMPTYDIR
) &&
592 !(flags
& OPT_SIMULATE
)) {
593 XCOPY_CreateDirectory(deststem
);
596 lstrcpyW(inputpath
, srcstem
);
597 lstrcatW(inputpath
, L
"*");
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
, L
".") != 0) &&
607 (lstrcmpW(finddata
->cFileName
, L
"..") != 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
, L
"\\");
616 lstrcpyW(outputpath
, deststem
);
617 if (*destspec
== 0x00) {
618 lstrcatW(outputpath
, finddata
->cFileName
);
619 lstrcatW(outputpath
, L
"\\");
622 XCOPY_DoCopy(inputpath
, srcspec
, outputpath
, destspec
, flags
);
626 findres
= FindNextFileW(h
, finddata
);
634 HeapFree(GetProcessHeap(), 0, finddata
);
635 HeapFree(GetProcessHeap(), 0, inputpath
);
636 HeapFree(GetProcessHeap(), 0, outputpath
);
642 /* =========================================================================
643 XCOPY_ParseCommandLine - Parses the command line
644 ========================================================================= */
645 static inline BOOL
is_whitespace(WCHAR c
)
647 return c
== ' ' || c
== '\t';
650 static WCHAR
*skip_whitespace(WCHAR
*p
)
652 for (; *p
&& is_whitespace(*p
); p
++);
656 static inline BOOL
is_digit(WCHAR c
)
658 return c
>= '0' && c
<= '9';
661 /* Windows XCOPY uses a simplified command line parsing algorithm
662 that lacks the escaped-quote logic of build_argv(), because
663 literal double quotes are illegal in any of its arguments.
664 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
665 static int find_end_of_word(const WCHAR
*word
, WCHAR
**end
)
667 BOOL in_quotes
= FALSE
;
668 const WCHAR
*ptr
= word
;
670 for (; *ptr
!= '\0' && *ptr
!= '"' &&
671 (in_quotes
|| !is_whitespace(*ptr
)); ptr
++);
673 in_quotes
= !in_quotes
;
676 /* Odd number of double quotes is illegal for XCOPY */
677 if (in_quotes
&& *ptr
== '\0')
679 if (*ptr
== '\0' || (!in_quotes
&& is_whitespace(*ptr
)))
686 /* Remove all double quotes from a word */
687 static void strip_quotes(WCHAR
*word
, WCHAR
**end
)
690 for (rp
= word
, wp
= word
; *rp
!= '\0'; rp
++) {
701 static int XCOPY_ParseCommandLine(WCHAR
*suppliedsource
,
702 WCHAR
*supplieddestination
, DWORD
*pflags
)
704 DWORD flags
= *pflags
;
705 WCHAR
*cmdline
, *word
, *end
, *next
;
706 int rc
= RC_INITERROR
;
708 cmdline
= _wcsdup(GetCommandLineW());
712 /* Skip first arg, which is the program name */
713 if ((rc
= find_end_of_word(cmdline
, &word
)) != RC_OK
)
715 word
= skip_whitespace(word
);
720 if ((rc
= find_end_of_word(word
, &end
)) != RC_OK
)
723 next
= skip_whitespace(end
);
726 strip_quotes(word
, &end
);
727 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word
));
729 /* First non-switch parameter is source, second is destination */
731 if (suppliedsource
[0] == 0x00) {
732 lstrcpyW(suppliedsource
, word
);
733 } else if (supplieddestination
[0] == 0x00) {
734 lstrcpyW(supplieddestination
, word
);
736 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS
));
740 /* Process all the switch options
741 Note: Windows docs say /P prompts when dest is created
742 but tests show it is done for each src file
743 regardless of the destination */
750 switch (toupper(word
[1])) {
751 case 'I': flags
|= OPT_ASSUMEDIR
; break;
752 case 'S': flags
|= OPT_RECURSIVE
; break;
753 case 'Q': flags
|= OPT_QUIET
; break;
754 case 'F': flags
|= OPT_FULL
; break;
755 case 'L': flags
|= OPT_SIMULATE
; break;
756 case 'W': flags
|= OPT_PAUSE
; break;
757 case 'T': flags
|= OPT_NOCOPY
| OPT_RECURSIVE
; break;
758 case 'Y': flags
|= OPT_NOPROMPT
; break;
759 case 'N': flags
|= OPT_SHORTNAME
; break;
760 case 'U': flags
|= OPT_MUSTEXIST
; break;
761 case 'R': flags
|= OPT_REPLACEREAD
; break;
762 case 'K': flags
|= OPT_KEEPATTRS
; break;
763 case 'H': flags
|= OPT_COPYHIDSYS
; break;
764 case 'C': flags
|= OPT_IGNOREERRORS
; break;
765 case 'P': flags
|= OPT_SRCPROMPT
; break;
766 case 'A': flags
|= OPT_ARCHIVEONLY
; break;
767 case 'M': flags
|= OPT_ARCHIVEONLY
|
768 OPT_REMOVEARCH
; break;
770 /* E can be /E or /EXCLUDE */
771 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT
, NORM_IGNORECASE
| SORT_STRINGSORT
,
772 &word
[1], 8, L
"EXCLUDE:", -1) == CSTR_EQUAL
) {
773 if (XCOPY_ProcessExcludeList(&word
[9])) {
774 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
777 flags
|= OPT_EXCLUDELIST
;
779 /* Do not support concatenated switches onto exclude lists yet */
783 flags
|= OPT_EMPTYDIR
| OPT_RECURSIVE
;
787 /* D can be /D or /D: */
788 case 'D': if (word
[2]==':' && is_digit(word
[3])) {
790 WCHAR
*pos
= &word
[3];
791 BOOL isError
= FALSE
;
792 memset(&st
, 0x00, sizeof(st
));
794 /* Microsoft xcopy's usage message implies that the date
795 * format depends on the locale, but that is false.
796 * It is hardcoded to month-day-year.
798 st
.wMonth
= _wtol(pos
);
799 while (*pos
&& is_digit(*pos
)) pos
++;
800 if (*pos
++ != '-') isError
= TRUE
;
803 st
.wDay
= _wtol(pos
);
804 while (*pos
&& is_digit(*pos
)) pos
++;
805 if (*pos
++ != '-') isError
= TRUE
;
809 st
.wYear
= _wtol(pos
);
810 while (*pos
&& is_digit(*pos
)) pos
++;
811 if (st
.wYear
< 100) st
.wYear
+=2000;
814 /* Handle switches straight after the supplied date */
817 if (!isError
&& SystemTimeToFileTime(&st
, &dateRange
)) {
819 WCHAR datestring
[32], timestring
[32];
821 flags
|= OPT_DATERANGE
;
824 FileTimeToSystemTime (&dateRange
, &st
);
825 GetDateFormatW(0, DATE_SHORTDATE
, &st
, NULL
, datestring
,
826 ARRAY_SIZE(datestring
));
827 GetTimeFormatW(0, TIME_NOSECONDS
, &st
,
828 NULL
, timestring
, ARRAY_SIZE(timestring
));
830 WINE_TRACE("Date being used is: %s %s\n",
831 wine_dbgstr_w(datestring
), wine_dbgstr_w(timestring
));
833 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
837 flags
|= OPT_DATENEWER
;
841 case '-': if (toupper(word
[2])=='Y') {
842 flags
&= ~OPT_NOPROMPT
;
843 rest
= &word
[3]; /* Skip over 3 characters */
846 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP
));
850 WINE_FIXME("ignoring /V\n");
853 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word
));
854 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM
), word
);
858 /* Unless overridden above, skip over the '/' and the first character */
859 if (rest
== NULL
) rest
= &word
[2];
861 /* By now, rest should point either to the null after the
862 switch, or the beginning of the next switch if there
863 was no whitespace between them */
864 if (!skip
&& *rest
&& *rest
!= '/') {
865 WINE_FIXME("Unexpected characters found and ignored '%s'\n", wine_dbgstr_w(rest
));
875 /* Default the destination if not supplied */
876 if (supplieddestination
[0] == 0x00)
877 lstrcpyW(supplieddestination
, L
".");
888 /* =========================================================================
889 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
890 converts it into a stem and a filespec
891 ========================================================================= */
892 static int XCOPY_ProcessSourceParm(WCHAR
*suppliedsource
, WCHAR
*stem
,
893 WCHAR
*spec
, DWORD flags
)
895 WCHAR actualsource
[MAX_PATH
];
901 * Validate the source, expanding to full path ensuring it exists
903 if (GetFullPathNameW(suppliedsource
, MAX_PATH
, actualsource
, NULL
) == 0) {
904 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
908 /* If full names required, convert to using the full path */
909 if (flags
& OPT_FULL
) {
910 lstrcpyW(suppliedsource
, actualsource
);
914 * Work out the stem of the source
917 /* If a directory is supplied, use that as-is (either fully or
919 If a filename is supplied + a directory or drive path, use that
922 If no directory or path specified, add eg. C:
923 stem is Drive/Directory is bit up to last \ (or first :)
924 spec is bit after that */
926 starPos
= wcschr(suppliedsource
, '*');
927 questPos
= wcschr(suppliedsource
, '?');
928 if (starPos
|| questPos
) {
929 attribs
= 0x00; /* Ensures skips invalid or directory check below */
931 attribs
= GetFileAttributesW(actualsource
);
934 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
935 XCOPY_FailMessage(GetLastError());
939 stem should be exactly as supplied plus a '\', unless it was
940 eg. C: in which case no slash required */
941 } else if (attribs
& FILE_ATTRIBUTE_DIRECTORY
) {
944 WINE_TRACE("Directory supplied\n");
945 lstrcpyW(stem
, suppliedsource
);
946 lastChar
= stem
[lstrlenW(stem
)-1];
947 if (lastChar
!= '\\' && lastChar
!= ':')
948 lstrcatW(stem
, L
"\\");
949 lstrcpyW(spec
, L
"*");
951 /* File or wildcard search:
953 Up to and including last slash if directory path supplied
954 If c:filename supplied, just the c:
955 Otherwise stem should be the current drive letter + ':' */
959 WINE_TRACE("Filename supplied\n");
960 lastDir
= wcsrchr(suppliedsource
, '\\');
963 lstrcpyW(stem
, suppliedsource
);
964 stem
[(lastDir
-suppliedsource
) + 1] = 0x00;
965 lstrcpyW(spec
, (lastDir
+1));
966 } else if (suppliedsource
[1] == ':') {
967 lstrcpyW(stem
, suppliedsource
);
969 lstrcpyW(spec
, suppliedsource
+2);
971 WCHAR curdir
[MAXSTRING
];
972 GetCurrentDirectoryW(ARRAY_SIZE(curdir
), curdir
);
976 lstrcpyW(spec
, suppliedsource
);
983 /* =========================================================================
984 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
985 converts it into a stem
986 ========================================================================= */
987 static int XCOPY_ProcessDestParm(WCHAR
*supplieddestination
, WCHAR
*stem
, WCHAR
*spec
,
988 WCHAR
*srcspec
, DWORD flags
)
990 WCHAR actualdestination
[MAX_PATH
];
995 * Validate the source, expanding to full path ensuring it exists
997 if (GetFullPathNameW(supplieddestination
, MAX_PATH
, actualdestination
, NULL
) == 0) {
998 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
1002 /* Destination is either a directory or a file */
1003 attribs
= GetFileAttributesW(actualdestination
);
1005 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
1007 /* If /I supplied and wildcard copy, assume directory */
1008 /* Also if destination ends with backslash */
1009 if ((flags
& OPT_ASSUMEDIR
&&
1010 (wcschr(srcspec
, '?') || wcschr(srcspec
, '*'))) ||
1011 (supplieddestination
[lstrlenW(supplieddestination
)-1] == '\\')) {
1017 char answer
[10] = "";
1021 /* Read the F and D characters from the resource file */
1022 wcscpy(fileChar
, XCOPY_LoadMessage(STRING_FILE_CHAR
));
1023 wcscpy(dirChar
, XCOPY_LoadMessage(STRING_DIR_CHAR
));
1025 while (answer
[0] != fileChar
[0] && answer
[0] != dirChar
[0]) {
1026 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR
), supplieddestination
);
1028 ReadFile(GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
), &count
, NULL
);
1029 WINE_TRACE("User answer %c\n", answer
[0]);
1031 answer
[0] = toupper(answer
[0]);
1034 if (answer
[0] == dirChar
[0]) {
1041 isDir
= (attribs
& FILE_ATTRIBUTE_DIRECTORY
);
1045 lstrcpyW(stem
, actualdestination
);
1048 /* Ensure ends with a '\' */
1049 if (stem
[lstrlenW(stem
)-1] != '\\')
1050 lstrcatW(stem
, L
"\\");
1053 WCHAR drive
[MAX_PATH
];
1054 WCHAR dir
[MAX_PATH
];
1055 WCHAR fname
[MAX_PATH
];
1056 WCHAR ext
[MAX_PATH
];
1057 _wsplitpath(actualdestination
, drive
, dir
, fname
, ext
);
1058 lstrcpyW(stem
, drive
);
1059 lstrcatW(stem
, dir
);
1060 lstrcpyW(spec
, fname
);
1061 lstrcatW(spec
, ext
);
1067 /* =========================================================================
1068 main - Main entrypoint for the xcopy command
1070 Processes the args, and drives the actual copying
1071 ========================================================================= */
1072 int __cdecl
wmain (int argc
, WCHAR
*argvW
[])
1075 WCHAR suppliedsource
[MAX_PATH
] = {0}; /* As supplied on the cmd line */
1076 WCHAR supplieddestination
[MAX_PATH
] = {0};
1077 WCHAR sourcestem
[MAX_PATH
] = {0}; /* Stem of source */
1078 WCHAR sourcespec
[MAX_PATH
] = {0}; /* Filespec of source */
1079 WCHAR destinationstem
[MAX_PATH
] = {0}; /* Stem of destination */
1080 WCHAR destinationspec
[MAX_PATH
] = {0}; /* Filespec of destination */
1081 WCHAR copyCmd
[MAXSTRING
]; /* COPYCMD env var */
1082 DWORD flags
= 0; /* Option flags */
1084 /* Preinitialize flags based on COPYCMD */
1085 if (GetEnvironmentVariableW(L
"COPYCMD", copyCmd
, MAXSTRING
)) {
1086 if (wcsstr(copyCmd
, L
"/Y") != NULL
|| wcsstr(copyCmd
, L
"/y") != NULL
)
1087 flags
|= OPT_NOPROMPT
;
1090 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1091 wine, but on windows these can be normal files. At least one installer
1092 uses files such as .packlist and (validly) expects them to be copied.
1093 Under wine, if we do not copy hidden files by default then they get
1095 flags
|= OPT_COPYHIDSYS
;
1098 * Parse the command line
1100 if ((rc
= XCOPY_ParseCommandLine(suppliedsource
, supplieddestination
,
1101 &flags
)) != RC_OK
) {
1108 /* Trace out the supplied information */
1109 WINE_TRACE("Supplied parameters:\n");
1110 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource
));
1111 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination
));
1113 /* Extract required information from source specification */
1114 rc
= XCOPY_ProcessSourceParm(suppliedsource
, sourcestem
, sourcespec
, flags
);
1115 if (rc
!= RC_OK
) return rc
;
1117 /* Extract required information from destination specification */
1118 rc
= XCOPY_ProcessDestParm(supplieddestination
, destinationstem
,
1119 destinationspec
, sourcespec
, flags
);
1120 if (rc
!= RC_OK
) return rc
;
1122 /* Trace out the resulting information */
1123 WINE_TRACE("Resolved parameters:\n");
1124 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem
));
1125 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec
));
1126 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem
));
1127 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec
));
1129 /* Pause if necessary */
1130 if (flags
& OPT_PAUSE
) {
1134 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE
));
1135 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), pausestr
, sizeof(pausestr
),
1139 /* Now do the hard work... */
1140 rc
= XCOPY_DoCopy(sourcestem
, sourcespec
,
1141 destinationstem
, destinationspec
,
1144 /* Clear up exclude list allocated memory */
1145 while (excludeList
) {
1146 EXCLUDELIST
*pos
= excludeList
;
1147 excludeList
= excludeList
-> next
;
1148 HeapFree(GetProcessHeap(), 0, pos
->name
);
1149 HeapFree(GetProcessHeap(), 0, pos
);
1152 /* Finished - print trailer and exit */
1153 if (flags
& OPT_SIMULATE
) {
1154 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY
), filesCopied
);
1155 } else if (!(flags
& OPT_NOCOPY
)) {
1156 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY
), filesCopied
);