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