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
);
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
);
275 buffer
[length
-1] = 0x00;
277 /* If more than CRLF */
279 thisEntry
= HeapAlloc(GetProcessHeap(), 0, sizeof(EXCLUDELIST
));
280 thisEntry
->next
= excludeList
;
281 excludeList
= thisEntry
;
282 thisEntry
->name
= HeapAlloc(GetProcessHeap(), 0,
283 (length
* sizeof(WCHAR
))+1);
284 lstrcpyW(thisEntry
->name
, buffer
);
285 CharUpperBuffW(thisEntry
->name
, length
);
286 WINE_TRACE("Read line : '%s'\n", wine_dbgstr_w(thisEntry
->name
));
290 /* See if EOF or error occurred */
292 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL
), filename
);
293 *endOfName
= endChar
;
297 /* Revert the input string to original form, and cleanup + return */
298 *endOfName
= endChar
;
303 /* =========================================================================
304 * Process the /EXCLUDE: file list, building up a list of substrings to
306 * Returns TRUE on any failure
307 * ========================================================================= */
308 static BOOL
XCOPY_ProcessExcludeList(WCHAR
* parms
) {
310 WCHAR
*filenameStart
= parms
;
312 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms
));
315 while (*parms
&& *parms
!= ' ' && *parms
!= '/') {
317 /* If found '+' then process the file found so far */
319 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
322 filenameStart
= parms
+1;
327 if (filenameStart
!= parms
) {
328 if (XCOPY_ProcessExcludeFile(filenameStart
, parms
)) {
336 /* =========================================================================
337 XCOPY_DoCopy - Recursive function to copy files based on input parms
340 This works by using FindFirstFile supplying the source stem and spec.
341 If results are found, any non-directory ones are processed
342 Then, if /S or /E is supplied, another search is made just for
343 directories, and this function is called again for that directory
345 ========================================================================= */
346 static int XCOPY_DoCopy(WCHAR
*srcstem
, WCHAR
*srcspec
,
347 WCHAR
*deststem
, WCHAR
*destspec
,
350 WIN32_FIND_DATAW
*finddata
;
353 WCHAR
*inputpath
, *outputpath
;
354 BOOL copiedFile
= FALSE
;
355 DWORD destAttribs
, srcAttribs
;
359 /* Allocate some working memory on heap to minimize footprint */
360 finddata
= HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW
));
361 inputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
362 outputpath
= HeapAlloc(GetProcessHeap(), 0, MAX_PATH
* sizeof(WCHAR
));
364 /* Build the search info into a single parm */
365 lstrcpyW(inputpath
, srcstem
);
366 lstrcatW(inputpath
, srcspec
);
368 /* Search 1 - Look for matching files */
369 h
= FindFirstFileW(inputpath
, finddata
);
370 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
374 /* Ignore . and .. */
375 if (lstrcmpW(finddata
->cFileName
, wchr_dot
)==0 ||
376 lstrcmpW(finddata
->cFileName
, wchr_dotdot
)==0 ||
377 finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) {
379 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata
->cFileName
));
382 /* Get the filename information */
383 lstrcpyW(copyFrom
, srcstem
);
384 if (flags
& OPT_SHORTNAME
) {
385 lstrcatW(copyFrom
, finddata
->cAlternateFileName
);
387 lstrcatW(copyFrom
, finddata
->cFileName
);
390 lstrcpyW(copyTo
, deststem
);
391 if (*destspec
== 0x00) {
392 if (flags
& OPT_SHORTNAME
) {
393 lstrcatW(copyTo
, finddata
->cAlternateFileName
);
395 lstrcatW(copyTo
, finddata
->cFileName
);
398 lstrcatW(copyTo
, destspec
);
402 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom
),
403 wine_dbgstr_w(copyTo
));
404 if (!copiedFile
&& !(flags
& OPT_SIMULATE
)) XCOPY_CreateDirectory(deststem
);
406 /* See if allowed to copy it */
407 srcAttribs
= GetFileAttributesW(copyFrom
);
408 WINE_TRACE("Source attribs: %d\n", srcAttribs
);
410 if ((srcAttribs
& FILE_ATTRIBUTE_HIDDEN
) ||
411 (srcAttribs
& FILE_ATTRIBUTE_SYSTEM
)) {
413 if (!(flags
& OPT_COPYHIDSYS
)) {
418 if (!(srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
419 (flags
& OPT_ARCHIVEONLY
)) {
423 /* See if file exists */
424 destAttribs
= GetFileAttributesW(copyTo
);
425 WINE_TRACE("Dest attribs: %d\n", srcAttribs
);
427 /* Check date ranges if a destination file already exists */
428 if (!skipFile
&& (flags
& OPT_DATERANGE
) &&
429 (CompareFileTime(&finddata
->ftLastWriteTime
, &dateRange
) < 0)) {
430 WINE_TRACE("Skipping file as modified date too old\n");
434 /* If just /D supplied, only overwrite if src newer than dest */
435 if (!skipFile
&& (flags
& OPT_DATENEWER
) &&
436 (destAttribs
!= INVALID_FILE_ATTRIBUTES
)) {
437 HANDLE h
= CreateFileW(copyTo
, GENERIC_READ
, FILE_SHARE_READ
,
438 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
,
440 if (h
!= INVALID_HANDLE_VALUE
) {
442 GetFileTime(h
, NULL
, NULL
, &writeTime
);
444 if (CompareFileTime(&finddata
->ftLastWriteTime
, &writeTime
) <= 0) {
445 WINE_TRACE("Skipping file as dest newer or same date\n");
452 /* See if exclude list provided. Note since filenames are case
453 insensitive, need to uppercase the filename before doing
455 if (!skipFile
&& (flags
& OPT_EXCLUDELIST
)) {
456 EXCLUDELIST
*pos
= excludeList
;
457 WCHAR copyFromUpper
[MAX_PATH
];
459 /* Uppercase source filename */
460 lstrcpyW(copyFromUpper
, copyFrom
);
461 CharUpperBuffW(copyFromUpper
, lstrlenW(copyFromUpper
));
463 /* Loop through testing each exclude line */
465 if (wcsstr(copyFromUpper
, pos
->name
) != NULL
) {
466 WINE_TRACE("Skipping file as matches exclude '%s'\n",
467 wine_dbgstr_w(pos
->name
));
476 /* Prompt each file if necessary */
477 if (!skipFile
&& (flags
& OPT_SRCPROMPT
)) {
480 BOOL answered
= FALSE
;
484 /* Read the Y and N characters from the resource file */
485 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
486 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
489 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT
), copyFrom
);
490 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
494 if (toupper(answer
[0]) == noChar
[0])
496 else if (toupper(answer
[0]) != yesChar
[0])
502 destAttribs
!= INVALID_FILE_ATTRIBUTES
&& !(flags
& OPT_NOPROMPT
)) {
505 BOOL answered
= FALSE
;
510 /* Read the A,Y and N characters from the resource file */
511 wcscpy(yesChar
, XCOPY_LoadMessage(STRING_YES_CHAR
));
512 wcscpy(allChar
, XCOPY_LoadMessage(STRING_ALL_CHAR
));
513 wcscpy(noChar
, XCOPY_LoadMessage(STRING_NO_CHAR
));
516 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE
), copyTo
);
517 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
),
521 if (toupper(answer
[0]) == allChar
[0])
522 flags
|= OPT_NOPROMPT
;
523 else if (toupper(answer
[0]) == noChar
[0])
525 else if (toupper(answer
[0]) != yesChar
[0])
530 /* See if it has to exist! */
531 if (destAttribs
== INVALID_FILE_ATTRIBUTES
&& (flags
& OPT_MUSTEXIST
)) {
535 /* Output a status message */
537 if (flags
& OPT_QUIET
) {
539 } else if (flags
& OPT_FULL
) {
540 const WCHAR infostr
[] = {'%', '1', ' ', '-', '>', ' ',
543 XCOPY_wprintf(infostr
, copyFrom
, copyTo
);
545 const WCHAR infostr
[] = {'%', '1', '\n', 0};
546 XCOPY_wprintf(infostr
, copyFrom
);
549 /* If allowing overwriting of read only files, remove any
551 if ((destAttribs
& FILE_ATTRIBUTE_READONLY
) &&
552 (flags
& OPT_REPLACEREAD
)) {
553 SetFileAttributesW(copyTo
, destAttribs
& ~FILE_ATTRIBUTE_READONLY
);
557 if (flags
& OPT_SIMULATE
|| flags
& OPT_NOCOPY
) {
559 } else if (CopyFileW(copyFrom
, copyTo
, FALSE
) == 0) {
561 DWORD error
= GetLastError();
562 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL
),
563 copyFrom
, copyTo
, error
);
564 XCOPY_FailMessage(error
);
566 if (flags
& OPT_IGNOREERRORS
) {
574 /* If /M supplied, remove the archive bit after successful copy */
576 if ((srcAttribs
& FILE_ATTRIBUTE_ARCHIVE
) &&
577 (flags
& OPT_REMOVEARCH
)) {
578 SetFileAttributesW(copyFrom
, (srcAttribs
& ~FILE_ATTRIBUTE_ARCHIVE
));
586 findres
= FindNextFileW(h
, finddata
);
590 /* Search 2 - do subdirs */
591 if (flags
& OPT_RECURSIVE
) {
592 lstrcpyW(inputpath
, srcstem
);
593 lstrcatW(inputpath
, wchr_star
);
595 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath
));
597 h
= FindFirstFileW(inputpath
, finddata
);
598 while (h
!= INVALID_HANDLE_VALUE
&& findres
) {
600 /* Only looking for dirs */
601 if ((finddata
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) &&
602 (lstrcmpW(finddata
->cFileName
, wchr_dot
) != 0) &&
603 (lstrcmpW(finddata
->cFileName
, wchr_dotdot
) != 0)) {
605 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata
->cFileName
));
607 /* Make up recursive information */
608 lstrcpyW(inputpath
, srcstem
);
609 lstrcatW(inputpath
, finddata
->cFileName
);
610 lstrcatW(inputpath
, wchr_slash
);
612 lstrcpyW(outputpath
, deststem
);
613 if (*destspec
== 0x00) {
614 lstrcatW(outputpath
, finddata
->cFileName
);
616 /* If /E is supplied, create the directory now */
617 if ((flags
& OPT_EMPTYDIR
) &&
618 !(flags
& OPT_SIMULATE
))
619 XCOPY_CreateDirectory(outputpath
);
621 lstrcatW(outputpath
, wchr_slash
);
624 XCOPY_DoCopy(inputpath
, srcspec
, outputpath
, destspec
, flags
);
628 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
)
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 /* Parse the arg : Month */
780 st
.wMonth
= _wtol(pos
);
781 while (*pos
&& isdigit(*pos
)) pos
++;
782 if (*pos
++ != '-') isError
= TRUE
;
784 /* Parse the arg : Day */
786 st
.wDay
= _wtol(pos
);
787 while (*pos
&& isdigit(*pos
)) pos
++;
788 if (*pos
++ != '-') isError
= TRUE
;
791 /* Parse the arg : Year */
793 st
.wYear
= _wtol(pos
);
794 while (*pos
&& isdigit(*pos
)) pos
++;
795 if (st
.wYear
< 100) st
.wYear
+=2000;
798 if (!isError
&& SystemTimeToFileTime(&st
, &dateRange
)) {
800 WCHAR datestring
[32], timestring
[32];
802 flags
|= OPT_DATERANGE
;
805 FileTimeToSystemTime (&dateRange
, &st
);
806 GetDateFormatW(0, DATE_SHORTDATE
, &st
, NULL
, datestring
,
807 sizeof(datestring
)/sizeof(WCHAR
));
808 GetTimeFormatW(0, TIME_NOSECONDS
, &st
,
809 NULL
, timestring
, sizeof(timestring
)/sizeof(WCHAR
));
811 WINE_TRACE("Date being used is: %s %s\n",
812 wine_dbgstr_w(datestring
), wine_dbgstr_w(timestring
));
814 XCOPY_FailMessage(ERROR_INVALID_PARAMETER
);
818 flags
|= OPT_DATENEWER
;
822 case '-': if (toupper(word
[2])=='Y')
823 flags
&= ~OPT_NOPROMPT
; break;
824 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP
));
828 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word
));
829 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM
), word
);
836 /* Default the destination if not supplied */
837 if (supplieddestination
[0] == 0x00)
838 lstrcpyW(supplieddestination
, wchr_dot
);
849 /* =========================================================================
850 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
851 converts it into a stem and a filespec
852 ========================================================================= */
853 static int XCOPY_ProcessSourceParm(WCHAR
*suppliedsource
, WCHAR
*stem
,
854 WCHAR
*spec
, DWORD flags
)
856 WCHAR actualsource
[MAX_PATH
];
862 * Validate the source, expanding to full path ensuring it exists
864 if (GetFullPathNameW(suppliedsource
, MAX_PATH
, actualsource
, NULL
) == 0) {
865 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
869 /* If full names required, convert to using the full path */
870 if (flags
& OPT_FULL
) {
871 lstrcpyW(suppliedsource
, actualsource
);
875 * Work out the stem of the source
878 /* If a directory is supplied, use that as-is (either fully or
880 If a filename is supplied + a directory or drive path, use that
883 If no directory or path specified, add eg. C:
884 stem is Drive/Directory is bit up to last \ (or first :)
885 spec is bit after that */
887 starPos
= wcschr(suppliedsource
, '*');
888 questPos
= wcschr(suppliedsource
, '?');
889 if (starPos
|| questPos
) {
890 attribs
= 0x00; /* Ensures skips invalid or directory check below */
892 attribs
= GetFileAttributesW(actualsource
);
895 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
896 XCOPY_FailMessage(GetLastError());
900 stem should be exactly as supplied plus a '\', unless it was
901 eg. C: in which case no slash required */
902 } else if (attribs
& FILE_ATTRIBUTE_DIRECTORY
) {
905 WINE_TRACE("Directory supplied\n");
906 lstrcpyW(stem
, suppliedsource
);
907 lastChar
= stem
[lstrlenW(stem
)-1];
908 if (lastChar
!= '\\' && lastChar
!= ':') {
909 lstrcatW(stem
, wchr_slash
);
911 lstrcpyW(spec
, wchr_star
);
913 /* File or wildcard search:
915 Up to and including last slash if directory path supplied
916 If c:filename supplied, just the c:
917 Otherwise stem should be the current drive letter + ':' */
921 WINE_TRACE("Filename supplied\n");
922 lastDir
= wcsrchr(suppliedsource
, '\\');
925 lstrcpyW(stem
, suppliedsource
);
926 stem
[(lastDir
-suppliedsource
) + 1] = 0x00;
927 lstrcpyW(spec
, (lastDir
+1));
928 } else if (suppliedsource
[1] == ':') {
929 lstrcpyW(stem
, suppliedsource
);
931 lstrcpyW(spec
, suppliedsource
+2);
933 WCHAR curdir
[MAXSTRING
];
934 GetCurrentDirectoryW(sizeof(curdir
)/sizeof(WCHAR
), curdir
);
938 lstrcpyW(spec
, suppliedsource
);
945 /* =========================================================================
946 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
947 converts it into a stem
948 ========================================================================= */
949 static int XCOPY_ProcessDestParm(WCHAR
*supplieddestination
, WCHAR
*stem
, WCHAR
*spec
,
950 WCHAR
*srcspec
, DWORD flags
)
952 WCHAR actualdestination
[MAX_PATH
];
957 * Validate the source, expanding to full path ensuring it exists
959 if (GetFullPathNameW(supplieddestination
, MAX_PATH
, actualdestination
, NULL
) == 0) {
960 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
964 /* Destination is either a directory or a file */
965 attribs
= GetFileAttributesW(actualdestination
);
967 if (attribs
== INVALID_FILE_ATTRIBUTES
) {
969 /* If /I supplied and wildcard copy, assume directory */
970 /* Also if destination ends with backslash */
971 if ((flags
& OPT_ASSUMEDIR
&&
972 (wcschr(srcspec
, '?') || wcschr(srcspec
, '*'))) ||
973 (supplieddestination
[lstrlenW(supplieddestination
)-1] == '\\')) {
979 char answer
[10] = "";
983 /* Read the F and D characters from the resource file */
984 wcscpy(fileChar
, XCOPY_LoadMessage(STRING_FILE_CHAR
));
985 wcscpy(dirChar
, XCOPY_LoadMessage(STRING_DIR_CHAR
));
987 while (answer
[0] != fileChar
[0] && answer
[0] != dirChar
[0]) {
988 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR
), supplieddestination
);
990 ReadFile(GetStdHandle(STD_INPUT_HANDLE
), answer
, sizeof(answer
), &count
, NULL
);
991 WINE_TRACE("User answer %c\n", answer
[0]);
993 answer
[0] = toupper(answer
[0]);
996 if (answer
[0] == dirChar
[0]) {
1003 isDir
= (attribs
& FILE_ATTRIBUTE_DIRECTORY
);
1007 lstrcpyW(stem
, actualdestination
);
1010 /* Ensure ends with a '\' */
1011 if (stem
[lstrlenW(stem
)-1] != '\\') {
1012 lstrcatW(stem
, wchr_slash
);
1016 WCHAR drive
[MAX_PATH
];
1017 WCHAR dir
[MAX_PATH
];
1018 WCHAR fname
[MAX_PATH
];
1019 WCHAR ext
[MAX_PATH
];
1020 _wsplitpath(actualdestination
, drive
, dir
, fname
, ext
);
1021 lstrcpyW(stem
, drive
);
1022 lstrcatW(stem
, dir
);
1023 lstrcpyW(spec
, fname
);
1024 lstrcatW(spec
, ext
);
1030 /* =========================================================================
1031 main - Main entrypoint for the xcopy command
1033 Processes the args, and drives the actual copying
1034 ========================================================================= */
1035 int wmain (int argc
, WCHAR
*argvW
[])
1038 WCHAR suppliedsource
[MAX_PATH
] = {0}; /* As supplied on the cmd line */
1039 WCHAR supplieddestination
[MAX_PATH
] = {0};
1040 WCHAR sourcestem
[MAX_PATH
] = {0}; /* Stem of source */
1041 WCHAR sourcespec
[MAX_PATH
] = {0}; /* Filespec of source */
1042 WCHAR destinationstem
[MAX_PATH
] = {0}; /* Stem of destination */
1043 WCHAR destinationspec
[MAX_PATH
] = {0}; /* Filespec of destination */
1044 WCHAR copyCmd
[MAXSTRING
]; /* COPYCMD env var */
1045 DWORD flags
= 0; /* Option flags */
1046 const WCHAR PROMPTSTR1
[] = {'/', 'Y', 0};
1047 const WCHAR PROMPTSTR2
[] = {'/', 'y', 0};
1048 const WCHAR COPYCMD
[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1050 /* Preinitialize flags based on COPYCMD */
1051 if (GetEnvironmentVariableW(COPYCMD
, copyCmd
, MAXSTRING
)) {
1052 if (wcsstr(copyCmd
, PROMPTSTR1
) != NULL
||
1053 wcsstr(copyCmd
, PROMPTSTR2
) != NULL
) {
1054 flags
|= OPT_NOPROMPT
;
1058 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1059 wine, but on windows these can be normal files. At least one installer
1060 uses files such as .packlist and (validly) expects them to be copied.
1061 Under wine, if we do not copy hidden files by default then they get
1063 flags
|= OPT_COPYHIDSYS
;
1066 * Parse the command line
1068 if ((rc
= XCOPY_ParseCommandLine(suppliedsource
, supplieddestination
,
1069 &flags
)) != RC_OK
) {
1076 /* Trace out the supplied information */
1077 WINE_TRACE("Supplied parameters:\n");
1078 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource
));
1079 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination
));
1081 /* Extract required information from source specification */
1082 rc
= XCOPY_ProcessSourceParm(suppliedsource
, sourcestem
, sourcespec
, flags
);
1083 if (rc
!= RC_OK
) return rc
;
1085 /* Extract required information from destination specification */
1086 rc
= XCOPY_ProcessDestParm(supplieddestination
, destinationstem
,
1087 destinationspec
, sourcespec
, flags
);
1088 if (rc
!= RC_OK
) return rc
;
1090 /* Trace out the resulting information */
1091 WINE_TRACE("Resolved parameters:\n");
1092 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem
));
1093 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec
));
1094 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem
));
1095 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec
));
1097 /* Pause if necessary */
1098 if (flags
& OPT_PAUSE
) {
1102 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE
));
1103 ReadFile (GetStdHandle(STD_INPUT_HANDLE
), pausestr
, sizeof(pausestr
),
1107 /* Now do the hard work... */
1108 rc
= XCOPY_DoCopy(sourcestem
, sourcespec
,
1109 destinationstem
, destinationspec
,
1112 /* Clear up exclude list allocated memory */
1113 while (excludeList
) {
1114 EXCLUDELIST
*pos
= excludeList
;
1115 excludeList
= excludeList
-> next
;
1116 HeapFree(GetProcessHeap(), 0, pos
->name
);
1117 HeapFree(GetProcessHeap(), 0, pos
);
1120 /* Finished - print trailer and exit */
1121 if (flags
& OPT_SIMULATE
) {
1122 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY
), filesCopied
);
1123 } else if (!(flags
& OPT_NOCOPY
)) {
1124 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY
), filesCopied
);
1126 if (rc
== RC_OK
&& filesCopied
== 0) rc
= RC_NOFILES
;