winemac: Honor requests to order owned windows relative to other owned windows of...
[wine/multimedia.git] / programs / xcopy / xcopy.c
blob7a688311bdabae66a8172cc0e8036fd4f04e30e1
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 * Apparently, valid return codes are:
35 * 0 - OK
36 * 1 - No files found to copy
37 * 2 - CTRL+C during copy
38 * 4 - Initialization error, or invalid source specification
39 * 5 - Disk write error
43 #include <stdio.h>
44 #include <stdlib.h>
45 #include <windows.h>
46 #include <wine/debug.h>
47 #include <wine/unicode.h>
48 #include "xcopy.h"
50 WINE_DEFAULT_DEBUG_CHANNEL(xcopy);
53 /* Typedefs */
54 typedef struct _EXCLUDELIST
56 struct _EXCLUDELIST *next;
57 WCHAR *name;
58 } EXCLUDELIST;
61 /* Global variables */
62 static ULONG filesCopied = 0; /* Number of files copied */
63 static EXCLUDELIST *excludeList = NULL; /* Excluded strings list */
64 static FILETIME dateRange; /* Date range to copy after*/
65 static const WCHAR wchr_slash[] = {'\\', 0};
66 static const WCHAR wchr_star[] = {'*', 0};
67 static const WCHAR wchr_dot[] = {'.', 0};
68 static const WCHAR wchr_dotdot[] = {'.', '.', 0};
71 /* To minimize stack usage during recursion, some temporary variables
72 made global */
73 static WCHAR copyFrom[MAX_PATH];
74 static WCHAR copyTo[MAX_PATH];
77 /* =========================================================================
78 * Load a string from the resource file, handling any error
79 * Returns string retrieved from resource file
80 * ========================================================================= */
81 static WCHAR *XCOPY_LoadMessage(UINT id) {
82 static WCHAR msg[MAXSTRING];
83 const WCHAR failedMsg[] = {'F', 'a', 'i', 'l', 'e', 'd', '!', 0};
85 if (!LoadStringW(GetModuleHandleW(NULL), id, msg, sizeof(msg)/sizeof(WCHAR))) {
86 WINE_FIXME("LoadString failed with %d\n", GetLastError());
87 lstrcpyW(msg, failedMsg);
89 return msg;
92 /* =========================================================================
93 * Output a formatted unicode string. Ideally this will go to the console
94 * and hence required WriteConsoleW to output it, however if file i/o is
95 * redirected, it needs to be WriteFile'd using OEM (not ANSI) format
96 * ========================================================================= */
97 static int __cdecl XCOPY_wprintf(const WCHAR *format, ...) {
99 static WCHAR *output_bufW = NULL;
100 static char *output_bufA = NULL;
101 static BOOL toConsole = TRUE;
102 static BOOL traceOutput = FALSE;
103 #define MAX_WRITECONSOLE_SIZE 65535
105 __ms_va_list parms;
106 DWORD nOut;
107 int len;
108 DWORD res = 0;
111 * Allocate buffer to use when writing to console
112 * Note: Not freed - memory will be allocated once and released when
113 * xcopy ends
116 if (!output_bufW) output_bufW = HeapAlloc(GetProcessHeap(), 0,
117 MAX_WRITECONSOLE_SIZE);
118 if (!output_bufW) {
119 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
120 return 0;
123 __ms_va_start(parms, format);
124 SetLastError(NO_ERROR);
125 len = FormatMessageW(FORMAT_MESSAGE_FROM_STRING, format, 0, 0, output_bufW,
126 MAX_WRITECONSOLE_SIZE/sizeof(*output_bufW), &parms);
127 __ms_va_end(parms);
128 if (len == 0 && GetLastError() != NO_ERROR) {
129 WINE_FIXME("Could not format string: le=%u, fmt=%s\n", GetLastError(), wine_dbgstr_w(format));
130 return 0;
133 /* Try to write as unicode whenever we think it's a console */
134 if (toConsole) {
135 res = WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),
136 output_bufW, len, &nOut, NULL);
139 /* If writing to console has failed (ever) we assume it's file
140 i/o so convert to OEM codepage and output */
141 if (!res) {
142 BOOL usedDefaultChar = FALSE;
143 DWORD convertedChars;
145 toConsole = FALSE;
148 * Allocate buffer to use when writing to file. Not freed, as above
150 if (!output_bufA) output_bufA = HeapAlloc(GetProcessHeap(), 0,
151 MAX_WRITECONSOLE_SIZE);
152 if (!output_bufA) {
153 WINE_FIXME("Out of memory - could not allocate 2 x 64K buffers\n");
154 return 0;
157 /* Convert to OEM, then output */
158 convertedChars = WideCharToMultiByte(GetConsoleOutputCP(), 0, output_bufW,
159 len, output_bufA, MAX_WRITECONSOLE_SIZE,
160 "?", &usedDefaultChar);
161 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), output_bufA, convertedChars,
162 &nOut, FALSE);
165 /* Trace whether screen or console */
166 if (!traceOutput) {
167 WINE_TRACE("Writing to console? (%d)\n", toConsole);
168 traceOutput = TRUE;
170 return nOut;
173 /* =========================================================================
174 * Load a string for a system error and writes it to the screen
175 * Returns string retrieved from resource file
176 * ========================================================================= */
177 static void XCOPY_FailMessage(DWORD err) {
178 LPWSTR lpMsgBuf;
179 int status;
181 status = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
182 FORMAT_MESSAGE_FROM_SYSTEM,
183 NULL, err, 0,
184 (LPWSTR) &lpMsgBuf, 0, NULL);
185 if (!status) {
186 WINE_FIXME("FIXME: Cannot display message for error %d, status %d\n",
187 err, GetLastError());
188 } else {
189 const WCHAR infostr[] = {'%', '1', '\n', 0};
190 XCOPY_wprintf(infostr, lpMsgBuf);
191 LocalFree ((HLOCAL)lpMsgBuf);
196 /* =========================================================================
197 * Routine copied from cmd.exe md command -
198 * This works recursively. so creating dir1\dir2\dir3 will create dir1 and
199 * dir2 if they do not already exist.
200 * ========================================================================= */
201 static BOOL XCOPY_CreateDirectory(const WCHAR* path)
203 int len;
204 WCHAR *new_path;
205 BOOL ret = TRUE;
207 new_path = HeapAlloc(GetProcessHeap(),0, sizeof(WCHAR) * (lstrlenW(path)+1));
208 lstrcpyW(new_path,path);
210 while ((len = lstrlenW(new_path)) && new_path[len - 1] == '\\')
211 new_path[len - 1] = 0;
213 while (!CreateDirectoryW(new_path,NULL))
215 WCHAR *slash;
216 DWORD last_error = GetLastError();
217 if (last_error == ERROR_ALREADY_EXISTS)
218 break;
220 if (last_error != ERROR_PATH_NOT_FOUND)
222 ret = FALSE;
223 break;
226 if (!(slash = wcsrchr(new_path,'\\')) && ! (slash = wcsrchr(new_path,'/')))
228 ret = FALSE;
229 break;
232 len = slash - new_path;
233 new_path[len] = 0;
234 if (!XCOPY_CreateDirectory(new_path))
236 ret = FALSE;
237 break;
239 new_path[len] = '\\';
241 HeapFree(GetProcessHeap(),0,new_path);
242 return ret;
245 /* =========================================================================
246 * Process a single file from the /EXCLUDE: file list, building up a list
247 * of substrings to avoid copying
248 * Returns TRUE on any failure
249 * ========================================================================= */
250 static BOOL XCOPY_ProcessExcludeFile(WCHAR* filename, WCHAR* endOfName) {
252 WCHAR endChar = *endOfName;
253 WCHAR buffer[MAXSTRING];
254 FILE *inFile = NULL;
255 const WCHAR readTextMode[] = {'r', 't', 0};
257 /* Null terminate the filename (temporarily updates the filename hence
258 parms not const) */
259 *endOfName = 0x00;
261 /* Open the file */
262 inFile = _wfopen(filename, readTextMode);
263 if (inFile == NULL) {
264 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OPENFAIL), filename);
265 *endOfName = endChar;
266 return TRUE;
269 /* Process line by line */
270 while (fgetws(buffer, sizeof(buffer)/sizeof(WCHAR), inFile) != NULL) {
271 EXCLUDELIST *thisEntry;
272 int length = lstrlenW(buffer);
274 /* Strip CRLF */
275 buffer[length-1] = 0x00;
277 /* If more than CRLF */
278 if (length > 1) {
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 return TRUE;
297 /* Revert the input string to original form, and cleanup + return */
298 *endOfName = endChar;
299 fclose(inFile);
300 return FALSE;
303 /* =========================================================================
304 * Process the /EXCLUDE: file list, building up a list of substrings to
305 * avoid copying
306 * Returns TRUE on any failure
307 * ========================================================================= */
308 static BOOL XCOPY_ProcessExcludeList(WCHAR* parms) {
310 WCHAR *filenameStart = parms;
312 WINE_TRACE("/EXCLUDE parms: '%s'\n", wine_dbgstr_w(parms));
313 excludeList = NULL;
315 while (*parms && *parms != ' ' && *parms != '/') {
317 /* If found '+' then process the file found so far */
318 if (*parms == '+') {
319 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
320 return TRUE;
322 filenameStart = parms+1;
324 parms++;
327 if (filenameStart != parms) {
328 if (XCOPY_ProcessExcludeFile(filenameStart, parms)) {
329 return TRUE;
333 return FALSE;
336 /* =========================================================================
337 XCOPY_DoCopy - Recursive function to copy files based on input parms
338 of a stem and a spec
340 This works by using FindFirstFile supplying the source stem and spec.
341 If results are found, any non-directory ones are processed
342 Then, if /S or /E is supplied, another search is made just for
343 directories, and this function is called again for that directory
345 ========================================================================= */
346 static int XCOPY_DoCopy(WCHAR *srcstem, WCHAR *srcspec,
347 WCHAR *deststem, WCHAR *destspec,
348 DWORD flags)
350 WIN32_FIND_DATAW *finddata;
351 HANDLE h;
352 BOOL findres = TRUE;
353 WCHAR *inputpath, *outputpath;
354 BOOL copiedFile = FALSE;
355 DWORD destAttribs, srcAttribs;
356 BOOL skipFile;
357 int ret = 0;
359 /* Allocate some working memory on heap to minimize footprint */
360 finddata = HeapAlloc(GetProcessHeap(), 0, sizeof(WIN32_FIND_DATAW));
361 inputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
362 outputpath = HeapAlloc(GetProcessHeap(), 0, MAX_PATH * sizeof(WCHAR));
364 /* Build the search info into a single parm */
365 lstrcpyW(inputpath, srcstem);
366 lstrcatW(inputpath, srcspec);
368 /* Search 1 - Look for matching files */
369 h = FindFirstFileW(inputpath, finddata);
370 while (h != INVALID_HANDLE_VALUE && findres) {
372 skipFile = FALSE;
374 /* Ignore . and .. */
375 if (lstrcmpW(finddata->cFileName, wchr_dot)==0 ||
376 lstrcmpW(finddata->cFileName, wchr_dotdot)==0 ||
377 finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
379 WINE_TRACE("Skipping directory, . or .. (%s)\n", wine_dbgstr_w(finddata->cFileName));
380 } else {
382 /* Get the filename information */
383 lstrcpyW(copyFrom, srcstem);
384 if (flags & OPT_SHORTNAME) {
385 lstrcatW(copyFrom, finddata->cAlternateFileName);
386 } else {
387 lstrcatW(copyFrom, finddata->cFileName);
390 lstrcpyW(copyTo, deststem);
391 if (*destspec == 0x00) {
392 if (flags & OPT_SHORTNAME) {
393 lstrcatW(copyTo, finddata->cAlternateFileName);
394 } else {
395 lstrcatW(copyTo, finddata->cFileName);
397 } else {
398 lstrcatW(copyTo, destspec);
401 /* Do the copy */
402 WINE_TRACE("ACTION: Copy '%s' -> '%s'\n", wine_dbgstr_w(copyFrom),
403 wine_dbgstr_w(copyTo));
404 if (!copiedFile && !(flags & OPT_SIMULATE)) XCOPY_CreateDirectory(deststem);
406 /* See if allowed to copy it */
407 srcAttribs = GetFileAttributesW(copyFrom);
408 WINE_TRACE("Source attribs: %d\n", srcAttribs);
410 if ((srcAttribs & FILE_ATTRIBUTE_HIDDEN) ||
411 (srcAttribs & FILE_ATTRIBUTE_SYSTEM)) {
413 if (!(flags & OPT_COPYHIDSYS)) {
414 skipFile = TRUE;
418 if (!(srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
419 (flags & OPT_ARCHIVEONLY)) {
420 skipFile = TRUE;
423 /* See if file exists */
424 destAttribs = GetFileAttributesW(copyTo);
425 WINE_TRACE("Dest attribs: %d\n", srcAttribs);
427 /* Check date ranges if a destination file already exists */
428 if (!skipFile && (flags & OPT_DATERANGE) &&
429 (CompareFileTime(&finddata->ftLastWriteTime, &dateRange) < 0)) {
430 WINE_TRACE("Skipping file as modified date too old\n");
431 skipFile = TRUE;
434 /* If just /D supplied, only overwrite if src newer than dest */
435 if (!skipFile && (flags & OPT_DATENEWER) &&
436 (destAttribs != INVALID_FILE_ATTRIBUTES)) {
437 HANDLE h = CreateFileW(copyTo, GENERIC_READ, FILE_SHARE_READ,
438 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
439 NULL);
440 if (h != INVALID_HANDLE_VALUE) {
441 FILETIME writeTime;
442 GetFileTime(h, NULL, NULL, &writeTime);
444 if (CompareFileTime(&finddata->ftLastWriteTime, &writeTime) <= 0) {
445 WINE_TRACE("Skipping file as dest newer or same date\n");
446 skipFile = TRUE;
448 CloseHandle(h);
452 /* See if exclude list provided. Note since filenames are case
453 insensitive, need to uppercase the filename before doing
454 strstr */
455 if (!skipFile && (flags & OPT_EXCLUDELIST)) {
456 EXCLUDELIST *pos = excludeList;
457 WCHAR copyFromUpper[MAX_PATH];
459 /* Uppercase source filename */
460 lstrcpyW(copyFromUpper, copyFrom);
461 CharUpperBuffW(copyFromUpper, lstrlenW(copyFromUpper));
463 /* Loop through testing each exclude line */
464 while (pos) {
465 if (wcsstr(copyFromUpper, pos->name) != NULL) {
466 WINE_TRACE("Skipping file as matches exclude '%s'\n",
467 wine_dbgstr_w(pos->name));
468 skipFile = TRUE;
469 pos = NULL;
470 } else {
471 pos = pos->next;
476 /* Prompt each file if necessary */
477 if (!skipFile && (flags & OPT_SRCPROMPT)) {
478 DWORD count;
479 char answer[10];
480 BOOL answered = FALSE;
481 WCHAR yesChar[2];
482 WCHAR noChar[2];
484 /* Read the Y and N characters from the resource file */
485 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
486 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
488 while (!answered) {
489 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SRCPROMPT), copyFrom);
490 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
491 &count, NULL);
493 answered = TRUE;
494 if (toupper(answer[0]) == noChar[0])
495 skipFile = TRUE;
496 else if (toupper(answer[0]) != yesChar[0])
497 answered = FALSE;
501 if (!skipFile &&
502 destAttribs != INVALID_FILE_ATTRIBUTES && !(flags & OPT_NOPROMPT)) {
503 DWORD count;
504 char answer[10];
505 BOOL answered = FALSE;
506 WCHAR yesChar[2];
507 WCHAR allChar[2];
508 WCHAR noChar[2];
510 /* Read the A,Y and N characters from the resource file */
511 wcscpy(yesChar, XCOPY_LoadMessage(STRING_YES_CHAR));
512 wcscpy(allChar, XCOPY_LoadMessage(STRING_ALL_CHAR));
513 wcscpy(noChar, XCOPY_LoadMessage(STRING_NO_CHAR));
515 while (!answered) {
516 XCOPY_wprintf(XCOPY_LoadMessage(STRING_OVERWRITE), copyTo);
517 ReadFile (GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer),
518 &count, NULL);
520 answered = TRUE;
521 if (toupper(answer[0]) == allChar[0])
522 flags |= OPT_NOPROMPT;
523 else if (toupper(answer[0]) == noChar[0])
524 skipFile = TRUE;
525 else if (toupper(answer[0]) != yesChar[0])
526 answered = FALSE;
530 /* See if it has to exist! */
531 if (destAttribs == INVALID_FILE_ATTRIBUTES && (flags & OPT_MUSTEXIST)) {
532 skipFile = TRUE;
535 /* Output a status message */
536 if (!skipFile) {
537 if (flags & OPT_QUIET) {
538 /* Skip message */
539 } else if (flags & OPT_FULL) {
540 const WCHAR infostr[] = {'%', '1', ' ', '-', '>', ' ',
541 '%', '2', '\n', 0};
543 XCOPY_wprintf(infostr, copyFrom, copyTo);
544 } else {
545 const WCHAR infostr[] = {'%', '1', '\n', 0};
546 XCOPY_wprintf(infostr, copyFrom);
549 /* If allowing overwriting of read only files, remove any
550 write protection */
551 if ((destAttribs & FILE_ATTRIBUTE_READONLY) &&
552 (flags & OPT_REPLACEREAD)) {
553 SetFileAttributesW(copyTo, destAttribs & ~FILE_ATTRIBUTE_READONLY);
556 copiedFile = TRUE;
557 if (flags & OPT_SIMULATE || flags & OPT_NOCOPY) {
558 /* Skip copy */
559 } else if (CopyFileW(copyFrom, copyTo, FALSE) == 0) {
561 DWORD error = GetLastError();
562 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPYFAIL),
563 copyFrom, copyTo, error);
564 XCOPY_FailMessage(error);
566 if (flags & OPT_IGNOREERRORS) {
567 skipFile = TRUE;
568 } else {
569 ret = RC_WRITEERROR;
570 goto cleanup;
574 /* If /M supplied, remove the archive bit after successful copy */
575 if (!skipFile) {
576 if ((srcAttribs & FILE_ATTRIBUTE_ARCHIVE) &&
577 (flags & OPT_REMOVEARCH)) {
578 SetFileAttributesW(copyFrom, (srcAttribs & ~FILE_ATTRIBUTE_ARCHIVE));
580 filesCopied++;
585 /* Find next file */
586 findres = FindNextFileW(h, finddata);
588 FindClose(h);
590 /* Search 2 - do subdirs */
591 if (flags & OPT_RECURSIVE) {
592 lstrcpyW(inputpath, srcstem);
593 lstrcatW(inputpath, wchr_star);
594 findres = TRUE;
595 WINE_TRACE("Processing subdirs with spec: %s\n", wine_dbgstr_w(inputpath));
597 h = FindFirstFileW(inputpath, finddata);
598 while (h != INVALID_HANDLE_VALUE && findres) {
600 /* Only looking for dirs */
601 if ((finddata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
602 (lstrcmpW(finddata->cFileName, wchr_dot) != 0) &&
603 (lstrcmpW(finddata->cFileName, wchr_dotdot) != 0)) {
605 WINE_TRACE("Handling subdir: %s\n", wine_dbgstr_w(finddata->cFileName));
607 /* Make up recursive information */
608 lstrcpyW(inputpath, srcstem);
609 lstrcatW(inputpath, finddata->cFileName);
610 lstrcatW(inputpath, wchr_slash);
612 lstrcpyW(outputpath, deststem);
613 if (*destspec == 0x00) {
614 lstrcatW(outputpath, finddata->cFileName);
616 /* If /E is supplied, create the directory now */
617 if ((flags & OPT_EMPTYDIR) &&
618 !(flags & OPT_SIMULATE))
619 XCOPY_CreateDirectory(outputpath);
621 lstrcatW(outputpath, wchr_slash);
624 XCOPY_DoCopy(inputpath, srcspec, outputpath, destspec, flags);
627 /* Find next one */
628 findres = FindNextFileW(h, finddata);
630 FindClose(h);
633 cleanup:
635 /* free up memory */
636 HeapFree(GetProcessHeap(), 0, finddata);
637 HeapFree(GetProcessHeap(), 0, inputpath);
638 HeapFree(GetProcessHeap(), 0, outputpath);
640 return ret;
644 /* =========================================================================
645 XCOPY_ParseCommandLine - Parses the command line
646 ========================================================================= */
647 static BOOL is_whitespace(WCHAR c)
649 return c == ' ' || c == '\t';
652 static WCHAR *skip_whitespace(WCHAR *p)
654 for (; *p && is_whitespace(*p); p++);
655 return p;
658 /* Windows XCOPY uses a simplified command line parsing algorithm
659 that lacks the escaped-quote logic of build_argv(), because
660 literal double quotes are illegal in any of its arguments.
661 Example: 'XCOPY "c:\DIR A" "c:DIR B\"' is OK. */
662 static int find_end_of_word(const WCHAR *word, WCHAR **end)
664 BOOL in_quotes = 0;
665 const WCHAR *ptr = word;
666 for (;;) {
667 for (; *ptr != '\0' && *ptr != '"' &&
668 (in_quotes || !is_whitespace(*ptr)); ptr++);
669 if (*ptr == '"') {
670 in_quotes = !in_quotes;
671 ptr++;
673 /* Odd number of double quotes is illegal for XCOPY */
674 if (in_quotes && *ptr == '\0')
675 return RC_INITERROR;
676 if (*ptr == '\0' || (!in_quotes && is_whitespace(*ptr)))
677 break;
679 *end = (WCHAR*)ptr;
680 return RC_OK;
683 /* Remove all double quotes from a word */
684 static void strip_quotes(WCHAR *word, WCHAR **end)
686 WCHAR *rp, *wp;
687 for (rp = word, wp = word; *rp != '\0'; rp++) {
688 if (*rp == '"')
689 continue;
690 if (wp < rp)
691 *wp = *rp;
692 wp++;
694 *wp = '\0';
695 *end = wp;
698 static int XCOPY_ParseCommandLine(WCHAR *suppliedsource,
699 WCHAR *supplieddestination, DWORD *pflags)
701 const WCHAR EXCLUDE[] = {'E', 'X', 'C', 'L', 'U', 'D', 'E', ':', 0};
702 DWORD flags = *pflags;
703 WCHAR *cmdline, *word, *end, *next;
704 int rc = RC_INITERROR;
706 cmdline = _wcsdup(GetCommandLineW());
707 if (cmdline == NULL)
708 return rc;
710 /* Skip first arg, which is the program name */
711 if ((rc = find_end_of_word(cmdline, &word)) != RC_OK)
712 goto out;
713 word = skip_whitespace(word);
715 while (*word)
717 WCHAR first;
718 if ((rc = find_end_of_word(word, &end)) != RC_OK)
719 goto out;
721 next = skip_whitespace(end);
722 first = word[0];
723 *end = '\0';
724 strip_quotes(word, &end);
725 WINE_TRACE("Processing Arg: '%s'\n", wine_dbgstr_w(word));
727 /* First non-switch parameter is source, second is destination */
728 if (first != '/') {
729 if (suppliedsource[0] == 0x00) {
730 lstrcpyW(suppliedsource, word);
731 } else if (supplieddestination[0] == 0x00) {
732 lstrcpyW(supplieddestination, word);
733 } else {
734 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARMS));
735 goto out;
737 } else {
738 /* Process all the switch options
739 Note: Windows docs say /P prompts when dest is created
740 but tests show it is done for each src file
741 regardless of the destination */
742 switch (toupper(word[1])) {
743 case 'I': flags |= OPT_ASSUMEDIR; break;
744 case 'S': flags |= OPT_RECURSIVE; break;
745 case 'Q': flags |= OPT_QUIET; break;
746 case 'F': flags |= OPT_FULL; break;
747 case 'L': flags |= OPT_SIMULATE; break;
748 case 'W': flags |= OPT_PAUSE; break;
749 case 'T': flags |= OPT_NOCOPY | OPT_RECURSIVE; break;
750 case 'Y': flags |= OPT_NOPROMPT; break;
751 case 'N': flags |= OPT_SHORTNAME; break;
752 case 'U': flags |= OPT_MUSTEXIST; break;
753 case 'R': flags |= OPT_REPLACEREAD; break;
754 case 'H': flags |= OPT_COPYHIDSYS; break;
755 case 'C': flags |= OPT_IGNOREERRORS; break;
756 case 'P': flags |= OPT_SRCPROMPT; break;
757 case 'A': flags |= OPT_ARCHIVEONLY; break;
758 case 'M': flags |= OPT_ARCHIVEONLY |
759 OPT_REMOVEARCH; break;
761 /* E can be /E or /EXCLUDE */
762 case 'E': if (CompareStringW(LOCALE_USER_DEFAULT,
763 NORM_IGNORECASE | SORT_STRINGSORT,
764 &word[1], 8,
765 EXCLUDE, -1) == CSTR_EQUAL) {
766 if (XCOPY_ProcessExcludeList(&word[9])) {
767 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
768 goto out;
769 } else flags |= OPT_EXCLUDELIST;
770 } else flags |= OPT_EMPTYDIR | OPT_RECURSIVE;
771 break;
773 /* D can be /D or /D: */
774 case 'D': if (word[2]==':' && isdigit(word[3])) {
775 SYSTEMTIME st;
776 WCHAR *pos = &word[3];
777 BOOL isError = FALSE;
778 memset(&st, 0x00, sizeof(st));
780 /* Parse the arg : Month */
781 st.wMonth = _wtol(pos);
782 while (*pos && isdigit(*pos)) pos++;
783 if (*pos++ != '-') isError = TRUE;
785 /* Parse the arg : Day */
786 if (!isError) {
787 st.wDay = _wtol(pos);
788 while (*pos && isdigit(*pos)) pos++;
789 if (*pos++ != '-') isError = TRUE;
792 /* Parse the arg : Year */
793 if (!isError) {
794 st.wYear = _wtol(pos);
795 while (*pos && isdigit(*pos)) pos++;
796 if (st.wYear < 100) st.wYear+=2000;
799 if (!isError && SystemTimeToFileTime(&st, &dateRange)) {
800 SYSTEMTIME st;
801 WCHAR datestring[32], timestring[32];
803 flags |= OPT_DATERANGE;
805 /* Debug info: */
806 FileTimeToSystemTime (&dateRange, &st);
807 GetDateFormatW(0, DATE_SHORTDATE, &st, NULL, datestring,
808 sizeof(datestring)/sizeof(WCHAR));
809 GetTimeFormatW(0, TIME_NOSECONDS, &st,
810 NULL, timestring, sizeof(timestring)/sizeof(WCHAR));
812 WINE_TRACE("Date being used is: %s %s\n",
813 wine_dbgstr_w(datestring), wine_dbgstr_w(timestring));
814 } else {
815 XCOPY_FailMessage(ERROR_INVALID_PARAMETER);
816 goto out;
818 } else {
819 flags |= OPT_DATENEWER;
821 break;
823 case '-': if (toupper(word[2])=='Y')
824 flags &= ~OPT_NOPROMPT;
825 break;
826 case '?': XCOPY_wprintf(XCOPY_LoadMessage(STRING_HELP));
827 rc = RC_HELP;
828 goto out;
829 default:
830 WINE_TRACE("Unhandled parameter '%s'\n", wine_dbgstr_w(word));
831 XCOPY_wprintf(XCOPY_LoadMessage(STRING_INVPARM), word);
832 goto out;
835 word = next;
838 /* Default the destination if not supplied */
839 if (supplieddestination[0] == 0x00)
840 lstrcpyW(supplieddestination, wchr_dot);
842 *pflags = flags;
843 rc = RC_OK;
845 out:
846 free(cmdline);
847 return rc;
851 /* =========================================================================
852 XCOPY_ProcessSourceParm - Takes the supplied source parameter, and
853 converts it into a stem and a filespec
854 ========================================================================= */
855 static int XCOPY_ProcessSourceParm(WCHAR *suppliedsource, WCHAR *stem,
856 WCHAR *spec, DWORD flags)
858 WCHAR actualsource[MAX_PATH];
859 WCHAR *starPos;
860 WCHAR *questPos;
861 DWORD attribs;
864 * Validate the source, expanding to full path ensuring it exists
866 if (GetFullPathNameW(suppliedsource, MAX_PATH, actualsource, NULL) == 0) {
867 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
868 return RC_INITERROR;
871 /* If full names required, convert to using the full path */
872 if (flags & OPT_FULL) {
873 lstrcpyW(suppliedsource, actualsource);
877 * Work out the stem of the source
880 /* If a directory is supplied, use that as-is (either fully or
881 partially qualified)
882 If a filename is supplied + a directory or drive path, use that
883 as-is
884 Otherwise
885 If no directory or path specified, add eg. C:
886 stem is Drive/Directory is bit up to last \ (or first :)
887 spec is bit after that */
889 starPos = wcschr(suppliedsource, '*');
890 questPos = wcschr(suppliedsource, '?');
891 if (starPos || questPos) {
892 attribs = 0x00; /* Ensures skips invalid or directory check below */
893 } else {
894 attribs = GetFileAttributesW(actualsource);
897 if (attribs == INVALID_FILE_ATTRIBUTES) {
898 XCOPY_FailMessage(GetLastError());
899 return RC_INITERROR;
901 /* Directory:
902 stem should be exactly as supplied plus a '\', unless it was
903 eg. C: in which case no slash required */
904 } else if (attribs & FILE_ATTRIBUTE_DIRECTORY) {
905 WCHAR lastChar;
907 WINE_TRACE("Directory supplied\n");
908 lstrcpyW(stem, suppliedsource);
909 lastChar = stem[lstrlenW(stem)-1];
910 if (lastChar != '\\' && lastChar != ':') {
911 lstrcatW(stem, wchr_slash);
913 lstrcpyW(spec, wchr_star);
915 /* File or wildcard search:
916 stem should be:
917 Up to and including last slash if directory path supplied
918 If c:filename supplied, just the c:
919 Otherwise stem should be the current drive letter + ':' */
920 } else {
921 WCHAR *lastDir;
923 WINE_TRACE("Filename supplied\n");
924 lastDir = wcsrchr(suppliedsource, '\\');
926 if (lastDir) {
927 lstrcpyW(stem, suppliedsource);
928 stem[(lastDir-suppliedsource) + 1] = 0x00;
929 lstrcpyW(spec, (lastDir+1));
930 } else if (suppliedsource[1] == ':') {
931 lstrcpyW(stem, suppliedsource);
932 stem[2] = 0x00;
933 lstrcpyW(spec, suppliedsource+2);
934 } else {
935 WCHAR curdir[MAXSTRING];
936 GetCurrentDirectoryW(sizeof(curdir)/sizeof(WCHAR), curdir);
937 stem[0] = curdir[0];
938 stem[1] = curdir[1];
939 stem[2] = 0x00;
940 lstrcpyW(spec, suppliedsource);
944 return RC_OK;
947 /* =========================================================================
948 XCOPY_ProcessDestParm - Takes the supplied destination parameter, and
949 converts it into a stem
950 ========================================================================= */
951 static int XCOPY_ProcessDestParm(WCHAR *supplieddestination, WCHAR *stem, WCHAR *spec,
952 WCHAR *srcspec, DWORD flags)
954 WCHAR actualdestination[MAX_PATH];
955 DWORD attribs;
956 BOOL isDir = FALSE;
959 * Validate the source, expanding to full path ensuring it exists
961 if (GetFullPathNameW(supplieddestination, MAX_PATH, actualdestination, NULL) == 0) {
962 WINE_FIXME("Unexpected failure expanding source path (%d)\n", GetLastError());
963 return RC_INITERROR;
966 /* Destination is either a directory or a file */
967 attribs = GetFileAttributesW(actualdestination);
969 if (attribs == INVALID_FILE_ATTRIBUTES) {
971 /* If /I supplied and wildcard copy, assume directory */
972 /* Also if destination ends with backslash */
973 if ((flags & OPT_ASSUMEDIR &&
974 (wcschr(srcspec, '?') || wcschr(srcspec, '*'))) ||
975 (supplieddestination[lstrlenW(supplieddestination)-1] == '\\')) {
977 isDir = TRUE;
979 } else {
980 DWORD count;
981 char answer[10] = "";
982 WCHAR fileChar[2];
983 WCHAR dirChar[2];
985 /* Read the F and D characters from the resource file */
986 wcscpy(fileChar, XCOPY_LoadMessage(STRING_FILE_CHAR));
987 wcscpy(dirChar, XCOPY_LoadMessage(STRING_DIR_CHAR));
989 while (answer[0] != fileChar[0] && answer[0] != dirChar[0]) {
990 XCOPY_wprintf(XCOPY_LoadMessage(STRING_QISDIR), supplieddestination);
992 ReadFile(GetStdHandle(STD_INPUT_HANDLE), answer, sizeof(answer), &count, NULL);
993 WINE_TRACE("User answer %c\n", answer[0]);
995 answer[0] = toupper(answer[0]);
998 if (answer[0] == dirChar[0]) {
999 isDir = TRUE;
1000 } else {
1001 isDir = FALSE;
1004 } else {
1005 isDir = (attribs & FILE_ATTRIBUTE_DIRECTORY);
1008 if (isDir) {
1009 lstrcpyW(stem, actualdestination);
1010 *spec = 0x00;
1012 /* Ensure ends with a '\' */
1013 if (stem[lstrlenW(stem)-1] != '\\') {
1014 lstrcatW(stem, wchr_slash);
1017 } else {
1018 WCHAR drive[MAX_PATH];
1019 WCHAR dir[MAX_PATH];
1020 WCHAR fname[MAX_PATH];
1021 WCHAR ext[MAX_PATH];
1022 _wsplitpath(actualdestination, drive, dir, fname, ext);
1023 lstrcpyW(stem, drive);
1024 lstrcatW(stem, dir);
1025 lstrcpyW(spec, fname);
1026 lstrcatW(spec, ext);
1028 return RC_OK;
1032 /* =========================================================================
1033 main - Main entrypoint for the xcopy command
1035 Processes the args, and drives the actual copying
1036 ========================================================================= */
1037 int wmain (int argc, WCHAR *argvW[])
1039 int rc = 0;
1040 WCHAR suppliedsource[MAX_PATH] = {0}; /* As supplied on the cmd line */
1041 WCHAR supplieddestination[MAX_PATH] = {0};
1042 WCHAR sourcestem[MAX_PATH] = {0}; /* Stem of source */
1043 WCHAR sourcespec[MAX_PATH] = {0}; /* Filespec of source */
1044 WCHAR destinationstem[MAX_PATH] = {0}; /* Stem of destination */
1045 WCHAR destinationspec[MAX_PATH] = {0}; /* Filespec of destination */
1046 WCHAR copyCmd[MAXSTRING]; /* COPYCMD env var */
1047 DWORD flags = 0; /* Option flags */
1048 const WCHAR PROMPTSTR1[] = {'/', 'Y', 0};
1049 const WCHAR PROMPTSTR2[] = {'/', 'y', 0};
1050 const WCHAR COPYCMD[] = {'C', 'O', 'P', 'Y', 'C', 'M', 'D', 0};
1052 /* Preinitialize flags based on COPYCMD */
1053 if (GetEnvironmentVariableW(COPYCMD, copyCmd, MAXSTRING)) {
1054 if (wcsstr(copyCmd, PROMPTSTR1) != NULL ||
1055 wcsstr(copyCmd, PROMPTSTR2) != NULL) {
1056 flags |= OPT_NOPROMPT;
1060 /* FIXME: On UNIX, files starting with a '.' are treated as hidden under
1061 wine, but on windows these can be normal files. At least one installer
1062 uses files such as .packlist and (validly) expects them to be copied.
1063 Under wine, if we do not copy hidden files by default then they get
1064 lose */
1065 flags |= OPT_COPYHIDSYS;
1068 * Parse the command line
1070 if ((rc = XCOPY_ParseCommandLine(suppliedsource, supplieddestination,
1071 &flags)) != RC_OK) {
1072 if (rc == RC_HELP)
1073 return RC_OK;
1074 else
1075 return rc;
1078 /* Trace out the supplied information */
1079 WINE_TRACE("Supplied parameters:\n");
1080 WINE_TRACE("Source : '%s'\n", wine_dbgstr_w(suppliedsource));
1081 WINE_TRACE("Destination : '%s'\n", wine_dbgstr_w(supplieddestination));
1083 /* Extract required information from source specification */
1084 rc = XCOPY_ProcessSourceParm(suppliedsource, sourcestem, sourcespec, flags);
1085 if (rc != RC_OK) return rc;
1087 /* Extract required information from destination specification */
1088 rc = XCOPY_ProcessDestParm(supplieddestination, destinationstem,
1089 destinationspec, sourcespec, flags);
1090 if (rc != RC_OK) return rc;
1092 /* Trace out the resulting information */
1093 WINE_TRACE("Resolved parameters:\n");
1094 WINE_TRACE("Source Stem : '%s'\n", wine_dbgstr_w(sourcestem));
1095 WINE_TRACE("Source Spec : '%s'\n", wine_dbgstr_w(sourcespec));
1096 WINE_TRACE("Dest Stem : '%s'\n", wine_dbgstr_w(destinationstem));
1097 WINE_TRACE("Dest Spec : '%s'\n", wine_dbgstr_w(destinationspec));
1099 /* Pause if necessary */
1100 if (flags & OPT_PAUSE) {
1101 DWORD count;
1102 char pausestr[10];
1104 XCOPY_wprintf(XCOPY_LoadMessage(STRING_PAUSE));
1105 ReadFile (GetStdHandle(STD_INPUT_HANDLE), pausestr, sizeof(pausestr),
1106 &count, NULL);
1109 /* Now do the hard work... */
1110 rc = XCOPY_DoCopy(sourcestem, sourcespec,
1111 destinationstem, destinationspec,
1112 flags);
1114 /* Clear up exclude list allocated memory */
1115 while (excludeList) {
1116 EXCLUDELIST *pos = excludeList;
1117 excludeList = excludeList -> next;
1118 HeapFree(GetProcessHeap(), 0, pos->name);
1119 HeapFree(GetProcessHeap(), 0, pos);
1122 /* Finished - print trailer and exit */
1123 if (flags & OPT_SIMULATE) {
1124 XCOPY_wprintf(XCOPY_LoadMessage(STRING_SIMCOPY), filesCopied);
1125 } else if (!(flags & OPT_NOCOPY)) {
1126 XCOPY_wprintf(XCOPY_LoadMessage(STRING_COPY), filesCopied);
1128 if (rc == RC_OK && filesCopied == 0) rc = RC_NOFILES;
1129 return rc;