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 %ld\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 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=%lu, 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(GetOEMCP(), 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 %ld, status %ld\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: %ld\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: %ld\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 const WCHAR
*skip_whitespace(const 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 get_arg(const WCHAR
**cmdline
, WCHAR
**arg
)
667 const WCHAR
*ptr
= *cmdline
;
670 if (*ptr
== '/') ptr
++;
671 while (*ptr
&& !is_whitespace(*ptr
) && *ptr
!= '/') {
673 while (*ptr
&& *ptr
!= '"') ptr
++;
674 /* Odd number of double quotes is illegal for XCOPY */
675 if (!*ptr
) return RC_INITERROR
;
680 len
= ptr
- *cmdline
;
681 *arg
= malloc((len
+ 1) * sizeof(WCHAR
));
684 memcpy(*arg
, *cmdline
, len
* sizeof(WCHAR
));
687 *cmdline
= skip_whitespace(ptr
);
691 /* Remove all double quotes from a word */
692 static void strip_quotes(WCHAR
*word
)
695 for (wp
= word
; *word
!= '\0'; word
++) {
705 static int XCOPY_ParseCommandLine(WCHAR
*suppliedsource
,
706 WCHAR
*supplieddestination
, DWORD
*pflags
)
708 DWORD flags
= *pflags
;
709 const WCHAR
*cmdline
;
713 cmdline
= GetCommandLineW();
714 /* Skip first arg, which is the program name */
715 if ((rc
= get_arg(&cmdline
, &word
)) != RC_OK
)
721 if ((rc
= get_arg(&cmdline
, &word
)) != RC_OK
)
723 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word
));
725 /* First non-switch parameter is source, second is destination */
726 if (word
[0] != '/') {
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 WCHAR
*p
= word
+ 1, *rest
;
746 switch (toupper(*p
)) {
747 case 'I': flags
|= OPT_ASSUMEDIR
; break;
748 case 'S': flags
|= OPT_RECURSIVE
; break;
749 case 'Q': flags
|= OPT_QUIET
; break;
750 case 'F': flags
|= OPT_FULL
; break;
751 case 'L': flags
|= OPT_SIMULATE
; break;
752 case 'W': flags
|= OPT_PAUSE
; break;
753 case 'T': flags
|= OPT_NOCOPY
| OPT_RECURSIVE
; break;
754 case 'Y': flags
|= OPT_NOPROMPT
; break;
755 case 'N': flags
|= OPT_SHORTNAME
; break;
756 case 'U': flags
|= OPT_MUSTEXIST
; break;
757 case 'R': flags
|= OPT_REPLACEREAD
; break;
758 case 'K': flags
|= OPT_KEEPATTRS
; break;
759 case 'H': flags
|= OPT_COPYHIDSYS
; break;
760 case 'C': flags
|= OPT_IGNOREERRORS
; break;
761 case 'P': flags
|= OPT_SRCPROMPT
; break;
762 case 'A': flags
|= OPT_ARCHIVEONLY
; break;
763 case 'M': flags
|= OPT_ARCHIVEONLY
|
764 OPT_REMOVEARCH
; break;
766 /* E can be /E or /EXCLUDE */
767 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT
, NORM_IGNORECASE
| SORT_STRINGSORT
,
768 p
, 8, L
"EXCLUDE:", -1) == CSTR_EQUAL
) {
769 if (XCOPY_ProcessExcludeList(&p
[8])) {
770 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
773 flags
|= OPT_EXCLUDELIST
;
774 rest
= p
+ wcslen(p
);
777 flags
|= OPT_EMPTYDIR
| OPT_RECURSIVE
;
781 /* D can be /D or /D: */
782 case 'D': if (p
[1]==':' && is_digit(p
[2])) {
785 BOOL isError
= FALSE
;
786 memset(&st
, 0x00, sizeof(st
));
788 /* Microsoft xcopy's usage message implies that the date
789 * format depends on the locale, but that is false.
790 * It is hardcoded to month-day-year.
792 st
.wMonth
= _wtol(pos
);
793 while (*pos
&& is_digit(*pos
)) pos
++;
794 if (*pos
++ != '-') isError
= TRUE
;
797 st
.wDay
= _wtol(pos
);
798 while (*pos
&& is_digit(*pos
)) pos
++;
799 if (*pos
++ != '-') isError
= TRUE
;
803 st
.wYear
= _wtol(pos
);
804 while (*pos
&& is_digit(*pos
)) pos
++;
805 if (st
.wYear
< 100) st
.wYear
+=2000;
808 /* Handle switches straight after the supplied date */
811 if (!isError
&& SystemTimeToFileTime(&st
, &dateRange
)) {
813 WCHAR datestring
[32], timestring
[32];
815 flags
|= OPT_DATERANGE
;
818 FileTimeToSystemTime (&dateRange
, &st
);
819 GetDateFormatW(0, DATE_SHORTDATE
, &st
, NULL
, datestring
,
820 ARRAY_SIZE(datestring
));
821 GetTimeFormatW(0, TIME_NOSECONDS
, &st
,
822 NULL
, timestring
, ARRAY_SIZE(timestring
));
824 WINE_TRACE("Date being used is: %s %s\n",
825 wine_dbgstr_w(datestring
), wine_dbgstr_w(timestring
));
827 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
831 flags
|= OPT_DATENEWER
;
835 case '-': if (toupper(p
[1])=='Y') {
836 flags
&= ~OPT_NOPROMPT
;
837 rest
= &p
[2]; /* Skip over 2 characters */
840 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP
));
843 WINE_FIXME("ignoring /V\n");
846 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(p
));
847 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM
), p
);
857 /* Default the destination if not supplied */
858 if (supplieddestination
[0] == 0x00)
859 lstrcpyW(supplieddestination
, L
".");
866 /* =========================================================================
867 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
868 converts it into a stem and a filespec
869 ========================================================================= */
870 static int XCOPY_ProcessSourceParm(WCHAR
*suppliedsource
, WCHAR
*stem
,
871 WCHAR
*spec
, DWORD flags
)
873 WCHAR actualsource
[MAX_PATH
];
879 * Validate the source, expanding to full path ensuring it exists
881 if (GetFullPathNameW(suppliedsource
, MAX_PATH
, actualsource
, NULL
) == 0) {
882 WINE_FIXME("Unexpected failure expanding source path (%ld)\n", GetLastError());
886 /* If full names required, convert to using the full path */
887 if (flags
& OPT_FULL
) {
888 lstrcpyW(suppliedsource
, actualsource
);
892 * Work out the stem of the source
895 /* If a directory is supplied, use that as-is (either fully or
897 If a filename is supplied + a directory or drive path, use that
900 If no directory or path specified, add eg. C:
901 stem is Drive/Directory is bit up to last \ (or first :)
902 spec is bit after that */
904 starPos
= wcschr(suppliedsource
, '*');
905 questPos
= wcschr(suppliedsource
, '?');
906 if (starPos
|| questPos
) {
907 attribs
= 0x00; /* Ensures skips invalid or directory check below */
909 attribs
= GetFileAttributesW(actualsource
);
912 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
913 XCOPY_FailMessage(GetLastError());
917 stem should be exactly as supplied plus a '\', unless it was
918 eg. C: in which case no slash required */
919 } else if (attribs
& FILE_ATTRIBUTE_DIRECTORY
) {
922 WINE_TRACE("Directory supplied\n");
923 lstrcpyW(stem
, suppliedsource
);
924 lastChar
= stem
[lstrlenW(stem
)-1];
925 if (lastChar
!= '\\' && lastChar
!= ':')
926 lstrcatW(stem
, L
"\\");
927 lstrcpyW(spec
, L
"*");
929 /* File or wildcard search:
931 Up to and including last slash if directory path supplied
932 If c:filename supplied, just the c:
933 Otherwise stem should be the current drive letter + ':' */
937 WINE_TRACE("Filename supplied\n");
938 lastDir
= wcsrchr(suppliedsource
, '\\');
941 lstrcpyW(stem
, suppliedsource
);
942 stem
[(lastDir
-suppliedsource
) + 1] = 0x00;
943 lstrcpyW(spec
, (lastDir
+1));
944 } else if (suppliedsource
[1] == ':') {
945 lstrcpyW(stem
, suppliedsource
);
947 lstrcpyW(spec
, suppliedsource
+2);
949 WCHAR curdir
[MAXSTRING
];
950 GetCurrentDirectoryW(ARRAY_SIZE(curdir
), curdir
);
954 lstrcpyW(spec
, suppliedsource
);
961 /* =========================================================================
962 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
963 converts it into a stem
964 ========================================================================= */
965 static int XCOPY_ProcessDestParm(WCHAR
*supplieddestination
, WCHAR
*stem
, WCHAR
*spec
,
966 WCHAR
*srcspec
, DWORD flags
)
968 WCHAR actualdestination
[MAX_PATH
];
973 * Validate the source, expanding to full path ensuring it exists
975 if (GetFullPathNameW(supplieddestination
, MAX_PATH
, actualdestination
, NULL
) == 0) {
976 WINE_FIXME("Unexpected failure expanding source path (%ld)\n", GetLastError());
980 /* Destination is either a directory or a file */
981 attribs
= GetFileAttributesW(actualdestination
);
983 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
985 /* If /I supplied and wildcard copy, assume directory */
986 /* Also if destination ends with backslash */
987 if ((flags
& OPT_ASSUMEDIR
&&
988 (wcschr(srcspec
, '?') || wcschr(srcspec
, '*'))) ||
989 (supplieddestination
[lstrlenW(supplieddestination
)-1] == '\\')) {
995 char answer
[10] = "";
999 /* Read the F and D characters from the resource file */
1000 wcscpy(fileChar
, XCOPY_LoadMessage(STRING_FILE_CHAR
));
1001 wcscpy(dirChar
, XCOPY_LoadMessage(STRING_DIR_CHAR
));
1003 while (answer
[0] != fileChar
[0] && answer
[0] != dirChar
[0]) {
1004 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR
), supplieddestination
);
1006 ReadFile(GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
), &count
, NULL
);
1007 WINE_TRACE("User answer %c\n", answer
[0]);
1009 answer
[0] = toupper(answer
[0]);
1012 if (answer
[0] == dirChar
[0]) {
1019 isDir
= (attribs
& FILE_ATTRIBUTE_DIRECTORY
);
1023 lstrcpyW(stem
, actualdestination
);
1026 /* Ensure ends with a '\' */
1027 if (stem
[lstrlenW(stem
)-1] != '\\')
1028 lstrcatW(stem
, L
"\\");
1031 WCHAR drive
[MAX_PATH
];
1032 WCHAR dir
[MAX_PATH
];
1033 WCHAR fname
[MAX_PATH
];
1034 WCHAR ext
[MAX_PATH
];
1035 _wsplitpath(actualdestination
, drive
, dir
, fname
, ext
);
1036 lstrcpyW(stem
, drive
);
1037 lstrcatW(stem
, dir
);
1038 lstrcpyW(spec
, fname
);
1039 lstrcatW(spec
, ext
);
1045 /* =========================================================================
1046 main - Main entrypoint for the xcopy command
1048 Processes the args, and drives the actual copying
1049 ========================================================================= */
1050 int __cdecl
wmain (int argc
, WCHAR
*argvW
[])
1053 WCHAR suppliedsource
[MAX_PATH
] = {0}; /* As supplied on the cmd line */
1054 WCHAR supplieddestination
[MAX_PATH
] = {0};
1055 WCHAR sourcestem
[MAX_PATH
] = {0}; /* Stem of source */
1056 WCHAR sourcespec
[MAX_PATH
] = {0}; /* Filespec of source */
1057 WCHAR destinationstem
[MAX_PATH
] = {0}; /* Stem of destination */
1058 WCHAR destinationspec
[MAX_PATH
] = {0}; /* Filespec of destination */
1059 WCHAR copyCmd
[MAXSTRING
]; /* COPYCMD env var */
1060 DWORD flags
= 0; /* Option flags */
1062 /* Preinitialize flags based on COPYCMD */
1063 if (GetEnvironmentVariableW(L
"COPYCMD", copyCmd
, MAXSTRING
)) {
1064 if (wcsstr(copyCmd
, L
"/Y") != NULL
|| wcsstr(copyCmd
, L
"/y") != NULL
)
1065 flags
|= OPT_NOPROMPT
;
1068 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1069 wine, but on windows these can be normal files. At least one installer
1070 uses files such as .packlist and (validly) expects them to be copied.
1071 Under wine, if we do not copy hidden files by default then they get
1073 flags
|= OPT_COPYHIDSYS
;
1076 * Parse the command line
1078 if ((rc
= XCOPY_ParseCommandLine(suppliedsource
, supplieddestination
,
1082 /* Trace out the supplied information */
1083 WINE_TRACE("Supplied parameters:\n");
1084 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource
));
1085 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination
));
1087 /* Extract required information from source specification */
1088 rc
= XCOPY_ProcessSourceParm(suppliedsource
, sourcestem
, sourcespec
, flags
);
1089 if (rc
!= RC_OK
) return rc
;
1091 /* Extract required information from destination specification */
1092 rc
= XCOPY_ProcessDestParm(supplieddestination
, destinationstem
,
1093 destinationspec
, sourcespec
, flags
);
1094 if (rc
!= RC_OK
) return rc
;
1096 /* Trace out the resulting information */
1097 WINE_TRACE("Resolved parameters:\n");
1098 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem
));
1099 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec
));
1100 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem
));
1101 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec
));
1103 /* Pause if necessary */
1104 if (flags
& OPT_PAUSE
) {
1108 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE
));
1109 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), pausestr
, sizeof(pausestr
),
1113 /* Now do the hard work... */
1114 rc
= XCOPY_DoCopy(sourcestem
, sourcespec
,
1115 destinationstem
, destinationspec
,
1118 /* Clear up exclude list allocated memory */
1119 while (excludeList
) {
1120 EXCLUDELIST
*pos
= excludeList
;
1121 excludeList
= excludeList
-> next
;
1122 HeapFree(GetProcessHeap(), 0, pos
->name
);
1123 HeapFree(GetProcessHeap(), 0, pos
);
1126 /* Finished - print trailer and exit */
1127 if (flags
& OPT_SIMULATE
) {
1128 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY
), filesCopied
);
1129 } else if (!(flags
& OPT_NOCOPY
)) {
1130 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY
), filesCopied
);