advapi32/tests: Allow ERROR_ACCESS_DENIED for newer Win10.
[wine.git] / programs / xcopy / xcopy.c
blobeb8c3f04ef03288fe2d21978029d1a7d1fd9db52
1 /*
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
22 * FIXME:
23 * This should now support all options listed in the xcopy help from
24 * windows XP except:
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
29 * /V - Verifies files
33 * Notes:
34 * Documented valid return codes are:
35 * 0 - OK
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
45 #include <stdio.h>
46 #include <stdlib.h>
47 #include <windows.h>
48 #include <wine/debug.h>
49 #include <wine/unicode.h>
50 #include "xcopy.h"
52 WINE_DEFAULT_DEBUG_CHANNEL(xcopy);
55 /* Typedefs */
56 typedef struct _EXCLUDELIST
58 struct _EXCLUDELIST *next;
59 WCHAR *name;
60 } EXCLUDELIST;
63 /* Global variables */
64 static ULONG filesCopied = 0; /* Number of files copied */
65 static EXCLUDELIST *excludeList = NULL; /* Excluded strings list */
66 static FILETIME dateRange; /* Date range to copy after*/
67 static const WCHAR wchr_slash[] = {'\\', 0};
68 static const WCHAR wchr_star[] = {'*', 0};
69 static const WCHAR wchr_dot[] = {'.', 0};
70 static const WCHAR wchr_dotdot[] = {'.', '.', 0};
73 /* To minimize stack usage during recursion, some temporary variables
74 made global */
75 static WCHAR copyFrom[MAX_PATH];
76 static WCHAR copyTo[MAX_PATH];
79 /* =========================================================================
80 * Load a string from the resource file, handling any error
81 * Returns string retrieved from resource file
82 * ========================================================================= */
83 static WCHAR *XCOPY_LoadMessage(UINT id) {
84 static WCHAR msg[MAXSTRING];
85 const WCHAR failedMsg[] = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
87 if (!LoadStringW(GetModuleHandleW(NULL), id, msg, ARRAY_SIZE(msg))) {
88 WINE_FIXME("LoadString failed with %d\n", GetLastError());
89 lstrcpyW(msg, failedMsg);
91 return msg;
94 /* =========================================================================
95 * Output a formatted unicode string. Ideally this will go to the console
96 * and hence required WriteConsoleW to output it, however if file i/o is
97 * redirected, it needs to be WriteFile'd using OEM (not ANSI) format
98 * ========================================================================= */
99 static int WINAPIV XCOPY_wprintf(const WCHAR *format, ...) {
101 static WCHAR *output_bufW = NULL;
102 static char *output_bufA = NULL;
103 static BOOL toConsole = TRUE;
104 static BOOL traceOutput = FALSE;
105 #define MAX_WRITECONSOLE_SIZE 65535
107 __ms_va_list parms;
108 DWORD nOut;
109 int len;
110 DWORD res = 0;
113 * Allocate buffer to use when writing to console
114 * Note: Not freed - memory will be allocated once and released when
115 * xcopy ends
118 if (!output_bufW) output_bufW = HeapAlloc(GetProcessHeap(), 0,
119 MAX_WRITECONSOLE_SIZE*sizeof(WCHAR));
120 if (!output_bufW) {
121 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
122 return 0;
125 __ms_va_start(parms, format);
126 SetLastError(NO_ERROR);
127 len = FormatMessageW(FORMAT_MESSAGE_FROM_STRING, format, 0, 0, output_bufW,
128 MAX_WRITECONSOLE_SIZE/sizeof(*output_bufW), &parms);
129 __ms_va_end(parms);
130 if (len == 0 && GetLastError() != NO_ERROR) {
131 WINE_FIXME("Could not format string: le=%u, fmt=%s\n", GetLastError(), wine_dbgstr_w(format));
132 return 0;
135 /* Try to write as unicode whenever we think it's a console */
136 if (toConsole) {
137 res = WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),
138 output_bufW, len, &nOut, NULL);
141 /* If writing to console has failed (ever) we assume it's file
142 i/o so convert to OEM codepage and output */
143 if (!res) {
144 BOOL usedDefaultChar = FALSE;
145 DWORD convertedChars;
147 toConsole = FALSE;
150 * Allocate buffer to use when writing to file. Not freed, as above
152 if (!output_bufA) output_bufA = HeapAlloc(GetProcessHeap(), 0,
153 MAX_WRITECONSOLE_SIZE);
154 if (!output_bufA) {
155 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
156 return 0;
159 /* Convert to OEM, then output */
160 convertedChars = WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW,
161 len, output_bufA, MAX_WRITECONSOLE_SIZE,
162 "?", &usedDefaultChar);
163 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), output_bufA, convertedChars,
164 &nOut, FALSE);
167 /* Trace whether screen or console */
168 if (!traceOutput) {
169 WINE_TRACE("Writing to console? (%d)\n", toConsole);
170 traceOutput = TRUE;
172 return nOut;
175 /* =========================================================================
176 * Load a string for a system error and writes it to the screen
177 * Returns string retrieved from resource file
178 * ========================================================================= */
179 static void XCOPY_FailMessage(DWORD err) {
180 LPWSTR lpMsgBuf;
181 int status;
183 status = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
184 FORMAT_MESSAGE_FROM_SYSTEM,
185 NULL, err, 0,
186 (LPWSTR) &lpMsgBuf, 0, NULL);
187 if (!status) {
188 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
189 err, GetLastError());
190 } else {
191 const WCHAR infostr[] = {'%', '1', '\n', 0};
192 XCOPY_wprintf(infostr, lpMsgBuf);
193 LocalFree ((HLOCAL)lpMsgBuf);
198 /* =========================================================================
199 * Routine copied from cmd.exe md command -
200 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
201 * dir2 if they do not already exist.
202 * ========================================================================= */
203 static BOOL XCOPY_CreateDirectory(const WCHAR* path)
205 int len;
206 WCHAR *new_path;
207 BOOL ret = TRUE;
209 new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
210 lstrcpyW(new_path,path);
212 while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
213 new_path[len - 1] = 0;
215 while (!CreateDirectoryW(new_path,NULL))
217 WCHAR *slash;
218 DWORD last_error = GetLastError();
219 if (last_error == ERROR_ALREADY_EXISTS)
220 break;
222 if (last_error != ERROR_PATH_NOT_FOUND)
224 ret = FALSE;
225 break;
228 if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
230 ret = FALSE;
231 break;
234 len = slash - new_path;
235 new_path[len] = 0;
236 if (!XCOPY_CreateDirectory(new_path))
238 ret = FALSE;
239 break;
241 new_path[len] = '\\';
243 HeapFree(GetProcessHeap(),0,new_path);
244 return ret;
247 /* =========================================================================
248 * Process a single file from the /EXCLUDE: file list, building up a list
249 * of substrings to avoid copying
250 * Returns TRUE on any failure
251 * ========================================================================= */
252 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName) {
254 WCHAR endChar = *endOfName;
255 WCHAR buffer[MAXSTRING];
256 FILE *inFile = NULL;
257 const WCHAR readTextMode[] = {'r', 't', 0};
259 /* Null terminate the filename (temporarily updates the filename hence
260 parms not const) */
261 *endOfName = 0x00;
263 /* Open the file */
264 inFile = _wfopen(filename, readTextMode);
265 if (inFile == NULL) {
266 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
267 *endOfName = endChar;
268 return TRUE;
271 /* Process line by line */
272 while (fgetws(buffer, ARRAY_SIZE(buffer), inFile) != NULL) {
273 EXCLUDELIST *thisEntry;
274 int length = lstrlenW(buffer);
276 /* If more than CRLF */
277 if (length > 1) {
278 buffer[length-1] = 0; /* strip 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 */
291 if (!feof(inFile)) {
292 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
293 *endOfName = endChar;
294 fclose(inFile);
295 return TRUE;
298 /* Revert the input string to original form, and cleanup + return */
299 *endOfName = endChar;
300 fclose(inFile);
301 return FALSE;
304 /* =========================================================================
305 * Process the /EXCLUDE: file list, building up a list of substrings to
306 * avoid copying
307 * Returns TRUE on any failure
308 * ========================================================================= */
309 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {
311 WCHAR *filenameStart = parms;
313 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
314 excludeList = NULL;
316 while (*parms && *parms != ' ' && *parms != '/') {
318 /* If found '+' then process the file found so far */
319 if (*parms == '+') {
320 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
321 return TRUE;
323 filenameStart = parms+1;
325 parms++;
328 if (filenameStart != parms) {
329 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
330 return TRUE;
334 return FALSE;
337 /* =========================================================================
338 XCOPY_DoCopy - Recursive function to copy files based on input parms
339 of a stem and a spec
341 This works by using FindFirstFile supplying the source stem and spec.
342 If results are found, any non-directory ones are processed
343 Then, if /S or /E is supplied, another search is made just for
344 directories, and this function is called again for that directory
346 ========================================================================= */
347 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
348 WCHAR *deststem, WCHAR *destspec,
349 DWORD flags)
351 WIN32_FIND_DATAW *finddata;
352 HANDLE h;
353 BOOL findres = TRUE;
354 WCHAR *inputpath, *outputpath;
355 BOOL copiedFile = FALSE;
356 DWORD destAttribs, srcAttribs;
357 BOOL skipFile;
358 int ret = 0;
360 /* Allocate some working memory on heap to minimize footprint */
361 finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW));
362 inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
363 outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
365 /* Build the search info into a single parm */
366 lstrcpyW(inputpath, srcstem);
367 lstrcatW(inputpath, srcspec);
369 /* Search 1 - Look for matching files */
370 h = FindFirstFileW(inputpath, finddata);
371 while (h != INVALID_HANDLE_VALUE && findres) {
373 skipFile = FALSE;
375 /* Ignore . and .. */
376 if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
377 lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
378 finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
380 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
381 } else {
383 /* Get the filename information */
384 lstrcpyW(copyFrom, srcstem);
385 if (flags & OPT_SHORTNAME) {
386 lstrcatW(copyFrom, finddata->cAlternateFileName);
387 } else {
388 lstrcatW(copyFrom, finddata->cFileName);
391 lstrcpyW(copyTo, deststem);
392 if (*destspec == 0x00) {
393 if (flags & OPT_SHORTNAME) {
394 lstrcatW(copyTo, finddata->cAlternateFileName);
395 } else {
396 lstrcatW(copyTo, finddata->cFileName);
398 } else {
399 lstrcatW(copyTo, destspec);
402 /* Do the copy */
403 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
404 wine_dbgstr_w(copyTo));
405 if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
407 /* See if allowed to copy it */
408 srcAttribs = GetFileAttributesW(copyFrom);
409 WINE_TRACE("Source attribs: %d\n", srcAttribs);
411 if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
412 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
414 if (!(flags & OPT_COPYHIDSYS)) {
415 skipFile = TRUE;
419 if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
420 (flags & OPT_ARCHIVEONLY)) {
421 skipFile = TRUE;
424 /* See if file exists */
425 destAttribs = GetFileAttributesW(copyTo);
426 WINE_TRACE("Dest attribs: %d\n", srcAttribs);
428 /* Check date ranges if a destination file already exists */
429 if (!skipFile && (flags & OPT_DATERANGE) &&
430 (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
431 WINE_TRACE("Skipping file as modified date too old\n");
432 skipFile = TRUE;
435 /* If just /D supplied, only overwrite if src newer than dest */
436 if (!skipFile && (flags & OPT_DATENEWER) &&
437 (destAttribs != INVALID_FILE_ATTRIBUTES)) {
438 HANDLE h = CreateFileW(copyTo, GENERIC_READ, FILE_SHARE_READ,
439 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
440 NULL);
441 if (h != INVALID_HANDLE_VALUE) {
442 FILETIME writeTime;
443 GetFileTime(h, NULL, NULL, &writeTime);
445 if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
446 WINE_TRACE("Skipping file as dest newer or same date\n");
447 skipFile = TRUE;
449 CloseHandle(h);
453 /* See if exclude list provided. Note since filenames are case
454 insensitive, need to uppercase the filename before doing
455 strstr */
456 if (!skipFile && (flags & OPT_EXCLUDELIST)) {
457 EXCLUDELIST *pos = excludeList;
458 WCHAR copyFromUpper[MAX_PATH];
460 /* Uppercase source filename */
461 lstrcpyW(copyFromUpper, copyFrom);
462 CharUpperBuffW(copyFromUpper, lstrlenW(copyFromUpper));
464 /* Loop through testing each exclude line */
465 while (pos) {
466 if (wcsstr(copyFromUpper, pos->name) != NULL) {
467 WINE_TRACE("Skipping file as matches exclude '%s'\n",
468 wine_dbgstr_w(pos->name));
469 skipFile = TRUE;
470 pos = NULL;
471 } else {
472 pos = pos->next;
477 /* Prompt each file if necessary */
478 if (!skipFile && (flags & OPT_SRCPROMPT)) {
479 DWORD count;
480 char answer[10];
481 BOOL answered = FALSE;
482 WCHAR yesChar[2];
483 WCHAR noChar[2];
485 /* Read the Y and N characters from the resource file */
486 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
487 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
489 while (!answered) {
490 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
491 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
492 &count, NULL);
494 answered = TRUE;
495 if (toupper(answer[0]) == noChar[0])
496 skipFile = TRUE;
497 else if (toupper(answer[0]) != yesChar[0])
498 answered = FALSE;
502 if (!skipFile &&
503 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
504 DWORD count;
505 char answer[10];
506 BOOL answered = FALSE;
507 WCHAR yesChar[2];
508 WCHAR allChar[2];
509 WCHAR noChar[2];
511 /* Read the A,Y and N characters from the resource file */
512 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
513 wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
514 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
516 while (!answered) {
517 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
518 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
519 &count, NULL);
521 answered = TRUE;
522 if (toupper(answer[0]) == allChar[0])
523 flags |= OPT_NOPROMPT;
524 else if (toupper(answer[0]) == noChar[0])
525 skipFile = TRUE;
526 else if (toupper(answer[0]) != yesChar[0])
527 answered = FALSE;
531 /* See if it has to exist! */
532 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
533 skipFile = TRUE;
536 /* Output a status message */
537 if (!skipFile) {
538 if (flags & OPT_QUIET) {
539 /* Skip message */
540 } else if (flags & OPT_FULL) {
541 const WCHAR infostr[] = {'%', '1', ' ', '-', '>', ' ',
542 '%', '2', '\n', 0};
544 XCOPY_wprintf(infostr, copyFrom, copyTo);
545 } else {
546 const WCHAR infostr[] = {'%', '1', '\n', 0};
547 XCOPY_wprintf(infostr, copyFrom);
550 /* If allowing overwriting of read only files, remove any
551 write protection */
552 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
553 (flags & OPT_REPLACEREAD)) {
554 SetFileAttributesW(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
557 copiedFile = TRUE;
558 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
559 /* Skip copy */
560 } else if (CopyFileW(copyFrom, copyTo, FALSE) == 0) {
562 DWORD error = GetLastError();
563 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
564 copyFrom, copyTo, error);
565 XCOPY_FailMessage(error);
567 if (flags & OPT_IGNOREERRORS) {
568 skipFile = TRUE;
569 } else {
570 ret = RC_WRITEERROR;
571 goto cleanup;
573 } else {
575 if (!skipFile) {
576 /* If keeping attributes, update the destination attributes
577 otherwise remove the read only attribute */
578 if (flags & OPT_KEEPATTRS) {
579 SetFileAttributesW(copyTo, srcAttribs | FILE_ATTRIBUTE_ARCHIVE);
580 } else {
581 SetFileAttributesW(copyTo,
582 (GetFileAttributesW(copyTo) & ~FILE_ATTRIBUTE_READONLY));
585 /* If /M supplied, remove the archive bit after successful copy */
586 if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
587 (flags & OPT_REMOVEARCH)) {
588 SetFileAttributesW(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
590 filesCopied++;
596 /* Find next file */
597 findres = FindNextFileW(h, finddata);
599 FindClose(h);
601 /* Search 2 - do subdirs */
602 if (flags & OPT_RECURSIVE) {
604 /* If /E is supplied, create the directory now */
605 if ((flags & OPT_EMPTYDIR) &&
606 !(flags & OPT_SIMULATE)) {
607 XCOPY_CreateDirectory(deststem);
610 lstrcpyW(inputpath, srcstem);
611 lstrcatW(inputpath, wchr_star);
612 findres = TRUE;
613 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
615 h = FindFirstFileW(inputpath, finddata);
616 while (h != INVALID_HANDLE_VALUE && findres) {
618 /* Only looking for dirs */
619 if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
620 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
621 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
623 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
625 /* Make up recursive information */
626 lstrcpyW(inputpath, srcstem);
627 lstrcatW(inputpath, finddata->cFileName);
628 lstrcatW(inputpath, wchr_slash);
630 lstrcpyW(outputpath, deststem);
631 if (*destspec == 0x00) {
632 lstrcatW(outputpath, finddata->cFileName);
633 lstrcatW(outputpath, wchr_slash);
636 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
639 /* Find next one */
640 findres = FindNextFileW(h, finddata);
642 FindClose(h);
645 cleanup:
647 /* free up memory */
648 HeapFree(GetProcessHeap(), 0, finddata);
649 HeapFree(GetProcessHeap(), 0, inputpath);
650 HeapFree(GetProcessHeap(), 0, outputpath);
652 return ret;
656 /* =========================================================================
657 XCOPY_ParseCommandLine - Parses the command line
658 ========================================================================= */
659 static inline BOOL is_whitespace(WCHAR c)
661 return c == ' ' || c == '\t';
664 static WCHAR *skip_whitespace(WCHAR *p)
666 for (; *p && is_whitespace(*p); p++);
667 return p;
670 static inline BOOL is_digit(WCHAR c)
672 return c >= '0' && c <= '9';
675 /* Windows XCOPY uses a simplified command line parsing algorithm
676 that lacks the escaped-quote logic of build_argv(), because
677 literal double quotes are illegal in any of its arguments.
678 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
679 static int find_end_of_word(const WCHAR *word, WCHAR **end)
681 BOOL in_quotes = FALSE;
682 const WCHAR *ptr = word;
683 for (;;) {
684 for (; *ptr != '\0' && *ptr != '"' &&
685 (in_quotes || !is_whitespace(*ptr)); ptr++);
686 if (*ptr == '"') {
687 in_quotes = !in_quotes;
688 ptr++;
690 /* Odd number of double quotes is illegal for XCOPY */
691 if (in_quotes && *ptr == '\0')
692 return RC_INITERROR;
693 if (*ptr == '\0' || (!in_quotes && is_whitespace(*ptr)))
694 break;
696 *end = (WCHAR*)ptr;
697 return RC_OK;
700 /* Remove all double quotes from a word */
701 static void strip_quotes(WCHAR *word, WCHAR **end)
703 WCHAR *rp, *wp;
704 for (rp = word, wp = word; *rp != '\0'; rp++) {
705 if (*rp == '"')
706 continue;
707 if (wp < rp)
708 *wp = *rp;
709 wp++;
711 *wp = '\0';
712 *end = wp;
715 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource,
716 WCHAR *supplieddestination, DWORD *pflags)
718 const WCHAR EXCLUDE[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
719 DWORD flags = *pflags;
720 WCHAR *cmdline, *word, *end, *next;
721 int rc = RC_INITERROR;
723 cmdline = _wcsdup(GetCommandLineW());
724 if (cmdline == NULL)
725 return rc;
727 /* Skip first arg, which is the program name */
728 if ((rc = find_end_of_word(cmdline, &word)) != RC_OK)
729 goto out;
730 word = skip_whitespace(word);
732 while (*word)
734 WCHAR first;
735 if ((rc = find_end_of_word(word, &end)) != RC_OK)
736 goto out;
738 next = skip_whitespace(end);
739 first = word[0];
740 *end = '\0';
741 strip_quotes(word, &end);
742 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word));
744 /* First non-switch parameter is source, second is destination */
745 if (first != '/') {
746 if (suppliedsource[0] == 0x00) {
747 lstrcpyW(suppliedsource, word);
748 } else if (supplieddestination[0] == 0x00) {
749 lstrcpyW(supplieddestination, word);
750 } else {
751 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
752 goto out;
754 } else {
755 /* Process all the switch options
756 Note: Windows docs say /P prompts when dest is created
757 but tests show it is done for each src file
758 regardless of the destination */
759 int skip=0;
760 WCHAR *rest;
762 while (word[0]) {
763 rest = NULL;
765 switch (toupper(word[1])) {
766 case 'I': flags |= OPT_ASSUMEDIR; break;
767 case 'S': flags |= OPT_RECURSIVE; break;
768 case 'Q': flags |= OPT_QUIET; break;
769 case 'F': flags |= OPT_FULL; break;
770 case 'L': flags |= OPT_SIMULATE; break;
771 case 'W': flags |= OPT_PAUSE; break;
772 case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
773 case 'Y': flags |= OPT_NOPROMPT; break;
774 case 'N': flags |= OPT_SHORTNAME; break;
775 case 'U': flags |= OPT_MUSTEXIST; break;
776 case 'R': flags |= OPT_REPLACEREAD; break;
777 case 'K': flags |= OPT_KEEPATTRS; break;
778 case 'H': flags |= OPT_COPYHIDSYS; break;
779 case 'C': flags |= OPT_IGNOREERRORS; break;
780 case 'P': flags |= OPT_SRCPROMPT; break;
781 case 'A': flags |= OPT_ARCHIVEONLY; break;
782 case 'M': flags |= OPT_ARCHIVEONLY |
783 OPT_REMOVEARCH; break;
785 /* E can be /E or /EXCLUDE */
786 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT,
787 NORM_IGNORECASE | SORT_STRINGSORT,
788 &word[1], 8,
789 EXCLUDE, -1) == CSTR_EQUAL) {
790 if (XCOPY_ProcessExcludeList(&word[9])) {
791 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
792 goto out;
793 } else {
794 flags |= OPT_EXCLUDELIST;
796 /* Do not support concatenated switches onto exclude lists yet */
797 rest = end;
799 } else {
800 flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
802 break;
804 /* D can be /D or /D: */
805 case 'D': if (word[2]==':' && is_digit(word[3])) {
806 SYSTEMTIME st;
807 WCHAR *pos = &word[3];
808 BOOL isError = FALSE;
809 memset(&st, 0x00, sizeof(st));
811 /* Microsoft xcopy's usage message implies that the date
812 * format depends on the locale, but that is false.
813 * It is hardcoded to month-day-year.
815 st.wMonth = _wtol(pos);
816 while (*pos && is_digit(*pos)) pos++;
817 if (*pos++ != '-') isError = TRUE;
819 if (!isError) {
820 st.wDay = _wtol(pos);
821 while (*pos && is_digit(*pos)) pos++;
822 if (*pos++ != '-') isError = TRUE;
825 if (!isError) {
826 st.wYear = _wtol(pos);
827 while (*pos && is_digit(*pos)) pos++;
828 if (st.wYear < 100) st.wYear+=2000;
831 /* Handle switches straight after the supplied date */
832 rest = pos;
834 if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
835 SYSTEMTIME st;
836 WCHAR datestring[32], timestring[32];
838 flags |= OPT_DATERANGE;
840 /* Debug info: */
841 FileTimeToSystemTime (&dateRange, &st);
842 GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring,
843 ARRAY_SIZE(datestring));
844 GetTimeFormatW(0, TIME_NOSECONDS, &st,
845 NULL, timestring, ARRAY_SIZE(timestring));
847 WINE_TRACE("Date being used is: %s %s\n",
848 wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
849 } else {
850 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
851 goto out;
853 } else {
854 flags |= OPT_DATENEWER;
856 break;
858 case '-': if (toupper(word[2])=='Y') {
859 flags &= ~OPT_NOPROMPT;
860 rest = &word[3]; /* Skip over 3 characters */
862 break;
863 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
864 rc = RC_HELP;
865 goto out;
866 case 'V':
867 WINE_FIXME("ignoring /V\n");
868 break;
869 default:
870 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word));
871 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), word);
872 goto out;
875 /* Unless overridden above, skip over the '/' and the first character */
876 if (rest == NULL) rest = &word[2];
878 /* By now, rest should point either to the null after the
879 switch, or the beginning of the next switch if there
880 was no whitespace between them */
881 if (!skip && *rest && *rest != '/') {
882 WINE_FIXME("Unexpected characters found and ignored '%s'\n", wine_dbgstr_w(rest));
883 skip=1;
884 } else {
885 word = rest;
889 word = next;
892 /* Default the destination if not supplied */
893 if (supplieddestination[0] == 0x00)
894 lstrcpyW(supplieddestination, wchr_dot);
896 *pflags = flags;
897 rc = RC_OK;
899 out:
900 free(cmdline);
901 return rc;
905 /* =========================================================================
906 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
907 converts it into a stem and a filespec
908 ========================================================================= */
909 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
910 WCHAR *spec, DWORD flags)
912 WCHAR actualsource[MAX_PATH];
913 WCHAR *starPos;
914 WCHAR *questPos;
915 DWORD attribs;
918 * Validate the source, expanding to full path ensuring it exists
920 if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
921 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
922 return RC_INITERROR;
925 /* If full names required, convert to using the full path */
926 if (flags & OPT_FULL) {
927 lstrcpyW(suppliedsource, actualsource);
931 * Work out the stem of the source
934 /* If a directory is supplied, use that as-is (either fully or
935 partially qualified)
936 If a filename is supplied + a directory or drive path, use that
937 as-is
938 Otherwise
939 If no directory or path specified, add eg. C:
940 stem is Drive/Directory is bit up to last \ (or first :)
941 spec is bit after that */
943 starPos = wcschr(suppliedsource, '*');
944 questPos = wcschr(suppliedsource, '?');
945 if (starPos || questPos) {
946 attribs = 0x00; /* Ensures skips invalid or directory check below */
947 } else {
948 attribs = GetFileAttributesW(actualsource);
951 if (attribs == INVALID_FILE_ATTRIBUTES) {
952 XCOPY_FailMessage(GetLastError());
953 return RC_INITERROR;
955 /* Directory:
956 stem should be exactly as supplied plus a '\', unless it was
957 eg. C: in which case no slash required */
958 } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
959 WCHAR lastChar;
961 WINE_TRACE("Directory supplied\n");
962 lstrcpyW(stem, suppliedsource);
963 lastChar = stem[lstrlenW(stem)-1];
964 if (lastChar != '\\' && lastChar != ':') {
965 lstrcatW(stem, wchr_slash);
967 lstrcpyW(spec, wchr_star);
969 /* File or wildcard search:
970 stem should be:
971 Up to and including last slash if directory path supplied
972 If c:filename supplied, just the c:
973 Otherwise stem should be the current drive letter + ':' */
974 } else {
975 WCHAR *lastDir;
977 WINE_TRACE("Filename supplied\n");
978 lastDir = wcsrchr(suppliedsource, '\\');
980 if (lastDir) {
981 lstrcpyW(stem, suppliedsource);
982 stem[(lastDir-suppliedsource) + 1] = 0x00;
983 lstrcpyW(spec, (lastDir+1));
984 } else if (suppliedsource[1] == ':') {
985 lstrcpyW(stem, suppliedsource);
986 stem[2] = 0x00;
987 lstrcpyW(spec, suppliedsource+2);
988 } else {
989 WCHAR curdir[MAXSTRING];
990 GetCurrentDirectoryW(ARRAY_SIZE(curdir), curdir);
991 stem[0] = curdir[0];
992 stem[1] = curdir[1];
993 stem[2] = 0x00;
994 lstrcpyW(spec, suppliedsource);
998 return RC_OK;
1001 /* =========================================================================
1002 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
1003 converts it into a stem
1004 ========================================================================= */
1005 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
1006 WCHAR *srcspec, DWORD flags)
1008 WCHAR actualdestination[MAX_PATH];
1009 DWORD attribs;
1010 BOOL isDir = FALSE;
1013 * Validate the source, expanding to full path ensuring it exists
1015 if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
1016 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
1017 return RC_INITERROR;
1020 /* Destination is either a directory or a file */
1021 attribs = GetFileAttributesW(actualdestination);
1023 if (attribs == INVALID_FILE_ATTRIBUTES) {
1025 /* If /I supplied and wildcard copy, assume directory */
1026 /* Also if destination ends with backslash */
1027 if ((flags & OPT_ASSUMEDIR &&
1028 (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) ||
1029 (supplieddestination[lstrlenW(supplieddestination)-1] == '\\')) {
1031 isDir = TRUE;
1033 } else {
1034 DWORD count;
1035 char answer[10] = "";
1036 WCHAR fileChar[2];
1037 WCHAR dirChar[2];
1039 /* Read the F and D characters from the resource file */
1040 wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
1041 wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
1043 while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
1044 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
1046 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
1047 WINE_TRACE("User answer %c\n", answer[0]);
1049 answer[0] = toupper(answer[0]);
1052 if (answer[0] == dirChar[0]) {
1053 isDir = TRUE;
1054 } else {
1055 isDir = FALSE;
1058 } else {
1059 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
1062 if (isDir) {
1063 lstrcpyW(stem, actualdestination);
1064 *spec = 0x00;
1066 /* Ensure ends with a '\' */
1067 if (stem[lstrlenW(stem)-1] != '\\') {
1068 lstrcatW(stem, wchr_slash);
1071 } else {
1072 WCHAR drive[MAX_PATH];
1073 WCHAR dir[MAX_PATH];
1074 WCHAR fname[MAX_PATH];
1075 WCHAR ext[MAX_PATH];
1076 _wsplitpath(actualdestination, drive, dir, fname, ext);
1077 lstrcpyW(stem, drive);
1078 lstrcatW(stem, dir);
1079 lstrcpyW(spec, fname);
1080 lstrcatW(spec, ext);
1082 return RC_OK;
1086 /* =========================================================================
1087 main - Main entrypoint for the xcopy command
1089 Processes the args, and drives the actual copying
1090 ========================================================================= */
1091 int wmain (int argc, WCHAR *argvW[])
1093 int rc = 0;
1094 WCHAR suppliedsource[MAX_PATH] = {0}; /* As supplied on the cmd line */
1095 WCHAR supplieddestination[MAX_PATH] = {0};
1096 WCHAR sourcestem[MAX_PATH] = {0}; /* Stem of source */
1097 WCHAR sourcespec[MAX_PATH] = {0}; /* Filespec of source */
1098 WCHAR destinationstem[MAX_PATH] = {0}; /* Stem of destination */
1099 WCHAR destinationspec[MAX_PATH] = {0}; /* Filespec of destination */
1100 WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */
1101 DWORD flags = 0; /* Option flags */
1102 const WCHAR PROMPTSTR1[] = {'/', 'Y', 0};
1103 const WCHAR PROMPTSTR2[] = {'/', 'y', 0};
1104 const WCHAR COPYCMD[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1106 /* Preinitialize flags based on COPYCMD */
1107 if (GetEnvironmentVariableW(COPYCMD, copyCmd, MAXSTRING)) {
1108 if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
1109 wcsstr(copyCmd, PROMPTSTR2) != NULL) {
1110 flags |= OPT_NOPROMPT;
1114 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1115 wine, but on windows these can be normal files. At least one installer
1116 uses files such as .packlist and (validly) expects them to be copied.
1117 Under wine, if we do not copy hidden files by default then they get
1118 lose */
1119 flags |= OPT_COPYHIDSYS;
1122 * Parse the command line
1124 if ((rc = XCOPY_ParseCommandLine(suppliedsource, supplieddestination,
1125 &flags)) != RC_OK) {
1126 if (rc == RC_HELP)
1127 return RC_OK;
1128 else
1129 return rc;
1132 /* Trace out the supplied information */
1133 WINE_TRACE("Supplied parameters:\n");
1134 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource));
1135 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
1137 /* Extract required information from source specification */
1138 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
1139 if (rc != RC_OK) return rc;
1141 /* Extract required information from destination specification */
1142 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
1143 destinationspec, sourcespec, flags);
1144 if (rc != RC_OK) return rc;
1146 /* Trace out the resulting information */
1147 WINE_TRACE("Resolved parameters:\n");
1148 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
1149 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
1150 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem));
1151 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec));
1153 /* Pause if necessary */
1154 if (flags & OPT_PAUSE) {
1155 DWORD count;
1156 char pausestr[10];
1158 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
1159 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
1160 &count, NULL);
1163 /* Now do the hard work... */
1164 rc = XCOPY_DoCopy(sourcestem, sourcespec,
1165 destinationstem, destinationspec,
1166 flags);
1168 /* Clear up exclude list allocated memory */
1169 while (excludeList) {
1170 EXCLUDELIST *pos = excludeList;
1171 excludeList = excludeList -> next;
1172 HeapFree(GetProcessHeap(), 0, pos->name);
1173 HeapFree(GetProcessHeap(), 0, pos);
1176 /* Finished - print trailer and exit */
1177 if (flags & OPT_SIMULATE) {
1178 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
1179 } else if (!(flags & OPT_NOCOPY)) {
1180 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
1182 return rc;