ddraw: Move the wined3d_texture_update_desc() call into ddraw_surface_create_wined3d_...
[wine.git] / programs / xcopy / xcopy.c
blob1d003007c0bb49e5f104a67f3e0dea0a4982b389
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 "xcopy.h"
51 WINE_DEFAULT_DEBUG_CHANNEL(xcopy);
54 /* Typedefs */
55 typedef struct _EXCLUDELIST
57 struct _EXCLUDELIST *next;
58 WCHAR *name;
59 } EXCLUDELIST;
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
69 made global */
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!");
85 return msg;
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
101 va_list parms;
102 DWORD nOut;
103 int len;
104 DWORD res = 0;
107 * Allocate buffer to use when writing to console
108 * Note: Not freed - memory will be allocated once and released when
109 * xcopy ends
112 if (!output_bufW) output_bufW = HeapAlloc(GetProcessHeap(), 0,
113 MAX_WRITECONSOLE_SIZE*sizeof(WCHAR));
114 if (!output_bufW) {
115 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
116 return 0;
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);
122 va_end(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));
125 return 0;
128 /* Try to write as unicode whenever we think it's a console */
129 if (toConsole) {
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 */
136 if (!res) {
137 BOOL usedDefaultChar = FALSE;
138 DWORD convertedChars;
140 toConsole = FALSE;
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);
147 if (!output_bufA) {
148 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
149 return 0;
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,
157 &nOut, FALSE);
160 /* Trace whether screen or console */
161 if (!traceOutput) {
162 WINE_TRACE("Writing to console? (%d)\n", toConsole);
163 traceOutput = TRUE;
165 return nOut;
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) {
173 LPWSTR lpMsgBuf;
174 int status;
176 status = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
177 FORMAT_MESSAGE_FROM_SYSTEM,
178 NULL, err, 0,
179 (LPWSTR) &lpMsgBuf, 0, NULL);
180 if (!status) {
181 WINE_FIXME("FIXME: Cannot display message for error %ld, status %ld\n",
182 err, GetLastError());
183 } else {
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)
197 int len;
198 WCHAR *new_path;
199 BOOL ret = TRUE;
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))
209 WCHAR *slash;
210 DWORD last_error = GetLastError();
211 if (last_error == ERROR_ALREADY_EXISTS)
212 break;
214 if (last_error != ERROR_PATH_NOT_FOUND)
216 ret = FALSE;
217 break;
220 if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
222 ret = FALSE;
223 break;
226 len = slash - new_path;
227 new_path[len] = 0;
228 if (!XCOPY_CreateDirectory(new_path))
230 ret = FALSE;
231 break;
233 new_path[len] = '\\';
235 HeapFree(GetProcessHeap(),0,new_path);
236 return ret;
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];
248 FILE *inFile = NULL;
250 /* Null terminate the filename (temporarily updates the filename hence
251 parms not const) */
252 *endOfName = 0x00;
254 /* Open the file */
255 inFile = _wfopen(filename, L"rt");
256 if (inFile == NULL) {
257 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
258 *endOfName = endChar;
259 return TRUE;
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 */
268 if (length > 1) {
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 */
282 if (!feof(inFile)) {
283 XCOPY_wprintf(XCOPY_LoadMessage(STRING_READFAIL), filename);
284 *endOfName = endChar;
285 fclose(inFile);
286 return TRUE;
289 /* Revert the input string to original form, and cleanup + return */
290 *endOfName = endChar;
291 fclose(inFile);
292 return FALSE;
295 /* =========================================================================
296 * Process the /EXCLUDE: file list, building up a list of substrings to
297 * avoid copying
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));
305 excludeList = NULL;
307 while (*parms && *parms != ' ' && *parms != '/') {
309 /* If found '+' then process the file found so far */
310 if (*parms == '+') {
311 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
312 return TRUE;
314 filenameStart = parms+1;
316 parms++;
319 if (filenameStart != parms) {
320 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
321 return TRUE;
325 return FALSE;
328 /* =========================================================================
329 XCOPY_DoCopy - Recursive function to copy files based on input parms
330 of a stem and a spec
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,
340 DWORD flags)
342 WIN32_FIND_DATAW *finddata;
343 HANDLE h;
344 BOOL findres = TRUE;
345 WCHAR *inputpath, *outputpath;
346 BOOL copiedFile = FALSE;
347 DWORD destAttribs, srcAttribs;
348 BOOL skipFile;
349 int ret = 0;
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) {
364 skipFile = FALSE;
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));
372 } else {
374 /* Get the filename information */
375 lstrcpyW(copyFrom, srcstem);
376 if (flags & OPT_SHORTNAME) {
377 lstrcatW(copyFrom, finddata->cAlternateFileName);
378 } else {
379 lstrcatW(copyFrom, finddata->cFileName);
382 lstrcpyW(copyTo, deststem);
383 if (*destspec == 0x00) {
384 if (flags & OPT_SHORTNAME) {
385 lstrcatW(copyTo, finddata->cAlternateFileName);
386 } else {
387 lstrcatW(copyTo, finddata->cFileName);
389 } else {
390 lstrcatW(copyTo, destspec);
393 /* Do the copy */
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)) {
406 skipFile = TRUE;
410 if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
411 (flags & OPT_ARCHIVEONLY)) {
412 skipFile = TRUE;
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");
423 skipFile = TRUE;
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,
431 NULL);
432 if (h != INVALID_HANDLE_VALUE) {
433 FILETIME writeTime;
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");
438 skipFile = TRUE;
440 CloseHandle(h);
444 /* See if exclude list provided. Note since filenames are case
445 insensitive, need to uppercase the filename before doing
446 strstr */
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 */
456 while (pos) {
457 if (wcsstr(copyFromUpper, pos->name) != NULL) {
458 WINE_TRACE("Skipping file as matches exclude '%s'\n",
459 wine_dbgstr_w(pos->name));
460 skipFile = TRUE;
461 pos = NULL;
462 } else {
463 pos = pos->next;
468 /* Prompt each file if necessary */
469 if (!skipFile && (flags & OPT_SRCPROMPT)) {
470 DWORD count;
471 char answer[10];
472 BOOL answered = FALSE;
473 WCHAR yesChar[2];
474 WCHAR noChar[2];
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));
480 while (!answered) {
481 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
482 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
483 &count, NULL);
485 answered = TRUE;
486 if (toupper(answer[0]) == noChar[0])
487 skipFile = TRUE;
488 else if (toupper(answer[0]) != yesChar[0])
489 answered = FALSE;
493 if (!skipFile &&
494 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
495 DWORD count;
496 char answer[10];
497 BOOL answered = FALSE;
498 WCHAR yesChar[2];
499 WCHAR allChar[2];
500 WCHAR noChar[2];
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));
507 while (!answered) {
508 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
509 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
510 &count, NULL);
512 answered = TRUE;
513 if (toupper(answer[0]) == allChar[0])
514 flags |= OPT_NOPROMPT;
515 else if (toupper(answer[0]) == noChar[0])
516 skipFile = TRUE;
517 else if (toupper(answer[0]) != yesChar[0])
518 answered = FALSE;
522 /* See if it has to exist! */
523 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
524 skipFile = TRUE;
527 /* Output a status message */
528 if (!skipFile) {
529 if (!(flags & OPT_QUIET)) {
530 if (flags & OPT_FULL)
531 XCOPY_wprintf(L"%1 -> %2\n", copyFrom, copyTo);
532 else
533 XCOPY_wprintf(L"%1\n", copyFrom);
536 /* If allowing overwriting of read only files, remove any
537 write protection */
538 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
539 (flags & OPT_REPLACEREAD)) {
540 SetFileAttributesW(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
543 copiedFile = TRUE;
544 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
545 /* Skip copy */
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) {
554 skipFile = TRUE;
555 } else {
556 ret = RC_WRITEERROR;
557 goto cleanup;
559 } else {
561 if (!skipFile) {
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);
566 } else {
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));
576 filesCopied++;
582 /* Find next file */
583 findres = FindNextFileW(h, finddata);
585 FindClose(h);
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"*");
598 findres = TRUE;
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);
625 /* Find next one */
626 findres = FindNextFileW(h, finddata);
628 FindClose(h);
631 cleanup:
633 /* free up memory */
634 HeapFree(GetProcessHeap(), 0, finddata);
635 HeapFree(GetProcessHeap(), 0, inputpath);
636 HeapFree(GetProcessHeap(), 0, outputpath);
638 return ret;
642 /* =========================================================================
643 XCOPY_ParseCommandLine - Parses the command line
644 ========================================================================= */
645 static inline BOOL is_whitespace(WCHAR c)
647 return c == ' ' || c == '\t';
650 static WCHAR *skip_whitespace(WCHAR *p)
652 for (; *p && is_whitespace(*p); p++);
653 return p;
656 static inline BOOL is_digit(WCHAR c)
658 return c >= '0' && c <= '9';
661 /* Windows XCOPY uses a simplified command line parsing algorithm
662 that lacks the escaped-quote logic of build_argv(), because
663 literal double quotes are illegal in any of its arguments.
664 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
665 static int find_end_of_word(const WCHAR *word, WCHAR **end)
667 BOOL in_quotes = FALSE;
668 const WCHAR *ptr = word;
669 for (;;) {
670 for (; *ptr != '\0' && *ptr != '"' &&
671 (in_quotes || !is_whitespace(*ptr)); ptr++);
672 if (*ptr == '"') {
673 in_quotes = !in_quotes;
674 ptr++;
676 /* Odd number of double quotes is illegal for XCOPY */
677 if (in_quotes && *ptr == '\0')
678 return RC_INITERROR;
679 if (*ptr == '\0' || (!in_quotes && is_whitespace(*ptr)))
680 break;
682 *end = (WCHAR*)ptr;
683 return RC_OK;
686 /* Remove all double quotes from a word */
687 static void strip_quotes(WCHAR *word, WCHAR **end)
689 WCHAR *rp, *wp;
690 for (rp = word, wp = word; *rp != '\0'; rp++) {
691 if (*rp == '"')
692 continue;
693 if (wp < rp)
694 *wp = *rp;
695 wp++;
697 *wp = '\0';
698 *end = wp;
701 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource,
702 WCHAR *supplieddestination, DWORD *pflags)
704 DWORD flags = *pflags;
705 WCHAR *cmdline, *word, *end, *next;
706 int rc = RC_INITERROR;
708 cmdline = _wcsdup(GetCommandLineW());
709 if (cmdline == NULL)
710 return rc;
712 /* Skip first arg, which is the program name */
713 if ((rc = find_end_of_word(cmdline, &word)) != RC_OK)
714 goto out;
715 word = skip_whitespace(word);
717 while (*word)
719 WCHAR first;
720 if ((rc = find_end_of_word(word, &end)) != RC_OK)
721 goto out;
723 next = skip_whitespace(end);
724 first = word[0];
725 *end = '\0';
726 strip_quotes(word, &end);
727 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word));
729 /* First non-switch parameter is source, second is destination */
730 if (first != '/') {
731 if (suppliedsource[0] == 0x00) {
732 lstrcpyW(suppliedsource, word);
733 } else if (supplieddestination[0] == 0x00) {
734 lstrcpyW(supplieddestination, word);
735 } else {
736 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
737 goto out;
739 } else {
740 /* Process all the switch options
741 Note: Windows docs say /P prompts when dest is created
742 but tests show it is done for each src file
743 regardless of the destination */
744 int skip=0;
745 WCHAR *rest;
747 while (word[0]) {
748 rest = NULL;
750 switch (toupper(word[1])) {
751 case 'I': flags |= OPT_ASSUMEDIR; break;
752 case 'S': flags |= OPT_RECURSIVE; break;
753 case 'Q': flags |= OPT_QUIET; break;
754 case 'F': flags |= OPT_FULL; break;
755 case 'L': flags |= OPT_SIMULATE; break;
756 case 'W': flags |= OPT_PAUSE; break;
757 case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
758 case 'Y': flags |= OPT_NOPROMPT; break;
759 case 'N': flags |= OPT_SHORTNAME; break;
760 case 'U': flags |= OPT_MUSTEXIST; break;
761 case 'R': flags |= OPT_REPLACEREAD; break;
762 case 'K': flags |= OPT_KEEPATTRS; break;
763 case 'H': flags |= OPT_COPYHIDSYS; break;
764 case 'C': flags |= OPT_IGNOREERRORS; break;
765 case 'P': flags |= OPT_SRCPROMPT; break;
766 case 'A': flags |= OPT_ARCHIVEONLY; break;
767 case 'M': flags |= OPT_ARCHIVEONLY |
768 OPT_REMOVEARCH; break;
770 /* E can be /E or /EXCLUDE */
771 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE | SORT_STRINGSORT,
772 &word[1], 8, L"EXCLUDE:", -1) == CSTR_EQUAL) {
773 if (XCOPY_ProcessExcludeList(&word[9])) {
774 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
775 goto out;
776 } else {
777 flags |= OPT_EXCLUDELIST;
779 /* Do not support concatenated switches onto exclude lists yet */
780 rest = end;
782 } else {
783 flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
785 break;
787 /* D can be /D or /D: */
788 case 'D': if (word[2]==':' && is_digit(word[3])) {
789 SYSTEMTIME st;
790 WCHAR *pos = &word[3];
791 BOOL isError = FALSE;
792 memset(&st, 0x00, sizeof(st));
794 /* Microsoft xcopy's usage message implies that the date
795 * format depends on the locale, but that is false.
796 * It is hardcoded to month-day-year.
798 st.wMonth = _wtol(pos);
799 while (*pos && is_digit(*pos)) pos++;
800 if (*pos++ != '-') isError = TRUE;
802 if (!isError) {
803 st.wDay = _wtol(pos);
804 while (*pos && is_digit(*pos)) pos++;
805 if (*pos++ != '-') isError = TRUE;
808 if (!isError) {
809 st.wYear = _wtol(pos);
810 while (*pos && is_digit(*pos)) pos++;
811 if (st.wYear < 100) st.wYear+=2000;
814 /* Handle switches straight after the supplied date */
815 rest = pos;
817 if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
818 SYSTEMTIME st;
819 WCHAR datestring[32], timestring[32];
821 flags |= OPT_DATERANGE;
823 /* Debug info: */
824 FileTimeToSystemTime (&dateRange, &st);
825 GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring,
826 ARRAY_SIZE(datestring));
827 GetTimeFormatW(0, TIME_NOSECONDS, &st,
828 NULL, timestring, ARRAY_SIZE(timestring));
830 WINE_TRACE("Date being used is: %s %s\n",
831 wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
832 } else {
833 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
834 goto out;
836 } else {
837 flags |= OPT_DATENEWER;
839 break;
841 case '-': if (toupper(word[2])=='Y') {
842 flags &= ~OPT_NOPROMPT;
843 rest = &word[3]; /* Skip over 3 characters */
845 break;
846 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
847 rc = RC_HELP;
848 goto out;
849 case 'V':
850 WINE_FIXME("ignoring /V\n");
851 break;
852 default:
853 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word));
854 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), word);
855 goto out;
858 /* Unless overridden above, skip over the '/' and the first character */
859 if (rest == NULL) rest = &word[2];
861 /* By now, rest should point either to the null after the
862 switch, or the beginning of the next switch if there
863 was no whitespace between them */
864 if (!skip && *rest && *rest != '/') {
865 WINE_FIXME("Unexpected characters found and ignored '%s'\n", wine_dbgstr_w(rest));
866 skip=1;
867 } else {
868 word = rest;
872 word = next;
875 /* Default the destination if not supplied */
876 if (supplieddestination[0] == 0x00)
877 lstrcpyW(supplieddestination, L".");
879 *pflags = flags;
880 rc = RC_OK;
882 out:
883 free(cmdline);
884 return rc;
888 /* =========================================================================
889 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
890 converts it into a stem and a filespec
891 ========================================================================= */
892 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
893 WCHAR *spec, DWORD flags)
895 WCHAR actualsource[MAX_PATH];
896 WCHAR *starPos;
897 WCHAR *questPos;
898 DWORD attribs;
901 * Validate the source, expanding to full path ensuring it exists
903 if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
904 WINE_FIXME("Unexpected failure expanding source path (%ld)\n", GetLastError());
905 return RC_INITERROR;
908 /* If full names required, convert to using the full path */
909 if (flags & OPT_FULL) {
910 lstrcpyW(suppliedsource, actualsource);
914 * Work out the stem of the source
917 /* If a directory is supplied, use that as-is (either fully or
918 partially qualified)
919 If a filename is supplied + a directory or drive path, use that
920 as-is
921 Otherwise
922 If no directory or path specified, add eg. C:
923 stem is Drive/Directory is bit up to last \ (or first :)
924 spec is bit after that */
926 starPos = wcschr(suppliedsource, '*');
927 questPos = wcschr(suppliedsource, '?');
928 if (starPos || questPos) {
929 attribs = 0x00; /* Ensures skips invalid or directory check below */
930 } else {
931 attribs = GetFileAttributesW(actualsource);
934 if (attribs == INVALID_FILE_ATTRIBUTES) {
935 XCOPY_FailMessage(GetLastError());
936 return RC_INITERROR;
938 /* Directory:
939 stem should be exactly as supplied plus a '\', unless it was
940 eg. C: in which case no slash required */
941 } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
942 WCHAR lastChar;
944 WINE_TRACE("Directory supplied\n");
945 lstrcpyW(stem, suppliedsource);
946 lastChar = stem[lstrlenW(stem)-1];
947 if (lastChar != '\\' && lastChar != ':')
948 lstrcatW(stem, L"\\");
949 lstrcpyW(spec, L"*");
951 /* File or wildcard search:
952 stem should be:
953 Up to and including last slash if directory path supplied
954 If c:filename supplied, just the c:
955 Otherwise stem should be the current drive letter + ':' */
956 } else {
957 WCHAR *lastDir;
959 WINE_TRACE("Filename supplied\n");
960 lastDir = wcsrchr(suppliedsource, '\\');
962 if (lastDir) {
963 lstrcpyW(stem, suppliedsource);
964 stem[(lastDir-suppliedsource) + 1] = 0x00;
965 lstrcpyW(spec, (lastDir+1));
966 } else if (suppliedsource[1] == ':') {
967 lstrcpyW(stem, suppliedsource);
968 stem[2] = 0x00;
969 lstrcpyW(spec, suppliedsource+2);
970 } else {
971 WCHAR curdir[MAXSTRING];
972 GetCurrentDirectoryW(ARRAY_SIZE(curdir), curdir);
973 stem[0] = curdir[0];
974 stem[1] = curdir[1];
975 stem[2] = 0x00;
976 lstrcpyW(spec, suppliedsource);
980 return RC_OK;
983 /* =========================================================================
984 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
985 converts it into a stem
986 ========================================================================= */
987 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
988 WCHAR *srcspec, DWORD flags)
990 WCHAR actualdestination[MAX_PATH];
991 DWORD attribs;
992 BOOL isDir = FALSE;
995 * Validate the source, expanding to full path ensuring it exists
997 if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
998 WINE_FIXME("Unexpected failure expanding source path (%ld)\n", GetLastError());
999 return RC_INITERROR;
1002 /* Destination is either a directory or a file */
1003 attribs = GetFileAttributesW(actualdestination);
1005 if (attribs == INVALID_FILE_ATTRIBUTES) {
1007 /* If /I supplied and wildcard copy, assume directory */
1008 /* Also if destination ends with backslash */
1009 if ((flags & OPT_ASSUMEDIR &&
1010 (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) ||
1011 (supplieddestination[lstrlenW(supplieddestination)-1] == '\\')) {
1013 isDir = TRUE;
1015 } else {
1016 DWORD count;
1017 char answer[10] = "";
1018 WCHAR fileChar[2];
1019 WCHAR dirChar[2];
1021 /* Read the F and D characters from the resource file */
1022 wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
1023 wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
1025 while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
1026 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
1028 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
1029 WINE_TRACE("User answer %c\n", answer[0]);
1031 answer[0] = toupper(answer[0]);
1034 if (answer[0] == dirChar[0]) {
1035 isDir = TRUE;
1036 } else {
1037 isDir = FALSE;
1040 } else {
1041 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
1044 if (isDir) {
1045 lstrcpyW(stem, actualdestination);
1046 *spec = 0x00;
1048 /* Ensure ends with a '\' */
1049 if (stem[lstrlenW(stem)-1] != '\\')
1050 lstrcatW(stem, L"\\");
1052 } else {
1053 WCHAR drive[MAX_PATH];
1054 WCHAR dir[MAX_PATH];
1055 WCHAR fname[MAX_PATH];
1056 WCHAR ext[MAX_PATH];
1057 _wsplitpath(actualdestination, drive, dir, fname, ext);
1058 lstrcpyW(stem, drive);
1059 lstrcatW(stem, dir);
1060 lstrcpyW(spec, fname);
1061 lstrcatW(spec, ext);
1063 return RC_OK;
1067 /* =========================================================================
1068 main - Main entrypoint for the xcopy command
1070 Processes the args, and drives the actual copying
1071 ========================================================================= */
1072 int __cdecl wmain (int argc, WCHAR *argvW[])
1074 int rc = 0;
1075 WCHAR suppliedsource[MAX_PATH] = {0}; /* As supplied on the cmd line */
1076 WCHAR supplieddestination[MAX_PATH] = {0};
1077 WCHAR sourcestem[MAX_PATH] = {0}; /* Stem of source */
1078 WCHAR sourcespec[MAX_PATH] = {0}; /* Filespec of source */
1079 WCHAR destinationstem[MAX_PATH] = {0}; /* Stem of destination */
1080 WCHAR destinationspec[MAX_PATH] = {0}; /* Filespec of destination */
1081 WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */
1082 DWORD flags = 0; /* Option flags */
1084 /* Preinitialize flags based on COPYCMD */
1085 if (GetEnvironmentVariableW(L"COPYCMD", copyCmd, MAXSTRING)) {
1086 if (wcsstr(copyCmd, L"/Y") != NULL || wcsstr(copyCmd, L"/y") != NULL)
1087 flags |= OPT_NOPROMPT;
1090 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1091 wine, but on windows these can be normal files. At least one installer
1092 uses files such as .packlist and (validly) expects them to be copied.
1093 Under wine, if we do not copy hidden files by default then they get
1094 lose */
1095 flags |= OPT_COPYHIDSYS;
1098 * Parse the command line
1100 if ((rc = XCOPY_ParseCommandLine(suppliedsource, supplieddestination,
1101 &flags)) != RC_OK) {
1102 if (rc == RC_HELP)
1103 return RC_OK;
1104 else
1105 return rc;
1108 /* Trace out the supplied information */
1109 WINE_TRACE("Supplied parameters:\n");
1110 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource));
1111 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
1113 /* Extract required information from source specification */
1114 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
1115 if (rc != RC_OK) return rc;
1117 /* Extract required information from destination specification */
1118 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
1119 destinationspec, sourcespec, flags);
1120 if (rc != RC_OK) return rc;
1122 /* Trace out the resulting information */
1123 WINE_TRACE("Resolved parameters:\n");
1124 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
1125 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
1126 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem));
1127 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec));
1129 /* Pause if necessary */
1130 if (flags & OPT_PAUSE) {
1131 DWORD count;
1132 char pausestr[10];
1134 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
1135 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
1136 &count, NULL);
1139 /* Now do the hard work... */
1140 rc = XCOPY_DoCopy(sourcestem, sourcespec,
1141 destinationstem, destinationspec,
1142 flags);
1144 /* Clear up exclude list allocated memory */
1145 while (excludeList) {
1146 EXCLUDELIST *pos = excludeList;
1147 excludeList = excludeList -> next;
1148 HeapFree(GetProcessHeap(), 0, pos->name);
1149 HeapFree(GetProcessHeap(), 0, pos);
1152 /* Finished - print trailer and exit */
1153 if (flags & OPT_SIMULATE) {
1154 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
1155 } else if (!(flags & OPT_NOCOPY)) {
1156 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
1158 return rc;